Step a blank Stroke Weight from 0.25 pt (#5349)

This commit is contained in:
MarekSpodBiedry
2026-11-04 09:43:30 +01:00
parent a207c7b816
commit 9218713071
2 changed files with 95 additions and 8 deletions
@@ -243,3 +243,57 @@ fn a_spinner_step_from_a_weight_shown_as_a_preset_moves_past_it() {
}
assert_eq!(app.session.shown_stroke().unwrap().width, 1.0);
}
/// With objects of different weights selected the Weight field is blank, and its spinner steps
/// from 0.25 pt as Illustrator does: up gives 0.5 pt and down gives 0 pt, for all of them.
#[test]
fn the_spinner_of_a_blank_weight_steps_from_a_quarter_point() {
use super::tests_appearance::frame_raw;
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
for (up, after) in [(true, 0.5), (false, 0.0)] {
let mut app = app_with_rect();
let a = app.session.active().unwrap().selection.objects[0];
let b = run(&mut app, "shape.rectangle", json!({"x": 120, "y": 10, "width": 50, "height": 50}))["id"].as_u64().unwrap();
run(&mut app, "stroke.set", json!({"ids": [b], "weight": 3}));
run(&mut app, "select.set", json!({ "ids": [a.0, b] }));
let t = texts(&ctx, &mut app, stroke::show);
let r = text_rect(&t, "Weight:");
let sp = ctx.global_style().spacing.item_spacing.x;
let pos = egui::pos2(r.right() + sp + 8.0, r.center().y + if up { -5.0 } else { 5.0 });
let button = |pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::NONE };
for event in [egui::Event::PointerMoved(pos), button(true), button(false)] {
frame_raw(&ctx, &mut app, egui::RawInput { events: vec![event], ..Default::default() }, stroke::show);
}
for id in [a.0, b] {
run(&mut app, "select.set", json!({ "ids": [id] }));
assert_eq!(app.session.shown_stroke().unwrap().width, after, "object {id}, up: {up}");
}
}
}
/// ↑/↓ in the focused blank Weight field step from 0.25 pt too, for every selected object.
#[test]
fn the_arrow_keys_in_a_blank_weight_step_from_a_quarter_point() {
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
for (key, after) in [(egui::Key::ArrowUp, 0.5), (egui::Key::ArrowDown, 0.0)] {
let mut app = app_with_rect();
let a = app.session.active().unwrap().selection.objects[0];
let b = run(&mut app, "shape.rectangle", json!({"x": 120, "y": 10, "width": 50, "height": 50}))["id"].as_u64().unwrap();
run(&mut app, "stroke.set", json!({"ids": [b], "weight": 3}));
run(&mut app, "select.set", json!({ "ids": [a.0, b] }));
let t = texts(&ctx, &mut app, stroke::show);
let r = text_rect(&t, "Weight:");
let sp = ctx.global_style().spacing.item_spacing.x;
// The blank field, right of the 16 px spinner.
click(&ctx, &mut app, egui::pos2(r.right() + 2.0 * sp + 16.0 + 30.0, r.center().y), stroke::show);
let press = egui::Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers: egui::Modifiers::NONE };
frame_events(&ctx, &mut app, vec![press], stroke::show);
for id in [a.0, b] {
run(&mut app, "select.set", json!({ "ids": [id] }));
assert_eq!(app.session.shown_stroke().unwrap().width, after, "object {id}, {key:?}");
}
}
}
+41 -8
View File
@@ -100,6 +100,26 @@ fn unit_field(
bounds: RangeInclusive<f64>,
stops: &[f64],
live: bool,
) -> Option<f64> {
unit_field_from(ui, id, label, name, value, unit, width, share_of_value, bounds, stops, live, None)
}
/// [`unit_field`] whose blank field (`value` is `None`) steps from `blank_from` (points) with
/// ↑/↓ and the wheel instead of doing nothing.
#[allow(clippy::too_many_arguments)]
fn unit_field_from(
ui: &mut Ui,
id: impl std::hash::Hash,
label: Option<PreviousLabel>,
name: Option<&str>,
value: Option<f64>,
unit: Unit,
width: f32,
share_of_value: bool,
bounds: RangeInclusive<f64>,
stops: &[f64],
live: bool,
blank_from: Option<f64>,
) -> Option<f64> {
let id = id_with(ui.scope_id(), id);
let shown = value.map(|v| unit.format(v)).unwrap_or_default();
@@ -111,8 +131,13 @@ fn unit_field(
let read = |b: &str| typed.parse(&decimal_point(b));
let bounds_unit = unit.from_pt(*bounds.start())..=unit.from_pt(*bounds.end());
let stops: Vec<f64> = stops.iter().map(|&s| unit.from_pt(s)).collect();
let stepped =
step_value(ui, &resp, &mut buf, &bounds_unit, (|b: &str| Some(unit.from_pt(read(b)?)), |v| unit.format(unit.to_pt(v))), None, &stops);
let blank_from = blank_from.filter(|_| value.is_none());
let read_unit = |b: &str| match read(b) {
Some(v) => Some(unit.from_pt(v)),
None if b.trim().is_empty() => blank_from.map(|v| unit.from_pt(v)),
None => None,
};
let stepped = step_value(ui, &resp, &mut buf, &bounds_unit, (read_unit, |v| unit.format(unit.to_pt(v))), None, &stops);
let scrubbed = scrub::field(ui, id, rect, &buf, value.map(|v| unit.from_pt(v)), STEP_DECIMALS, &bounds_unit);
let commit = commit && buf != shown;
if commit {
@@ -566,12 +591,18 @@ pub fn spin_field_stops_named(
presets: &[f64],
) -> Option<f64> {
let label = take_previous_label(ui);
spin_generic(ui, label, value, None, None, None, width, step, min, presets, presets, &|v| unit.format(v), &mut |ui, fw, label| {
unit_field(ui, id, label, name, value, unit, fw, false, min..=f64::INFINITY, presets, false)
let blank_from = BLANK_WEIGHT_STEPS_FROM;
let fmt = |v: f64| unit.format(v);
spin_generic(ui, label, value, Some(blank_from), None, None, None, width, step, min, presets, presets, &fmt, &mut |ui, fw, label| {
unit_field_from(ui, id, label, name, value, unit, fw, false, min..=f64::INFINITY, presets, false, Some(blank_from))
})
.and_then(SpinPick::value)
}
/// Where a blank Stroke Weight (the selected objects differ) steps from, in points: up goes to
/// 0.5 pt and down to 0 pt, as in Illustrator.
const BLANK_WEIGHT_STEPS_FROM: f64 = 0.25;
/// What a [`spin_field_auto`] returns: a value (points), or its Auto entry.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum SpinPick {
@@ -623,7 +654,7 @@ pub fn spin_field_auto_named(
presets: &[f64],
) -> Option<SpinPick> {
let label = take_previous_label(ui);
spin_generic(ui, label, value, auto, None, None, width, step, min, presets, &[], &|v| unit.format(v), &mut |ui, fw, label| {
spin_generic(ui, label, value, None, auto, None, None, width, step, min, presets, &[], &|v| unit.format(v), &mut |ui, fw, label| {
unit_field(ui, id, label, name, value, unit, fw, false, min..=f64::INFINITY, &[], false)
})
}
@@ -662,7 +693,7 @@ pub fn spin_plain_named(
let label = take_previous_label(ui);
let value = value.into();
let fmt = |v: f64| format!("{v}{suffix}");
spin_generic(ui, label, value, None, None, None, width, step, min, presets, &[], &fmt, &mut |ui, fw, label| {
spin_generic(ui, label, value, None, None, None, None, width, step, min, presets, &[], &fmt, &mut |ui, fw, label| {
bounded_field(ui, id, label, name, value, suffix, decimals, fw, &(min..=f64::INFINITY), None, false, false)
})
.and_then(SpinPick::value)
@@ -686,6 +717,7 @@ pub fn spin_kerning(
ui,
label,
value,
None,
automatic,
optical,
Some(optical_label),
@@ -706,6 +738,7 @@ fn spin_generic(
ui: &mut Ui,
label: Option<PreviousLabel>,
value: Option<f64>,
blank_from: Option<f64>,
auto: Option<bool>,
optical: Option<bool>,
optical_label: Option<&str>,
@@ -744,12 +777,12 @@ fn spin_generic(
ui.painter().line_segment([m + vec2(0.0, d), m + vec2(3.0, -d)], Stroke::new(1.2, c));
}
sresp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, enabled, tl!("Step up or down")));
// A blank field (values that differ) has nothing to step from. A click steps once; an
// A blank field (values that differ) steps from `blank_from`, if any. A click steps once; an
// arrow held down steps again and again after a pause, as a spin button does (#4267).
let repeat = held_repeat(ui, &sresp);
if (repeat == Some(true) || (sresp.clicked() && repeat.is_none()))
&& let Some(p) = sresp.interact_pointer_pos()
&& let Some(v) = value
&& let Some(v) = value.or(blank_from)
{
let modifiers = ui.input(|i| i.modifiers);
out = Some(SpinPick::Value(spinner_step(v, step, min, up.contains(p), modifiers, stops, fmt)));