From 0d68c1283adb71265d28479422c49a575eab8457 Mon Sep 17 00:00:00 2001 From: haven <54509200+Haven-King@users.noreply.github.com> Date: Wed, 7 Oct 2026 14:03:48 -0700 Subject: [PATCH 1/3] M7: keep ligatures under small tracking Only real letterspacing (|tracking| > LIGATURE_TRACKING_LIMIT = 50/1000 em) drops standard ligatures now, and an explicit liga/clig in the character's features always wins. The OpenType panel's hint follows the same rule. --- crates/text/src/features.rs | 37 +++++++++++++++++++++++++-- crates/text/src/lib.rs | 2 +- crates/text/src/tests_typo.rs | 19 +++++++++++--- crates/ui-egui/src/panels/opentype.rs | 2 +- 4 files changed, 53 insertions(+), 7 deletions(-) diff --git a/crates/text/src/features.rs b/crates/text/src/features.rs index 9e47666d..ef446dde 100644 --- a/crates/text/src/features.rs +++ b/crates/text/src/features.rs @@ -3,13 +3,25 @@ use harfrust::{Feature, Tag}; use vectorcraft_doc::CharStyle; +/// Tracking (in 1/1000 em) beyond which standard ligatures are dropped automatically. +/// +/// Letterspaced type should not ligate: an `fi` glyph keeps its letters tight while everything +/// around it opens up, which reads as a blot. Small tracking adjustments made to fit a line +/// (a few thousandths either way) are not letterspacing and keep the ligatures, so only +/// `|tracking|` above this threshold suppresses them. An explicit `liga` / `clig` in the +/// character's own features always wins. +pub const LIGATURE_TRACKING_LIMIT: f64 = 50.0; + /// OpenType features applied during shaping. Kerning, `case` and ligature suppression are driven /// by the character style (`kerning`, `all_caps`, `tracking`); the rest are layout-wide options. +/// Standard ligatures are dropped when `|tracking|` exceeds [`LIGATURE_TRACKING_LIMIT`], unless +/// the character's features turn `liga` (or `clig`) on explicitly. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct OtFeatures { /// Vertical glyph alternates (`vert`), set by the writing direction. pub vertical: bool, - /// Standard ligatures (`liga`, `clig`). Suppressed automatically when tracking is non-zero. + /// Standard ligatures (`liga`, `clig`). Suppressed automatically when `|tracking|` exceeds + /// [`LIGATURE_TRACKING_LIMIT`], unless the character turns them on explicitly. pub ligatures: bool, /// Contextual alternates (`calt`). pub contextual: bool, @@ -117,7 +129,7 @@ impl OtFeatures { if st.kerning.is_some() { v.push(f(b"kern", false)); } - let liga = s.ligatures && st.tracking.abs() < 1e-9; + let liga = s.ligatures && (!ligatures_suppressed_by(st.tracking) || explicit_ligatures(&st.features)); if !liga { v.push(f(b"liga", false)); v.push(f(b"clig", false)); @@ -150,6 +162,27 @@ impl OtFeatures { } } +/// Does `tracking` (1/1000 em) count as letterspacing that drops standard ligatures? +pub fn ligatures_suppressed_by(tracking: f64) -> bool { + // A non-finite value is not a real letterspacing request; keep the default. + tracking.is_finite() && tracking.abs() > LIGATURE_TRACKING_LIMIT +} + +/// Do `features` turn standard ligatures on explicitly (`liga`, `+liga`, `clig`, `+clig`)? The last +/// ligature tag wins, matching [`OtFeatures::with_tags`]. Absence of a tag means "default", which +/// [`OtFeatures::to_tags`] never writes, so this is distinguishable from an explicit request. +pub fn explicit_ligatures>(features: &[S]) -> bool { + features + .iter() + .rev() + .find_map(|t| match t.as_ref() { + "liga" | "+liga" | "clig" | "+clig" => Some(true), + "-liga" | "-clig" => Some(false), + _ => None, + }) + .unwrap_or(false) +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/text/src/lib.rs b/crates/text/src/lib.rs index 7833601a..cb79ed0f 100644 --- a/crates/text/src/lib.rs +++ b/crates/text/src/lib.rs @@ -23,7 +23,7 @@ pub mod test_fonts; pub mod thread; pub use craft_fonts::{CRAFT_FONTS, CraftFont}; -pub use features::OtFeatures; +pub use features::{LIGATURE_TRACKING_LIMIT, OtFeatures, explicit_ligatures, ligatures_suppressed_by}; pub use fontdb::{FALLBACK_FAMILY, FontClass, FontDb, FontFace, FontMatch, FontTraits, style_weight, system_font_dirs}; use kurbo::{Affine, BezPath, Point, Rect, Vec2}; pub use layout::{layout, layout_with}; diff --git a/crates/text/src/tests_typo.rs b/crates/text/src/tests_typo.rs index dcd91800..a486224f 100644 --- a/crates/text/src/tests_typo.rs +++ b/crates/text/src/tests_typo.rs @@ -370,9 +370,22 @@ fn opentype_ligature_switches() { assert_eq!(layout(db(), &t).glyphs.len(), 1, "fi ligature by default"); let off = LayoutOptions { features: OtFeatures { ligatures: false, ..Default::default() }, ..Default::default() }; assert_eq!(layout_with(db(), &t, &off).glyphs.len(), 2); - // Tracking suppresses ligatures (as letterspaced type should). - let tracked = TextObject::point(Point::ZERO, "fi", CharStyle { tracking: 100.0, ..serif }); - assert_eq!(layout(db(), &tracked).glyphs.len(), 2); + let fi = |tracking: f64, features: &[&str]| { + let st = CharStyle { tracking, features: features.iter().map(|s| s.to_string()).collect(), ..serif.clone() }; + layout(db(), &TextObject::point(Point::ZERO, "fi", st)).glyphs.len() + }; + // Small tracking adjustments (fitting a line) keep the ligature. + assert_eq!(fi(-5.0, &[]), 1, "tracking -5 keeps fi"); + assert_eq!(fi(20.0, &[]), 1, "tracking +20 keeps fi"); + assert_eq!(fi(LIGATURE_TRACKING_LIMIT, &[]), 1, "the limit itself keeps fi"); + // Real letterspacing suppresses ligatures (as letterspaced type should)... + assert_eq!(fi(100.0, &[]), 2, "tracking 100 drops fi"); + assert_eq!(fi(-100.0, &[]), 2, "tracking -100 drops fi"); + // ...unless the character turns them on explicitly. + assert_eq!(fi(100.0, &["liga"]), 1, "explicit liga wins over tracking"); + assert_eq!(fi(100.0, &["+liga"]), 1); + assert_eq!(fi(100.0, &["liga", "-liga"]), 2, "the last tag wins"); + assert_eq!(fi(0.0, &["-liga"]), 2, "explicit off still turns it off"); let f = OtFeatures::from_tags(["dlig", "-liga", "smcp", "onum", "bogus"]); assert!(f.discretionary_ligatures && !f.ligatures && f.small_caps && f.oldstyle_figures && !f.fractions); } diff --git a/crates/ui-egui/src/panels/opentype.rs b/crates/ui-egui/src/panels/opentype.rs index 9f737233..2be67f8e 100644 --- a/crates/ui-egui/src/panels/opentype.rs +++ b/crates/ui-egui/src/panels/opentype.rs @@ -63,7 +63,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { *field(&mut next) = !on; } } - if st.tracking.abs() > 1e-9 && cur.ligatures { + if cur.ligatures && vectorcraft_text::ligatures_suppressed_by(st.tracking) && !vectorcraft_text::explicit_ligatures(&st.features) { widgets::dim_label(ui, tl!("Ligatures are off while the text is tracked.")); } if next != cur { From ba93f62bcb954b015a5d614fbb4e288dd9267085 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Fri, 9 Oct 2026 03:48:27 +0700 Subject: [PATCH 2/3] Fix #577: the gradient stop popover stays open after the click that opens it The Gradient panel's colour chip opens the stop popover on a click. The popover closes on a click elsewhere, and the opening click itself, which is released in the frame the popover first draws and lies outside it, counted as one: the popover closed at once (a double-click got through, as the popover lets the double-click that opens it from the canvas pass). The popover now records the frame it first drew in and only a click in a later frame closes it. Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 2 +- crates/ui-egui/src/dialogs/gradient_stop.rs | 50 ++++++++++++++++++--- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 1b4c4fa7..4e3852de 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -4,7 +4,7 @@ VectorCraft is a clean-room, open-source, pure-Rust reimplementation of the Adob This file tracks **how far we are and what's left**. Time estimates are wall-clock hours of continuous Claude Opus 5.5 agent work (including builds and the CI gate), given both for **one agent** and for **4–6 parallel agents** on disjoint crates. They are counted from the remaining work (see [Parity estimate](#parity-estimate)), calibrated against measured throughput, and updated as work lands. -_Last updated: 2026-10-09 (after placed documents (File › Place of a `.vectorcraft` file, linked), M4.14–M4.98, M8.1–M8.20, M14.4–M14.7, the MCP prompts, resource templates, completions and logging, the 2026-10-07 issue fixes, the Layers panel rework and the 2026-10-08 community contributions: Pen and shape-tool modifiers, path editing, Layers, Artboards, Swatches/Brushes and dialogs, Japanese composition and PDF text, Hebrew/Arabic type, interface languages (Spanish among them), the font menu, saved selections, MCP and EPS, the missing-font export warning (#319), open paths in the Shape Builder (#374), PDF/`.ai` import keeping strokes live and hidden and nested layers (#372), per-corner Live Corners (#381), SVG type opening at the size it draws at (#396), the Wayland clipboard with copied-file paste (#398), ruler guides that can be selected, moved and deleted (#414), two batches of Preferences options that were stored but unused (#394), skeleton reads with `document.find` for large documents over MCP (#416) and anchors dragged with Direct Selection snapping to points, segments and smart guides, clipped SVG layers opening as clipping layers (#420), PDF/`.ai` layers read on pages of tens of millions of operators (#419), Document Color Mode putting RGB greys on the black plate on request and converting pattern tiles (#421) and movable dialogs with Offset Path's Joins dropdown (#423) and numeric fields scrubbed by dragging their label (#400), stroke weight presets in the stroke unit (#426), the Appearance panel's Opacity popup (#430), type on a path brackets, flip and Type on a Path Options that keep the type in place (#429), Layers rows dragged with a ghost and dropped on the trash or New Layer (#435), tool flyouts opening on a right or corner press (#436), tool flyouts torn off into floating toolbars (#437), the Shape Builder's erase mode deleting edges too (#439), installed fonts registered outside the font folders and fonts installed while the app runs listed in the font menus (#443), alike circular corners on rectangles that aren't square (#442), tool cursors as OS cursors so the pointer no longer trails over the canvas (#444), artboards copied and pasted with their art (#446), guides dragged out of the rulers snapping to the art's side midpoints and the artboards, artboard guides (#451), the toolbar's double arrow and Window › Toolbars › Double Column switching between one and two columns in any window height (#455), the Smart Guides display options and Snapping Tolerance (#394), Font Size and Leading combo boxes with preset lists in the Character and Properties panels and the Control bar (#458), screenshots pasted from the clipboard on macOS, Windows and the web (#457), the Properties panel's Fill and Stroke matching the Control bar's, Illustrator's Window menu panel keys (Shift+F7 for Align…) and Alt-wheel zoom whatever the wheel preference (Discord feedback), the point/area type widget (Discord feedback), groups in legacy Illustrator .ai files (Discord feedback), a rotating desktop log file (#465), image controls in the Control bar and Properties panel: Image Trace with its presets, Mask, Crop Image, Embed/Unembed, and an Image Trace object's preset, Expand and Release (Discord feedback), the Pen working as the Anchor Point tool with Alt held over a selected path's handles and anchors, and anchor buttons that convert, connect and cut paths at the selected anchors (Discord feedback), Japanese hanging punctuation (burasagari, #463), Escape leaving Presentation Mode with no screen mode saved across restarts, documents dropped on the desktop app opening as tabs of their own and a note that an `.ai` file's art off its artboards isn't in the part read (#472), Add/Delete Anchor Point tool icons drawn from the Pen tool's (#479), EPS import running more of PostScript Level 3 (patterns, Type 3 fonts, mesh shadings, masked images, VM and resource probes), and a preview that stands in saying why (Discord feedback, #466, #469, #474), chords with modifiers recorded in Edit › Keyboard Shortcuts (#487), Type Object Selection by Path Only (#394), Anti-aliased Artwork (#394), Show Pixel Grid (#394), pen and touch taps pressed and lifted within one frame clicking where they tapped and the Linux/Wayland pen-tablet workaround (#491), bounding-box resizes landing on the other objects' anchors and centres with labels, and the Rotate, Scale, Shear and Reflect tools snapping their drags (#482), Shift-drag marquees that toggle the objects or anchors they reach and a Lasso whose Alt subtraction keeps the rest of the selection (#483), the mouse wheel stepping the focused numeric field, the dialogs' and preferences' drag values made such fields (#485), Show The Home Screen When No Documents Are Open (#394), Numbers Without Units Are Points (#394), Auto-Collapse Iconic Panels (#394), the Contextual Task Bar moved by its handle and its More Options menu pinning and resetting its position, a designed macOS DMG window (#493), panels, panel groups and the Tools panel undocking into floating panels that move, stack and dock again (#495), the 0.5.0 startup crash on dual-GPU Linux desktops under Wayland fixed: the desktop's own GPU by default and a restart on the next GPU when one can't show the window (#502), path editing while drawing: Cmd/Ctrl with the Pen borrowing Direct Selection and drawing on afterwards, Pen+Alt reshaping segments, handles of a whole selected path dragging, and handle drags no longer re-gathering snap targets every move (#494), the Control bar's and Properties panel's Convert buttons for a path selected as a whole with Direct Selection, the Pen and the anchor tools (#504), the Pen closing a path dragged out of its first anchor every time, without flicker (#501), the Paste menu checking the system clipboard on a background thread on Linux so an X11 clipboard owner that never answers no longer freezes the window (#510), PDF/`.ai` import keeping a type object's outline stroke on the type, wrapped lines as area type, barcode digits in place and layers in the file's stacking order (#508), the Italian interface (#514), EPS images with several data sources read in turn (#498), `restore` undoing dictionary changes in EPS import (part of #505), the macOS menu bar laid out as a Mac app's (the VectorCraft menu with Settings, Language and Appearance, the system's Hide, Services and Window items; owner's request), Object › Image Trace › Make tracing with the Default preset instead of asking for a preset's name (#543), the rulers' unit menu (#528), Live Corners on stars, polygons and pen paths (#511), the drawing tools snapping to Smart Guides as the Selection tool does, with Construction Guides (#506), the in-window menus' Select All, Cut, Copy and Paste acting on the text field they were opened from (#516), Windows rendering through DX12 before Vulkan (#545), Image Trace tracing colour layers in parallel and refusing traces too large for memory, with the canvas outlining huge selections by their bounds (#525), SVGs of chosen artboards holding only the art over each artboard (#550), the font menu scrolling only its list, under a search field and filters that stay put (#555), a click on an object of a selection making it the key object that Align aligns to and Distribute Spacing spaces from, drawn with a thicker outline (#541), Shape Builder merges taken from the planar map itself, so merged regions leave no inner edge, hairline gap or spur in their outline (#569), `.ai` written by the exports and the CLI (`--export`, `convert`) as Save As writes it (#526); see [Honest assessment](#honest-assessment-2026-10-05))._ +_Last updated: 2026-10-09 (after placed documents (File › Place of a `.vectorcraft` file, linked), M4.14–M4.98, M8.1–M8.20, M14.4–M14.7, the MCP prompts, resource templates, completions and logging, the 2026-10-07 issue fixes, the Layers panel rework and the 2026-10-08 community contributions: Pen and shape-tool modifiers, path editing, Layers, Artboards, Swatches/Brushes and dialogs, Japanese composition and PDF text, Hebrew/Arabic type, interface languages (Spanish among them), the font menu, saved selections, MCP and EPS, the missing-font export warning (#319), open paths in the Shape Builder (#374), PDF/`.ai` import keeping strokes live and hidden and nested layers (#372), per-corner Live Corners (#381), SVG type opening at the size it draws at (#396), the Wayland clipboard with copied-file paste (#398), ruler guides that can be selected, moved and deleted (#414), two batches of Preferences options that were stored but unused (#394), skeleton reads with `document.find` for large documents over MCP (#416) and anchors dragged with Direct Selection snapping to points, segments and smart guides, clipped SVG layers opening as clipping layers (#420), PDF/`.ai` layers read on pages of tens of millions of operators (#419), Document Color Mode putting RGB greys on the black plate on request and converting pattern tiles (#421) and movable dialogs with Offset Path's Joins dropdown (#423) and numeric fields scrubbed by dragging their label (#400), stroke weight presets in the stroke unit (#426), the Appearance panel's Opacity popup (#430), type on a path brackets, flip and Type on a Path Options that keep the type in place (#429), Layers rows dragged with a ghost and dropped on the trash or New Layer (#435), tool flyouts opening on a right or corner press (#436), tool flyouts torn off into floating toolbars (#437), the Shape Builder's erase mode deleting edges too (#439), installed fonts registered outside the font folders and fonts installed while the app runs listed in the font menus (#443), alike circular corners on rectangles that aren't square (#442), tool cursors as OS cursors so the pointer no longer trails over the canvas (#444), artboards copied and pasted with their art (#446), guides dragged out of the rulers snapping to the art's side midpoints and the artboards, artboard guides (#451), the toolbar's double arrow and Window › Toolbars › Double Column switching between one and two columns in any window height (#455), the Smart Guides display options and Snapping Tolerance (#394), Font Size and Leading combo boxes with preset lists in the Character and Properties panels and the Control bar (#458), screenshots pasted from the clipboard on macOS, Windows and the web (#457), the Properties panel's Fill and Stroke matching the Control bar's, Illustrator's Window menu panel keys (Shift+F7 for Align…) and Alt-wheel zoom whatever the wheel preference (Discord feedback), the point/area type widget (Discord feedback), groups in legacy Illustrator .ai files (Discord feedback), a rotating desktop log file (#465), image controls in the Control bar and Properties panel: Image Trace with its presets, Mask, Crop Image, Embed/Unembed, and an Image Trace object's preset, Expand and Release (Discord feedback), the Pen working as the Anchor Point tool with Alt held over a selected path's handles and anchors, and anchor buttons that convert, connect and cut paths at the selected anchors (Discord feedback), Japanese hanging punctuation (burasagari, #463), Escape leaving Presentation Mode with no screen mode saved across restarts, documents dropped on the desktop app opening as tabs of their own and a note that an `.ai` file's art off its artboards isn't in the part read (#472), Add/Delete Anchor Point tool icons drawn from the Pen tool's (#479), EPS import running more of PostScript Level 3 (patterns, Type 3 fonts, mesh shadings, masked images, VM and resource probes), and a preview that stands in saying why (Discord feedback, #466, #469, #474), chords with modifiers recorded in Edit › Keyboard Shortcuts (#487), Type Object Selection by Path Only (#394), Anti-aliased Artwork (#394), Show Pixel Grid (#394), pen and touch taps pressed and lifted within one frame clicking where they tapped and the Linux/Wayland pen-tablet workaround (#491), bounding-box resizes landing on the other objects' anchors and centres with labels, and the Rotate, Scale, Shear and Reflect tools snapping their drags (#482), Shift-drag marquees that toggle the objects or anchors they reach and a Lasso whose Alt subtraction keeps the rest of the selection (#483), the mouse wheel stepping the focused numeric field, the dialogs' and preferences' drag values made such fields (#485), Show The Home Screen When No Documents Are Open (#394), Numbers Without Units Are Points (#394), Auto-Collapse Iconic Panels (#394), the Contextual Task Bar moved by its handle and its More Options menu pinning and resetting its position, a designed macOS DMG window (#493), panels, panel groups and the Tools panel undocking into floating panels that move, stack and dock again (#495), the 0.5.0 startup crash on dual-GPU Linux desktops under Wayland fixed: the desktop's own GPU by default and a restart on the next GPU when one can't show the window (#502), path editing while drawing: Cmd/Ctrl with the Pen borrowing Direct Selection and drawing on afterwards, Pen+Alt reshaping segments, handles of a whole selected path dragging, and handle drags no longer re-gathering snap targets every move (#494), the Control bar's and Properties panel's Convert buttons for a path selected as a whole with Direct Selection, the Pen and the anchor tools (#504), the Pen closing a path dragged out of its first anchor every time, without flicker (#501), the Paste menu checking the system clipboard on a background thread on Linux so an X11 clipboard owner that never answers no longer freezes the window (#510), PDF/`.ai` import keeping a type object's outline stroke on the type, wrapped lines as area type, barcode digits in place and layers in the file's stacking order (#508), the Italian interface (#514), EPS images with several data sources read in turn (#498), `restore` undoing dictionary changes in EPS import (part of #505), the macOS menu bar laid out as a Mac app's (the VectorCraft menu with Settings, Language and Appearance, the system's Hide, Services and Window items; owner's request), Object › Image Trace › Make tracing with the Default preset instead of asking for a preset's name (#543), the rulers' unit menu (#528), Live Corners on stars, polygons and pen paths (#511), the drawing tools snapping to Smart Guides as the Selection tool does, with Construction Guides (#506), the in-window menus' Select All, Cut, Copy and Paste acting on the text field they were opened from (#516), Windows rendering through DX12 before Vulkan (#545), Image Trace tracing colour layers in parallel and refusing traces too large for memory, with the canvas outlining huge selections by their bounds (#525), SVGs of chosen artboards holding only the art over each artboard (#550), the font menu scrolling only its list, under a search field and filters that stay put (#555), a click on an object of a selection making it the key object that Align aligns to and Distribute Spacing spaces from, drawn with a thicker outline (#541), Shape Builder merges taken from the planar map itself, so merged regions leave no inner edge, hairline gap or spur in their outline (#569), `.ai` written by the exports and the CLI (`--export`, `convert`) as Save As writes it (#526), the gradient stop popover staying open after the click that opens it from the Gradient panel (#577); see [Honest assessment](#honest-assessment-2026-10-05))._ ## Where we are diff --git a/crates/ui-egui/src/dialogs/gradient_stop.rs b/crates/ui-egui/src/dialogs/gradient_stop.rs index 4d102f33..7282415a 100644 --- a/crates/ui-egui/src/dialogs/gradient_stop.rs +++ b/crates/ui-egui/src/dialogs/gradient_stop.rs @@ -29,6 +29,9 @@ use crate::widgets::{self, Live}; pub(super) const SPEC: DialogSpec = DialogSpec::window(show, confirm); +/// The field holding the frame the popover was first drawn in. +const OPENED: &str = "__opened"; + /// What the popover edits: the selected stop of the gradient behind the active proxy (with all /// its stops), or the selected point of a freeform gradient. enum Target { @@ -131,12 +134,18 @@ fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { }); }) .response; - if let (Some(tab), Some(d)) = (tab, app.ui.dialog.as_mut()) { - d.fields.insert("tab".into(), json!(tab)); + // The frame it opened in (its first drawn). + let frame = ctx.cumulative_frame_nr(); + let opened = d.fields.get(OPENED).and_then(Value::as_u64).unwrap_or(frame); + if let Some(d) = app.ui.dialog.as_mut() { + if let Some(tab) = tab { + d.fields.insert("tab".into(), json!(tab)); + } + d.fields.insert(OPENED.into(), json!(opened)); } - // The double-click that opened the popover lands on its chip, outside it: only a later click - // elsewhere closes it. - if resp.clicked_elsewhere() && !ctx.input(|i| i.pointer.button_double_clicked(egui::PointerButton::Primary)) { + // The click that opened the popover (the Gradient panel's colour chip) or the double-click + // (a stop's chip) lands outside it: only a later click elsewhere closes it (#577). + if frame > opened && resp.clicked_elsewhere() && !ctx.input(|i| i.pointer.button_double_clicked(egui::PointerButton::Primary)) { app.ui.dialog = None; } } @@ -246,10 +255,18 @@ mod tests { } } + thread_local! { + // One context per test, so frames follow one another as in the app. + static CTX: egui::Context = { + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + ctx + }; + } + /// One headless frame of the dialog layer and the shortcuts. fn frame(app: &mut VectorcraftApp, events: Vec) { - let ctx = egui::Context::default(); - crate::theme::install_fonts(&ctx); + let ctx = CTX.with(Clone::clone); let mut out = ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| { crate::shortcuts::handle(app, ui.ctx()); super::super::show(app, ui.ctx()); @@ -312,6 +329,25 @@ mod tests { assert!(app.ui.dialog.is_none(), "a click elsewhere closes it"); } + /// #577: the Gradient panel's colour chip opens it with a click whose release lands in the + /// frame it opens in, outside it: that click doesn't close it. + #[test] + fn the_click_that_opens_it_from_the_panel_keeps_it_open() { + let mut app = app(); + let at = egui::pos2(5.0, 5.0); + frame(&mut app, vec![egui::Event::PointerMoved(at), click(at, true)]); + // The panel opens it while the click is released. + app.ui.dialog = Some(Dialog::new("gradientStop", json!({"index": 1, "screen": [200.0, 200.0], "tab": "color"}))); + frame(&mut app, vec![click(at, false)]); + for _ in 0..3 { + frame(&mut app, vec![]); + } + assert!(app.ui.dialog.is_some(), "the opening click must not close it"); + let away = egui::pos2(5.0, 600.0); + frame(&mut app, vec![egui::Event::PointerMoved(away), click(away, true), click(away, false)]); + assert!(app.ui.dialog.is_none(), "a later click elsewhere closes it"); + } + #[test] fn the_popover_closes_when_its_stop_goes_away() { let mut app = app(); From 5a09d1aef85f265d57fc2c2be0194e3026f0fe92 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Fri, 9 Oct 2026 03:56:10 +0700 Subject: [PATCH 3/3] Type: ligatures drop past -20 or +55 tracking, as Adobe's composers do #583 kept ligatures within +-50/1000 em. Adobe's composers are described as dropping them past about -8% and +22% of the word space (John Hudson, www-style, 2012-07): about -20 and +55 for a typical 250/1000 em space. Condensed type shows a fixed ligature sooner than expanded type does. Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 2 +- crates/text/src/features.rs | 23 +++++++++++++---------- crates/text/src/lib.rs | 2 +- crates/text/src/tests_typo.rs | 6 ++++-- 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index bf9d85b0..962fb51b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -4,7 +4,7 @@ VectorCraft is a clean-room, open-source, pure-Rust reimplementation of the Adob This file tracks **how far we are and what's left**. Time estimates are wall-clock hours of continuous Claude Opus 5.5 agent work (including builds and the CI gate), given both for **one agent** and for **4–6 parallel agents** on disjoint crates. They are counted from the remaining work (see [Parity estimate](#parity-estimate)), calibrated against measured throughput, and updated as work lands. -_Last updated: 2026-10-09 (after placed documents (File › Place of a `.vectorcraft` file, linked), M4.14–M4.98, M8.1–M8.20, M14.4–M14.7, the MCP prompts, resource templates, completions and logging, the 2026-10-07 issue fixes, the Layers panel rework and the 2026-10-08 community contributions: Pen and shape-tool modifiers, path editing, Layers, Artboards, Swatches/Brushes and dialogs, Japanese composition and PDF text, Hebrew/Arabic type, interface languages (Spanish among them), the font menu, saved selections, MCP and EPS, the missing-font export warning (#319), open paths in the Shape Builder (#374), PDF/`.ai` import keeping strokes live and hidden and nested layers (#372), per-corner Live Corners (#381), SVG type opening at the size it draws at (#396), the Wayland clipboard with copied-file paste (#398), ruler guides that can be selected, moved and deleted (#414), two batches of Preferences options that were stored but unused (#394), skeleton reads with `document.find` for large documents over MCP (#416) and anchors dragged with Direct Selection snapping to points, segments and smart guides, clipped SVG layers opening as clipping layers (#420), PDF/`.ai` layers read on pages of tens of millions of operators (#419), Document Color Mode putting RGB greys on the black plate on request and converting pattern tiles (#421) and movable dialogs with Offset Path's Joins dropdown (#423) and numeric fields scrubbed by dragging their label (#400), stroke weight presets in the stroke unit (#426), the Appearance panel's Opacity popup (#430), type on a path brackets, flip and Type on a Path Options that keep the type in place (#429), Layers rows dragged with a ghost and dropped on the trash or New Layer (#435), tool flyouts opening on a right or corner press (#436), tool flyouts torn off into floating toolbars (#437), the Shape Builder's erase mode deleting edges too (#439), installed fonts registered outside the font folders and fonts installed while the app runs listed in the font menus (#443), alike circular corners on rectangles that aren't square (#442), tool cursors as OS cursors so the pointer no longer trails over the canvas (#444), artboards copied and pasted with their art (#446), guides dragged out of the rulers snapping to the art's side midpoints and the artboards, artboard guides (#451), the toolbar's double arrow and Window › Toolbars › Double Column switching between one and two columns in any window height (#455), the Smart Guides display options and Snapping Tolerance (#394), Font Size and Leading combo boxes with preset lists in the Character and Properties panels and the Control bar (#458), screenshots pasted from the clipboard on macOS, Windows and the web (#457), the Properties panel's Fill and Stroke matching the Control bar's, Illustrator's Window menu panel keys (Shift+F7 for Align…) and Alt-wheel zoom whatever the wheel preference (Discord feedback), the point/area type widget (Discord feedback), groups in legacy Illustrator .ai files (Discord feedback), a rotating desktop log file (#465), image controls in the Control bar and Properties panel: Image Trace with its presets, Mask, Crop Image, Embed/Unembed, and an Image Trace object's preset, Expand and Release (Discord feedback), the Pen working as the Anchor Point tool with Alt held over a selected path's handles and anchors, and anchor buttons that convert, connect and cut paths at the selected anchors (Discord feedback), Japanese hanging punctuation (burasagari, #463), Escape leaving Presentation Mode with no screen mode saved across restarts, documents dropped on the desktop app opening as tabs of their own and a note that an `.ai` file's art off its artboards isn't in the part read (#472), Add/Delete Anchor Point tool icons drawn from the Pen tool's (#479), EPS import running more of PostScript Level 3 (patterns, Type 3 fonts, mesh shadings, masked images, VM and resource probes), and a preview that stands in saying why (Discord feedback, #466, #469, #474), chords with modifiers recorded in Edit › Keyboard Shortcuts (#487), Type Object Selection by Path Only (#394), Anti-aliased Artwork (#394), Show Pixel Grid (#394), pen and touch taps pressed and lifted within one frame clicking where they tapped and the Linux/Wayland pen-tablet workaround (#491), bounding-box resizes landing on the other objects' anchors and centres with labels, and the Rotate, Scale, Shear and Reflect tools snapping their drags (#482), Shift-drag marquees that toggle the objects or anchors they reach and a Lasso whose Alt subtraction keeps the rest of the selection (#483), the mouse wheel stepping the focused numeric field, the dialogs' and preferences' drag values made such fields (#485), Show The Home Screen When No Documents Are Open (#394), Numbers Without Units Are Points (#394), Auto-Collapse Iconic Panels (#394), the Contextual Task Bar moved by its handle and its More Options menu pinning and resetting its position, a designed macOS DMG window (#493), panels, panel groups and the Tools panel undocking into floating panels that move, stack and dock again (#495), the 0.5.0 startup crash on dual-GPU Linux desktops under Wayland fixed: the desktop's own GPU by default and a restart on the next GPU when one can't show the window (#502), path editing while drawing: Cmd/Ctrl with the Pen borrowing Direct Selection and drawing on afterwards, Pen+Alt reshaping segments, handles of a whole selected path dragging, and handle drags no longer re-gathering snap targets every move (#494), the Control bar's and Properties panel's Convert buttons for a path selected as a whole with Direct Selection, the Pen and the anchor tools (#504), the Pen closing a path dragged out of its first anchor every time, without flicker (#501), the Paste menu checking the system clipboard on a background thread on Linux so an X11 clipboard owner that never answers no longer freezes the window (#510), PDF/`.ai` import keeping a type object's outline stroke on the type, wrapped lines as area type, barcode digits in place and layers in the file's stacking order (#508), the Italian interface (#514), EPS images with several data sources read in turn (#498), `restore` undoing dictionary changes in EPS import (part of #505), the macOS menu bar laid out as a Mac app's (the VectorCraft menu with Settings, Language and Appearance, the system's Hide, Services and Window items; owner's request), Object › Image Trace › Make tracing with the Default preset instead of asking for a preset's name (#543), the rulers' unit menu (#528), Live Corners on stars, polygons and pen paths (#511), the drawing tools snapping to Smart Guides as the Selection tool does, with Construction Guides (#506), the in-window menus' Select All, Cut, Copy and Paste acting on the text field they were opened from (#516), Windows rendering through DX12 before Vulkan (#545), Image Trace tracing colour layers in parallel and refusing traces too large for memory, with the canvas outlining huge selections by their bounds (#525), SVGs of chosen artboards holding only the art over each artboard (#550), the font menu scrolling only its list, under a search field and filters that stay put (#555), a click on an object of a selection making it the key object that Align aligns to and Distribute Spacing spaces from, drawn with a thicker outline (#541), Shape Builder merges taken from the planar map itself, so merged regions leave no inner edge, hairline gap or spur in their outline (#569), `.ai` written by the exports and the CLI (`--export`, `convert`) as Save As writes it (#526), SVG round trips keeping hidden objects and `data-*` attributes (#522), the gradient stop popover staying open after the click that opens it from the Gradient panel (#577); see [Honest assessment](#honest-assessment-2026-10-05))._ +_Last updated: 2026-10-09 (after placed documents (File › Place of a `.vectorcraft` file, linked), M4.14–M4.98, M8.1–M8.20, M14.4–M14.7, the MCP prompts, resource templates, completions and logging, the 2026-10-07 issue fixes, the Layers panel rework and the 2026-10-08 community contributions: Pen and shape-tool modifiers, path editing, Layers, Artboards, Swatches/Brushes and dialogs, Japanese composition and PDF text, Hebrew/Arabic type, interface languages (Spanish among them), the font menu, saved selections, MCP and EPS, the missing-font export warning (#319), open paths in the Shape Builder (#374), PDF/`.ai` import keeping strokes live and hidden and nested layers (#372), per-corner Live Corners (#381), SVG type opening at the size it draws at (#396), the Wayland clipboard with copied-file paste (#398), ruler guides that can be selected, moved and deleted (#414), two batches of Preferences options that were stored but unused (#394), skeleton reads with `document.find` for large documents over MCP (#416) and anchors dragged with Direct Selection snapping to points, segments and smart guides, clipped SVG layers opening as clipping layers (#420), PDF/`.ai` layers read on pages of tens of millions of operators (#419), Document Color Mode putting RGB greys on the black plate on request and converting pattern tiles (#421) and movable dialogs with Offset Path's Joins dropdown (#423) and numeric fields scrubbed by dragging their label (#400), stroke weight presets in the stroke unit (#426), the Appearance panel's Opacity popup (#430), type on a path brackets, flip and Type on a Path Options that keep the type in place (#429), Layers rows dragged with a ghost and dropped on the trash or New Layer (#435), tool flyouts opening on a right or corner press (#436), tool flyouts torn off into floating toolbars (#437), the Shape Builder's erase mode deleting edges too (#439), installed fonts registered outside the font folders and fonts installed while the app runs listed in the font menus (#443), alike circular corners on rectangles that aren't square (#442), tool cursors as OS cursors so the pointer no longer trails over the canvas (#444), artboards copied and pasted with their art (#446), guides dragged out of the rulers snapping to the art's side midpoints and the artboards, artboard guides (#451), the toolbar's double arrow and Window › Toolbars › Double Column switching between one and two columns in any window height (#455), the Smart Guides display options and Snapping Tolerance (#394), Font Size and Leading combo boxes with preset lists in the Character and Properties panels and the Control bar (#458), screenshots pasted from the clipboard on macOS, Windows and the web (#457), the Properties panel's Fill and Stroke matching the Control bar's, Illustrator's Window menu panel keys (Shift+F7 for Align…) and Alt-wheel zoom whatever the wheel preference (Discord feedback), the point/area type widget (Discord feedback), groups in legacy Illustrator .ai files (Discord feedback), a rotating desktop log file (#465), image controls in the Control bar and Properties panel: Image Trace with its presets, Mask, Crop Image, Embed/Unembed, and an Image Trace object's preset, Expand and Release (Discord feedback), the Pen working as the Anchor Point tool with Alt held over a selected path's handles and anchors, and anchor buttons that convert, connect and cut paths at the selected anchors (Discord feedback), Japanese hanging punctuation (burasagari, #463), Escape leaving Presentation Mode with no screen mode saved across restarts, documents dropped on the desktop app opening as tabs of their own and a note that an `.ai` file's art off its artboards isn't in the part read (#472), Add/Delete Anchor Point tool icons drawn from the Pen tool's (#479), EPS import running more of PostScript Level 3 (patterns, Type 3 fonts, mesh shadings, masked images, VM and resource probes), and a preview that stands in saying why (Discord feedback, #466, #469, #474), chords with modifiers recorded in Edit › Keyboard Shortcuts (#487), Type Object Selection by Path Only (#394), Anti-aliased Artwork (#394), Show Pixel Grid (#394), pen and touch taps pressed and lifted within one frame clicking where they tapped and the Linux/Wayland pen-tablet workaround (#491), bounding-box resizes landing on the other objects' anchors and centres with labels, and the Rotate, Scale, Shear and Reflect tools snapping their drags (#482), Shift-drag marquees that toggle the objects or anchors they reach and a Lasso whose Alt subtraction keeps the rest of the selection (#483), the mouse wheel stepping the focused numeric field, the dialogs' and preferences' drag values made such fields (#485), Show The Home Screen When No Documents Are Open (#394), Numbers Without Units Are Points (#394), Auto-Collapse Iconic Panels (#394), the Contextual Task Bar moved by its handle and its More Options menu pinning and resetting its position, a designed macOS DMG window (#493), panels, panel groups and the Tools panel undocking into floating panels that move, stack and dock again (#495), the 0.5.0 startup crash on dual-GPU Linux desktops under Wayland fixed: the desktop's own GPU by default and a restart on the next GPU when one can't show the window (#502), path editing while drawing: Cmd/Ctrl with the Pen borrowing Direct Selection and drawing on afterwards, Pen+Alt reshaping segments, handles of a whole selected path dragging, and handle drags no longer re-gathering snap targets every move (#494), the Control bar's and Properties panel's Convert buttons for a path selected as a whole with Direct Selection, the Pen and the anchor tools (#504), the Pen closing a path dragged out of its first anchor every time, without flicker (#501), the Paste menu checking the system clipboard on a background thread on Linux so an X11 clipboard owner that never answers no longer freezes the window (#510), PDF/`.ai` import keeping a type object's outline stroke on the type, wrapped lines as area type, barcode digits in place and layers in the file's stacking order (#508), the Italian interface (#514), EPS images with several data sources read in turn (#498), `restore` undoing dictionary changes in EPS import (part of #505), the macOS menu bar laid out as a Mac app's (the VectorCraft menu with Settings, Language and Appearance, the system's Hide, Services and Window items; owner's request), Object › Image Trace › Make tracing with the Default preset instead of asking for a preset's name (#543), the rulers' unit menu (#528), Live Corners on stars, polygons and pen paths (#511), the drawing tools snapping to Smart Guides as the Selection tool does, with Construction Guides (#506), the in-window menus' Select All, Cut, Copy and Paste acting on the text field they were opened from (#516), Windows rendering through DX12 before Vulkan (#545), Image Trace tracing colour layers in parallel and refusing traces too large for memory, with the canvas outlining huge selections by their bounds (#525), SVGs of chosen artboards holding only the art over each artboard (#550), the font menu scrolling only its list, under a search field and filters that stay put (#555), a click on an object of a selection making it the key object that Align aligns to and Distribute Spacing spaces from, drawn with a thicker outline (#541), Shape Builder merges taken from the planar map itself, so merged regions leave no inner edge, hairline gap or spur in their outline (#569), `.ai` written by the exports and the CLI (`--export`, `convert`) as Save As writes it (#526), SVG round trips keeping hidden objects and `data-*` attributes (#522), the gradient stop popover staying open after the click that opens it from the Gradient panel (#577), standard ligatures kept under small tracking and dropped only past about −8% / +22% of a word space or when the character asks for them (#583); see [Honest assessment](#honest-assessment-2026-10-05))._ ## Where we are diff --git a/crates/text/src/features.rs b/crates/text/src/features.rs index ef446dde..1cf82630 100644 --- a/crates/text/src/features.rs +++ b/crates/text/src/features.rs @@ -3,25 +3,27 @@ use harfrust::{Feature, Tag}; use vectorcraft_doc::CharStyle; -/// Tracking (in 1/1000 em) beyond which standard ligatures are dropped automatically. +/// Tracking (in 1/1000 em) outside which standard ligatures are dropped automatically: tighter +/// than the first, looser than the second. /// /// Letterspaced type should not ligate: an `fi` glyph keeps its letters tight while everything -/// around it opens up, which reads as a blot. Small tracking adjustments made to fit a line -/// (a few thousandths either way) are not letterspacing and keep the ligatures, so only -/// `|tracking|` above this threshold suppresses them. An explicit `liga` / `clig` in the -/// character's own features always wins. -pub const LIGATURE_TRACKING_LIMIT: f64 = 50.0; +/// around it opens up (or stays open while it closes up), which reads as a blot. Small tracking +/// adjustments made to fit a line are not letterspacing and keep the ligatures. The limits are +/// about −8% and +22% of a typical word space (250/1000 em), the thresholds Adobe's composers are +/// described as using (John Hudson, www-style, 2012-07): condensing shows the fixed ligature +/// sooner than expanding. An explicit `liga` / `clig` in the character's own features always wins. +pub const LIGATURE_TRACKING_LIMITS: (f64, f64) = (-20.0, 55.0); /// OpenType features applied during shaping. Kerning, `case` and ligature suppression are driven /// by the character style (`kerning`, `all_caps`, `tracking`); the rest are layout-wide options. -/// Standard ligatures are dropped when `|tracking|` exceeds [`LIGATURE_TRACKING_LIMIT`], unless +/// Standard ligatures are dropped when tracking is outside [`LIGATURE_TRACKING_LIMITS`], unless /// the character's features turn `liga` (or `clig`) on explicitly. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct OtFeatures { /// Vertical glyph alternates (`vert`), set by the writing direction. pub vertical: bool, - /// Standard ligatures (`liga`, `clig`). Suppressed automatically when `|tracking|` exceeds - /// [`LIGATURE_TRACKING_LIMIT`], unless the character turns them on explicitly. + /// Standard ligatures (`liga`, `clig`). Suppressed automatically when tracking is outside + /// [`LIGATURE_TRACKING_LIMITS`], unless the character turns them on explicitly. pub ligatures: bool, /// Contextual alternates (`calt`). pub contextual: bool, @@ -165,7 +167,8 @@ impl OtFeatures { /// Does `tracking` (1/1000 em) count as letterspacing that drops standard ligatures? pub fn ligatures_suppressed_by(tracking: f64) -> bool { // A non-finite value is not a real letterspacing request; keep the default. - tracking.is_finite() && tracking.abs() > LIGATURE_TRACKING_LIMIT + let (tight, loose) = LIGATURE_TRACKING_LIMITS; + tracking.is_finite() && (tracking < tight || tracking > loose) } /// Do `features` turn standard ligatures on explicitly (`liga`, `+liga`, `clig`, `+clig`)? The last diff --git a/crates/text/src/lib.rs b/crates/text/src/lib.rs index cb79ed0f..83e79b2c 100644 --- a/crates/text/src/lib.rs +++ b/crates/text/src/lib.rs @@ -23,7 +23,7 @@ pub mod test_fonts; pub mod thread; pub use craft_fonts::{CRAFT_FONTS, CraftFont}; -pub use features::{LIGATURE_TRACKING_LIMIT, OtFeatures, explicit_ligatures, ligatures_suppressed_by}; +pub use features::{LIGATURE_TRACKING_LIMITS, OtFeatures, explicit_ligatures, ligatures_suppressed_by}; pub use fontdb::{FALLBACK_FAMILY, FontClass, FontDb, FontFace, FontMatch, FontTraits, style_weight, system_font_dirs}; use kurbo::{Affine, BezPath, Point, Rect, Vec2}; pub use layout::{layout, layout_with}; diff --git a/crates/text/src/tests_typo.rs b/crates/text/src/tests_typo.rs index a486224f..c3e1f7f1 100644 --- a/crates/text/src/tests_typo.rs +++ b/crates/text/src/tests_typo.rs @@ -377,9 +377,11 @@ fn opentype_ligature_switches() { // Small tracking adjustments (fitting a line) keep the ligature. assert_eq!(fi(-5.0, &[]), 1, "tracking -5 keeps fi"); assert_eq!(fi(20.0, &[]), 1, "tracking +20 keeps fi"); - assert_eq!(fi(LIGATURE_TRACKING_LIMIT, &[]), 1, "the limit itself keeps fi"); - // Real letterspacing suppresses ligatures (as letterspaced type should)... + let (tight, loose) = LIGATURE_TRACKING_LIMITS; + assert_eq!((fi(tight, &[]), fi(loose, &[])), (1, 1), "the limits themselves keep fi"); + // Real letterspacing suppresses ligatures (as letterspaced type should), condensing sooner... assert_eq!(fi(100.0, &[]), 2, "tracking 100 drops fi"); + assert_eq!(fi(-30.0, &[]), 2, "tracking -30 drops fi"); assert_eq!(fi(-100.0, &[]), 2, "tracking -100 drops fi"); // ...unless the character turns them on explicitly. assert_eq!(fi(100.0, &["liga"]), 1, "explicit liga wins over tracking");