Merge pull request #1013 from Ni-zav/fix/991-shift-stepper-s20261010T1758Z-a74b

fix(widgets): apply Shift and Ctrl modifiers to numeric stepper buttons
This commit is contained in:
BFlat 2026-10-11 03:45:43 +07:00 committed by GitHub
commit c403e57d2a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 63 additions and 3 deletions

View File

@ -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 |

View File

@ -139,3 +139,37 @@ 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 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;
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);
// 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 };
for event in [egui::Event::PointerMoved(pos), button(true), button(false)] {
let events = vec![egui::Event::ModifiersChanged(modifiers), event];
frame_raw(&ctx, app, egui::RawInput { events, ..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);
// 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);
}

View File

@ -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);
}
}