From 057c2ee2c1b1ca54528cd0777930ed52b6588793 Mon Sep 17 00:00:00 2001 From: Nigel Ihza Firdausy <106657905+Ni-zav@users.noreply.github.com> Date: Sun, 11 Oct 2026 00:51:40 +0700 Subject: [PATCH 1/4] fix(widgets): honor Shift on numeric spinner buttons (#991) --- crates/ui-egui/src/widgets.rs | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index 6cb9da82..d8883a17 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -1170,8 +1170,8 @@ fn spin_generic( && let Some(p) = sresp.interact_pointer_pos() && let Some(v) = value { - let nv = if up.contains(p) { v + step } else { v - step }; - out = Some(SpinPick::Value(nv.max(min))); + let modifiers = ui.input(|i| i.modifiers); + out = Some(SpinPick::Value(spinner_step(v, step, min, up.contains(p), modifiers))); } let fw = if presets.is_empty() { width - 15.0 } else { width - 34.0 }; if let Some(v) = field(ui, fw) { @@ -1198,6 +1198,14 @@ fn spin_generic( out } +/// Spinner buttons follow the same modifier sizes as ↑/↓ in a focused number field: +/// Shift ×10, Ctrl/Cmd ÷10, otherwise the caller's normal step. +fn spinner_step(value: f64, step: f64, min: f64, up: bool, modifiers: egui::Modifiers) -> f64 { + let multiplier = STEPS.iter().find(|(m, _)| modifiers.matches_logically(*m)).map_or(1.0, |(_, factor)| *factor); + let delta = step * multiplier; + (value + if up { delta } else { -delta }).max(min) +} + /// Drag phase of a live slider/handle: previews while dragging, commits on release. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Live { @@ -1826,3 +1834,21 @@ pub fn text_presets(ui: &mut Ui, id: impl std::hash::Hash + std::fmt::Debug, val }); out } + +#[cfg(test)] +mod spinner_modifier_tests { + use super::*; + + #[test] + fn shift_steps_stroke_weight_by_ten_points() { + assert_eq!(spinner_step(14.0, 1.0, 0.0, true, egui::Modifiers::SHIFT), 24.0); + assert_eq!(spinner_step(14.0, 1.0, 0.0, false, egui::Modifiers::SHIFT), 4.0); + } + + #[test] + fn unmodified_and_command_steps_keep_existing_behavior() { + assert_eq!(spinner_step(14.0, 1.0, 0.0, true, egui::Modifiers::NONE), 15.0); + assert_eq!(spinner_step(14.0, 1.0, 0.0, false, egui::Modifiers::COMMAND), 13.9); + assert_eq!(spinner_step(0.0, 1.0, 0.0, false, egui::Modifiers::SHIFT), 0.0); + } +} From 6d5fdc8a2aaf5fb387643a6bdba77b9b60bd3799 Mon Sep 17 00:00:00 2001 From: Nigel Ihza Firdausy <106657905+Ni-zav@users.noreply.github.com> Date: Sun, 11 Oct 2026 02:06:41 +0700 Subject: [PATCH 2/4] test(ui): Shift-click on the Stroke weight stepper steps 10 pt (#991) The headless panel test of #1015, kept on top of #1013's fix (#1015's own change to the spinner is superseded by it). --- crates/ui-egui/src/panels/tests_strokeux.rs | 43 +++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/crates/ui-egui/src/panels/tests_strokeux.rs b/crates/ui-egui/src/panels/tests_strokeux.rs index 9f630263..8b0e5f0b 100644 --- a/crates/ui-egui/src/panels/tests_strokeux.rs +++ b/crates/ui-egui/src/panels/tests_strokeux.rs @@ -139,3 +139,46 @@ fn weight_presets_display_cleanly_in_their_native_unit() { // Units no stroke is measured in keep the pt ladder, in points. assert_eq!(stroke::weight_presets(Unit::Meters), stroke::weight_presets(Unit::Points)); } + +/// #991: the spinner to the left of Stroke Weight honors Shift on both arrows, while a +/// normal click still steps by one point (the focused numeric field already handles Shift+↑/↓). +#[test] +fn stroke_weight_stepper_uses_ten_point_shift_steps() { + use super::tests_appearance::frame_raw; + + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let mut app = app_with_rect(); + run(&mut app, "stroke.set", json!({"weight": 14})); + let t = texts(&ctx, &mut app, stroke::show); + let r = text_rect(&t, "Weight:"); + let x = r.right() + ctx.global_style().spacing.item_spacing.x + 8.0; + let click_step = |app: &mut VectorcraftApp, y: f32, modifiers: egui::Modifiers| { + let pos = egui::pos2(x, y); + let button = |pressed| egui::Event::PointerButton { + pos, + button: egui::PointerButton::Primary, + pressed, + modifiers, + }; + for event in [egui::Event::PointerMoved(pos), button(true), button(false)] { + frame_raw( + &ctx, + app, + egui::RawInput { + modifiers, + events: vec![event], + ..Default::default() + }, + stroke::show, + ); + } + }; + + click_step(&mut app, r.center().y - 5.0, egui::Modifiers::SHIFT); + assert_eq!(app.session.shown_stroke().unwrap().width, 24.0); + click_step(&mut app, r.center().y + 5.0, egui::Modifiers::NONE); + assert_eq!(app.session.shown_stroke().unwrap().width, 23.0); + click_step(&mut app, r.center().y + 5.0, egui::Modifiers::SHIFT); + assert_eq!(app.session.shown_stroke().unwrap().width, 13.0); +} From 9e09d53fd96c53a5d2e6313a927ece8e2a23ad69 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sun, 11 Oct 2026 02:06:53 +0700 Subject: [PATCH 3/4] Review #1013: the stepper test from #1015 compiles and hits the spinner The headless test taken from #1015 set `RawInput::modifiers`, which egui 0.36 doesn't have, and clicked left of the spinner (the right-aligned label's text ends before its box). Modifiers now come as `ModifiersChanged` events, as the Appearance panel tests send them, and the click is placed from the field's text. It also checks Ctrl/Cmd steps a tenth. It fails with the spinner ignoring modifiers (15 pt for 24). rustfmt. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/panels/tests_strokeux.rs | 31 ++++++++------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/crates/ui-egui/src/panels/tests_strokeux.rs b/crates/ui-egui/src/panels/tests_strokeux.rs index 8b0e5f0b..b10b80da 100644 --- a/crates/ui-egui/src/panels/tests_strokeux.rs +++ b/crates/ui-egui/src/panels/tests_strokeux.rs @@ -140,8 +140,8 @@ fn weight_presets_display_cleanly_in_their_native_unit() { assert_eq!(stroke::weight_presets(Unit::Meters), stroke::weight_presets(Unit::Points)); } -/// #991: the spinner to the left of Stroke Weight honors Shift on both arrows, while a -/// normal click still steps by one point (the focused numeric field already handles Shift+↑/↓). +/// #991: the spinner left of the Stroke Weight field steps ten with Shift on both arrows and a +/// tenth with Ctrl/Cmd, as ↑/↓ do in the focused field, while a plain click steps one point. #[test] fn stroke_weight_stepper_uses_ten_point_shift_steps() { use super::tests_appearance::frame_raw; @@ -151,27 +151,15 @@ fn stroke_weight_stepper_uses_ten_point_shift_steps() { let mut app = app_with_rect(); run(&mut app, "stroke.set", json!({"weight": 14})); let t = texts(&ctx, &mut app, stroke::show); - let r = text_rect(&t, "Weight:"); - let x = r.right() + ctx.global_style().spacing.item_spacing.x + 8.0; + // The 16 px spinner sits just left of the field's text. + let r = text_rect(&t, "14 pt"); + let x = r.left() - 14.0; let click_step = |app: &mut VectorcraftApp, y: f32, modifiers: egui::Modifiers| { let pos = egui::pos2(x, y); - let button = |pressed| egui::Event::PointerButton { - pos, - button: egui::PointerButton::Primary, - pressed, - modifiers, - }; + let button = |pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers }; for event in [egui::Event::PointerMoved(pos), button(true), button(false)] { - frame_raw( - &ctx, - app, - egui::RawInput { - modifiers, - events: vec![event], - ..Default::default() - }, - stroke::show, - ); + let events = vec![egui::Event::ModifiersChanged(modifiers), event]; + frame_raw(&ctx, app, egui::RawInput { events, ..Default::default() }, stroke::show); } }; @@ -181,4 +169,7 @@ fn stroke_weight_stepper_uses_ten_point_shift_steps() { assert_eq!(app.session.shown_stroke().unwrap().width, 23.0); click_step(&mut app, r.center().y + 5.0, egui::Modifiers::SHIFT); assert_eq!(app.session.shown_stroke().unwrap().width, 13.0); + // Ctrl/Cmd steps a tenth, as ↑/↓ and the wheel do in the field. + click_step(&mut app, r.center().y + 5.0, egui::Modifiers::COMMAND); + assert!((app.session.shown_stroke().unwrap().width - 12.9).abs() < 1e-6); } From e0d494b2eb09341e99766beb40daadbba8898626 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sun, 11 Oct 2026 02:24:31 +0700 Subject: [PATCH 4/4] ROADMAP: the validation batch, Object Mosaic counts and stepper modifiers Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ROADMAP.md b/ROADMAP.md index b97cf0a2..eaca3a38 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -123,7 +123,7 @@ Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gap | Date | What landed | |---|---| -| 2026-10-11 | Effect › Brush Strokes: Accented Edges, Angled Strokes, Crosshatch, Dark Strokes, Ink Outlines, Spatter, Sprayed Strokes, Sumi-e (G2) | +| 2026-10-11 | Effect › Brush Strokes: Accented Edges, Angled Strokes, Crosshatch, Dark Strokes, Ink Outlines, Spatter, Sprayed Strokes, Sumi-e (G2) Command parameters from agents and files checked before anything changes (paint, place, cuts, dashes, crop, asset ids, anchors, paths, distort; #984–#999); command dialogs keep whole numbers whole, so Create Object Mosaic takes the tiles typed (#1000); stepper arrows take Shift (×10) and Ctrl/Cmd (÷10) like the arrow keys (#991). | | 2026-10-10 | Initial live Revolve (Effect › 3D and Materials, #846, #605), in its own `three-d` crate. Progress docs re-measured and restructured to the craftrules standard (this page, `docs/target-app-parity.md`, `gaps.md`, `ui-parity.md`, the format, hardware, localization, effects and type checklists). Same day: v0.8.0; Effect › Distort, Pixelate and Texture filters (13); German interface; Variables (data merge); Graph Type value axes and tick marks; restart when the first frame never reaches the screen (#964) | | 2026-10-09 | Radial Blur, Smart Blur, Unsharp Mask (gap 2 begins); 340 commits, the busiest day | | 2026-10-08 | v0.5.0–v0.7.0; community contributions: Pen and shape-tool modifiers, Layers, Artboards, Japanese composition, Hebrew/Arabic type, interface languages, saved selections |