From 4f295e5fee8f98423720cf9b7cf370045c09d8ae Mon Sep 17 00:00:00 2001 From: ruchirguitar <113586074+ruchirguitar@users.noreply.github.com> Date: Thu, 12 Nov 2026 01:06:28 +0530 Subject: [PATCH 1/3] UI: keep numeric field caret on short pointer drags (#1326) A pointer movement under the click tolerance was still treated as a tap, so the focus click selected the whole numeric value. Any movement from the press now keeps the caret; only a tap without movement selects all. Regression test: a_sub_threshold_movement_released_as_a_click_keeps_its_caret. --- crates/ui-egui/src/tests/numfields.rs | 17 +++++++++++++++++ crates/ui-egui/src/widgets/text.rs | 8 ++++---- docs/gaps.md | 2 +- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/crates/ui-egui/src/tests/numfields.rs b/crates/ui-egui/src/tests/numfields.rs index 45801c22ec..d7ceed77b9 100644 --- a/crates/ui-egui/src/tests/numfields.rs +++ b/crates/ui-egui/src/tests/numfields.rs @@ -1361,6 +1361,23 @@ fn a_short_drag_released_as_a_click_keeps_its_caret() { assert_eq!(f.frame(vec![enter()], &draw), Some(1273456.0), "typing lands where the drag left the caret"); } +/// #1326: the smallest pointer movement egui still reports as a click (under the 3-point drag +/// threshold) must keep the caret too; only a press released where it went down selects all. +#[test] +fn a_sub_threshold_movement_released_as_a_click_keeps_its_caret() { + let mut f = Field::new(); + let draw = |ui: &mut egui::Ui| numeric_fixture(ui, 0); + f.frame(vec![], &draw); + let at = f.text_point(field_text(0), 2, 7.0); + f.frame(vec![Event::PointerMoved(at), pointer_button(at, true)], &draw); + let pressed = f.selection(); + let nudge = egui::vec2(1.5, 0.0); + f.frame(vec![Event::PointerMoved(at + nudge)], &draw); + f.frame(vec![pointer_button(at + nudge, false)], &draw); + assert_ne!(f.selection(), Some((0, field_text(0).chars().count())), "a 1.5 px movement is a drag, not a tap (#1326)"); + assert_eq!(f.selection(), pressed, "the movement keeps the caret where the press landed"); +} + /// A tap (no movement) on a fresh numeric field still selects all on release. #[test] fn a_tap_without_movement_still_selects_all_on_release() { diff --git a/crates/ui-egui/src/widgets/text.rs b/crates/ui-egui/src/widgets/text.rs index 3551baebd2..5ede108998 100644 --- a/crates/ui-egui/src/widgets/text.rs +++ b/crates/ui-egui/src/widgets/text.rs @@ -171,13 +171,13 @@ pub(super) fn select_all_on_focus(ui: &Ui, resp: &Response, text: &str) { }); } let clicked_focus = released && ui.data_mut(|d| d.remove_temp::(pending)).unwrap_or(false) && resp.clicked_by(egui::PointerButton::Primary); - // A press released a few points away is a short drag, not a tap. egui still reports a - // click, and a release inside one character leaves no text selection, so select-all on - // release would replace the drag the user just made (#1326). + // Any pointer movement from the press is a drag, not a tap. egui still reports a click for + // a movement under its click tolerance, and a release inside one character leaves no text + // selection, so select-all on release would replace the drag the user just made (#1326). let dragged = clicked_focus && ui .data_mut(|d| d.remove_temp::(press_pos)) - .is_some_and(|p0| ui.input(|i| i.pointer.interact_pos()).is_some_and(|p1| (p1 - p0).length() > 3.0)); + .is_some_and(|p0| ui.input(|i| i.pointer.interact_pos()).is_some_and(|p1| (p1 - p0).length() > 0.0)); if resp.lost_focus() { ui.data_mut(|d| { d.remove_temp::(pending); diff --git a/docs/gaps.md b/docs/gaps.md index 07308c801a..be3ffe3f41 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -320,7 +320,7 @@ macOS cursor delivery (#2292) is narrowed to a native host adapter using the exi - **Done:** Divide splits filled paths along open cutting lines and curves (#1307), preserving the frontmost filled appearance, implicit filled closure, holes, unrelated objects and one Undo/Redo; the existing bounded-map fallback remains. -- **Done:** Numeric fields keep a text drag's start when the pointer gains focus and movement exceeds the click threshold (#1326, in part). A completed focus click selects the whole value; keyboard focus, double-click and numeric stepping remain regression-tested. Short movements still classified as clicks can select all; that case remains open. +- **Done:** Numeric fields keep a text drag's start when the pointer gains focus, including short movements that egui still classifies as clicks (#1326). Any pointer movement from the press keeps the caret; only a tap without movement selects the whole value. Keyboard focus, double-click and numeric stepping remain regression-tested (`tests/numfields.rs`, `a_short_drag_released_as_a_click_keeps_its_caret` and `a_sub_threshold_movement_released_as_a_click_keeps_its_caret`). - **Done:** DXF reads control-point SPLINE closure (#1299 follow-up after #1320); open curves keep their open state. DXF export now recognizes circular cubic paths conservatively and writes native CIRCLE/ARC where stroke semantics permit; R12 weighted strokes remain POLYLINE, and a non-circular ellipse keeps the existing fallback. Alter Paths for Appearance outlines only the strokes a CAD line can't draw (aligned, profiled, offset dashes, wider than 2.11 mm, or in R13 and R14), so a 1 pt circle or arc stays a CIRCLE or ARC. CNC preset and VCarve QA remain open. Escape leaves Artboard editing for Selection; during a drag it first cancels the gesture. Headless egui key events cover idle Escape, active-drag cancellation and a subsequent Escape in `tests/distortkeys.rs`. - **Done:** DXF reads control-point SPLINE closure (#1299 follow-up after #1320); open curves keep their open state. DXF export now recognizes circular cubic paths conservatively and writes native CIRCLE/ARC where stroke semantics permit; R12 weighted strokes remain POLYLINE. A closed four-cubic ellipse, or a circle scaled out of proportion, is written as a native ELLIPSE in R13 and later; R12 keeps the existing fallback. CNC preset and VCarve QA remain open. Escape leaves Artboard editing for Selection; during a drag it first cancels the gesture. Headless egui key events cover idle Escape, active-drag cancellation and a subsequent Escape in `tests/distortkeys.rs`. From 3adadd52f4daad74527b886ade6174cd6fb7c6eb Mon Sep 17 00:00:00 2001 From: ruchirguitar <113586074+ruchirguitar@users.noreply.github.com> Date: Thu, 12 Nov 2026 19:40:04 +0530 Subject: [PATCH 2/3] UI: a tap within 1 pt still selects a numeric field's value (#1326) Review feedback: with no tolerance, pen or trackpad jitter turned a tap into a drag and the value was no longer selected. A release within 1 pt of the press is a tap; anything further keeps the caret. Regression test: a_tap_with_a_pixel_of_jitter_still_selects_all_on_release. --- crates/ui-egui/src/tests/numfields.rs | 18 ++++++++++++++++-- crates/ui-egui/src/widgets/text.rs | 15 ++++++++++----- docs/gaps.md | 2 +- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/crates/ui-egui/src/tests/numfields.rs b/crates/ui-egui/src/tests/numfields.rs index d7ceed77b9..0e1e794b8d 100644 --- a/crates/ui-egui/src/tests/numfields.rs +++ b/crates/ui-egui/src/tests/numfields.rs @@ -1361,8 +1361,8 @@ fn a_short_drag_released_as_a_click_keeps_its_caret() { assert_eq!(f.frame(vec![enter()], &draw), Some(1273456.0), "typing lands where the drag left the caret"); } -/// #1326: the smallest pointer movement egui still reports as a click (under the 3-point drag -/// threshold) must keep the caret too; only a press released where it went down selects all. +/// #1326: a pointer movement well under egui's click tolerance must keep the caret too; only a +/// press released within a point of where it went down selects all. #[test] fn a_sub_threshold_movement_released_as_a_click_keeps_its_caret() { let mut f = Field::new(); @@ -1391,6 +1391,20 @@ fn a_tap_without_movement_still_selects_all_on_release() { assert_eq!(f.selection(), Some((0, field_text(0).chars().count())), "a plain tap selects all"); } +/// A pen or trackpad tap often moves a pixel between press and release: it is still a tap. +#[test] +fn a_tap_with_a_pixel_of_jitter_still_selects_all_on_release() { + let mut f = Field::new(); + let draw = |ui: &mut egui::Ui| numeric_fixture(ui, 0); + f.frame(vec![], &draw); + let at = f.text_point(field_text(0), 2, 7.0); + f.frame(vec![Event::PointerMoved(at), pointer_button(at, true)], &draw); + let jitter = egui::vec2(0.7, -0.6); + f.frame(vec![Event::PointerMoved(at + jitter)], &draw); + f.frame(vec![pointer_button(at + jitter, false)], &draw); + assert_eq!(f.selection(), Some((0, field_text(0).chars().count())), "a jittery tap selects all"); +} + #[test] fn keyboard_focus_keeps_selecting_the_entire_numeric_field() { for mode in 0..4 { diff --git a/crates/ui-egui/src/widgets/text.rs b/crates/ui-egui/src/widgets/text.rs index 5ede108998..eec3cd3a94 100644 --- a/crates/ui-egui/src/widgets/text.rs +++ b/crates/ui-egui/src/widgets/text.rs @@ -147,10 +147,14 @@ pub(crate) fn own_field_shift_enter(ui: &Ui, id: egui::Id, buf: &mut String, sho shift_enter_keeps_focus(ui, id) } +/// How far, in points, a focus click may move between press and release and still be a tap that +/// selects the whole numeric value. Covers pen and trackpad jitter; anything further is a drag. +const TAP_SLOP: f32 = 1.0; + /// Numeric fields select the whole text on keyboard focus, a completed focus click or a /// double-click. A pointer press keeps its caret so dragging selects from the pressed position, /// and a short drag the toolkit still counts as a click keeps it too (#1326): only a tap -/// releases into select-all. +/// (released within [`TAP_SLOP`] of its press) releases into select-all. pub(super) fn select_all_on_focus(ui: &Ui, resp: &Response, text: &str) { let pending = resp.id.with("select-all-on-focus-click"); let press_pos = resp.id.with("select-all-on-focus-press"); @@ -171,13 +175,14 @@ pub(super) fn select_all_on_focus(ui: &Ui, resp: &Response, text: &str) { }); } let clicked_focus = released && ui.data_mut(|d| d.remove_temp::(pending)).unwrap_or(false) && resp.clicked_by(egui::PointerButton::Primary); - // Any pointer movement from the press is a drag, not a tap. egui still reports a click for - // a movement under its click tolerance, and a release inside one character leaves no text - // selection, so select-all on release would replace the drag the user just made (#1326). + // A pointer released more than a point from its press is a drag, not a tap. egui still + // reports a click for a movement under its click tolerance, and a release inside one + // character leaves no text selection, so select-all on release would replace the drag the + // user just made (#1326). The one point of slack keeps a jittery pen or trackpad tap a tap. let dragged = clicked_focus && ui .data_mut(|d| d.remove_temp::(press_pos)) - .is_some_and(|p0| ui.input(|i| i.pointer.interact_pos()).is_some_and(|p1| (p1 - p0).length() > 0.0)); + .is_some_and(|p0| ui.input(|i| i.pointer.interact_pos()).is_some_and(|p1| (p1 - p0).length() > TAP_SLOP)); if resp.lost_focus() { ui.data_mut(|d| { d.remove_temp::(pending); diff --git a/docs/gaps.md b/docs/gaps.md index be3ffe3f41..5e101fe46f 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -320,7 +320,7 @@ macOS cursor delivery (#2292) is narrowed to a native host adapter using the exi - **Done:** Divide splits filled paths along open cutting lines and curves (#1307), preserving the frontmost filled appearance, implicit filled closure, holes, unrelated objects and one Undo/Redo; the existing bounded-map fallback remains. -- **Done:** Numeric fields keep a text drag's start when the pointer gains focus, including short movements that egui still classifies as clicks (#1326). Any pointer movement from the press keeps the caret; only a tap without movement selects the whole value. Keyboard focus, double-click and numeric stepping remain regression-tested (`tests/numfields.rs`, `a_short_drag_released_as_a_click_keeps_its_caret` and `a_sub_threshold_movement_released_as_a_click_keeps_its_caret`). +- **Done:** Numeric fields keep a text drag's start when the pointer gains focus, including short movements that egui still classifies as clicks (#1326). A release more than 1 pt from the press keeps the caret; a tap, including one with a pixel of pen or trackpad jitter, selects the whole value. Keyboard focus, double-click and numeric stepping remain regression-tested (`tests/numfields.rs`, `a_short_drag_released_as_a_click_keeps_its_caret` and `a_sub_threshold_movement_released_as_a_click_keeps_its_caret`). - **Done:** DXF reads control-point SPLINE closure (#1299 follow-up after #1320); open curves keep their open state. DXF export now recognizes circular cubic paths conservatively and writes native CIRCLE/ARC where stroke semantics permit; R12 weighted strokes remain POLYLINE, and a non-circular ellipse keeps the existing fallback. Alter Paths for Appearance outlines only the strokes a CAD line can't draw (aligned, profiled, offset dashes, wider than 2.11 mm, or in R13 and R14), so a 1 pt circle or arc stays a CIRCLE or ARC. CNC preset and VCarve QA remain open. Escape leaves Artboard editing for Selection; during a drag it first cancels the gesture. Headless egui key events cover idle Escape, active-drag cancellation and a subsequent Escape in `tests/distortkeys.rs`. - **Done:** DXF reads control-point SPLINE closure (#1299 follow-up after #1320); open curves keep their open state. DXF export now recognizes circular cubic paths conservatively and writes native CIRCLE/ARC where stroke semantics permit; R12 weighted strokes remain POLYLINE. A closed four-cubic ellipse, or a circle scaled out of proportion, is written as a native ELLIPSE in R13 and later; R12 keeps the existing fallback. CNC preset and VCarve QA remain open. Escape leaves Artboard editing for Selection; during a drag it first cancels the gesture. Headless egui key events cover idle Escape, active-drag cancellation and a subsequent Escape in `tests/distortkeys.rs`. From be1b946b1e5a9085b8a90dead125e236ff4f8990 Mon Sep 17 00:00:00 2001 From: ruchirguitar <113586074+ruchirguitar@users.noreply.github.com> Date: Thu, 12 Nov 2026 19:46:23 +0530 Subject: [PATCH 3/3] Docs: revision rows for the numeric-field tap tolerance (#1326) --- docs/gaps.md | 1 + docs/ui-parity.md | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/gaps.md b/docs/gaps.md index 5e101fe46f..e4b81f6b48 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -853,6 +853,7 @@ trap, and the defaults (0.25 pt, 100%, 40%) are VectorCraft's own, the pages giv | Date | Change | Summary | |---|---|---| +| 2026-11-12 | minor | G11: numeric fields keep the caret when a focus click is released more than 1 pt from its press; a tap within 1 pt still selects the whole value (#6659, follow-up to #1326); short-drag and jitter-tap regressions in `tests/numfields.rs`; estimates unchanged. | | 2026-11-10 | minor | G6: MCP catalogue (#6460): headless lists `UI_COMMANDS` (`ui: true`), eight prompts, effect vs command ids, 33 tools, commands/reference; estimate unchanged. | | 2026-11-11 | minor | G5: built-in Status Effects graphic style library (Poison Tint, Freeze Edge, Burn Aura, Stun Pulse, Stealth Fade, Shield Buff); Missing trimmed; estimate 7–17 → 6–16 h; MCP docs list UI Frames and Map Markers under Symbol libraries. | | 2026-11-11 | minor | G5: built-in Inventory Icons symbol library (Sword, Shield, Potion, Key, Gem; no default 9-slice); Missing trimmed; estimate 8–18 → 7–17 h. | diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 44d0b19a4e..964ebdb039 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -283,7 +283,7 @@ tools, never checked side by side) isn't captured row by row. | Number fields: Up/Down step, Shift ×10, Cmd/Ctrl ÷10, wheel over a hovered field without a click; steps, the wheel and label scrubs stop at the field's bounds (a stroke weight at 0, a miter limit at 1) | done | Shift+arrow in the Stroke weight field doesn't step by 10 (#991); bounds #3370. The field takes the wheel only when the pointer was moved onto it after the field last moved or was covered by a window or overlay, and the wheel stream began there; a stream that began elsewhere stays there until 0.5 s pass with no wheel input or the pointer is pressed on a field, so a panel scrolled under a still pointer keeps scrolling past the field. This rule and its 0.5 s are VectorCraft's choices. | | Numeric spinner arrows step once on a click and keep repeating while held with the mouse | done | #6307; headless test `a_held_spinner_arrow_repeats` holds past egui's click timeout and checks release. | | Arithmetic in fields (`+ - * /`, and `%` of the value before it: `3cm*50%`, `50pt+25%`) and unit suffixes (`3 mm`, `1in`, `2p6`) | done | `Unit::parse`; mixed units in one expression not verified | -| Numeric fields: focus click selects the whole value; dragging selects from the pressed character | partial | #1326; select-all waits for click release; px/pt/plain/optional fields, both directions and replacement text tested; native API input, 17 px partial replacement, Undo/Redo, saved-file round trip and screenshots verified; Tab, double-click, arrows and wheel preserved; short movements still classified as clicks can select all and remain open | +| Numeric fields: focus click selects the whole value; dragging selects from the pressed character | partial | #1326; select-all waits for click release; px/pt/plain/optional fields, both directions and replacement text tested; native API input, 17 px partial replacement, Undo/Redo, saved-file round trip and screenshots verified; Tab, double-click, arrows and wheel preserved; a release more than 1 pt from the press keeps the caret, a tap within 1 pt selects all (#6659) | | Units: pt, pc, in, mm, cm, px, ft-in, m, yd, ft; General/Stroke/Type units | done | units preference reverting to points on SVG output reported (#864). A file that records no units (PDF, EPS or `.ai` without ruler units, raster images, metafiles, Affinity files) opens and reverts in General units; native, SVG and DXF files and a `.ai` or EPS with ruler units keep theirs, as a [Community Expert's answer](https://community.adobe.com/questions-652/change-units-from-points-to-inches-won-t-stick-765372) describes (#5820) | | Units › East Asian Type (Q and Ha) | done | #394: `unitsAsianType` offers every unit and Q and Ha, a quarter of a millimetre each ([Scripting Guide's Measurement Units](https://ai-scripting.docsforadobe.dev/scripting/measurementUnits/): 0.709 pt per Q; [InDesign's unit notation](https://helpx.adobe.com/in/indesign/desktop/layout-and-grid-tools/rulers-and-measure-tools/change-ruler-measurement-units.html): `q`, `h`); only it lists Q and Ha. The public pages do not say which fields use it, so no field uses it yet and that waits for a source. Tests `q_and_ha_are_a_quarter_millimetre`, `east_asian_type_unit_*` | | Field precision: 3 decimals for steps; values kept as f64 | done | | @@ -511,6 +511,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development | Date | Change | Summary | |---|---|---| +| 2026-11-12 | minor | Numeric fields: a focus click released more than 1 pt from its press keeps the caret; a tap within 1 pt still selects the whole value (#6659, follow-up to #1326). Scores and estimates unchanged. | | 2026-11-11 | minor | Outline view: a click on a path's outline selects it through an image or a gradient mesh in front, which Outline draws as a frame or mesh lines (#6437). | | 2026-11-11 | minor | A middle-click on a document tab (or its ×) closes it as the × does; closing a document's last view with unsaved changes asks to save first (#6506). VectorCraft addition; scores and estimates unchanged. | | 2026-11-03 | minor | A double-click on a ruler adds a guide at the pointer (#5239): a vertical one on the top ruler, a horizontal one on the left ruler; scores and estimates unchanged. |