mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-30 04:37:29 -06:00
Stroke Weight: a spinner click from a weight shown as a preset goes past it
A weight of 2.004 pt shows as 0.167 p in picas, the same text as the 2 pt preset. One click down on the spinner went to 2 pt and the field did not change. The spinner now treats a preset shown with the same text as the weight as reached, as the field's arrow keys do, so the click goes on to 1 pt. A plain step also passes a whole-point step shown with the same text as the weight: 2.0004 pt goes down to 1 pt.
This commit is contained in:
@@ -222,3 +222,24 @@ fn every_weight_spinner_steps_to_the_next_preset() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A weight the field shows with a preset's text has reached that preset: one click down on the
|
||||
/// spinner from 2.004 pt, shown as `0.167 p` like the 2 pt preset, goes on to 1 pt.
|
||||
#[test]
|
||||
fn a_spinner_step_from_a_weight_shown_as_a_preset_moves_past_it() {
|
||||
use super::tests_appearance::frame_raw;
|
||||
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let mut app = app_with_rect();
|
||||
app.session.prefs.units_stroke = "picas".into();
|
||||
run(&mut app, "stroke.set", json!({"weight": 2.004}));
|
||||
let t = texts(&ctx, &mut app, stroke::show);
|
||||
let r = text_rect(&t, "0.167 p");
|
||||
let pos = egui::pos2(r.left() - 14.0, r.center().y + 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);
|
||||
}
|
||||
assert_eq!(app.session.shown_stroke().unwrap().width, 1.0);
|
||||
}
|
||||
|
||||
@@ -276,17 +276,21 @@ pub(crate) mod accessibility_tests {
|
||||
mod spinner_modifier_tests {
|
||||
use super::*;
|
||||
|
||||
fn pt(v: f64) -> String {
|
||||
Unit::Points.format(v)
|
||||
}
|
||||
|
||||
#[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);
|
||||
assert_eq!(spinner_step(14.0, 1.0, 0.0, true, egui::Modifiers::SHIFT, &[], &pt), 24.0);
|
||||
assert_eq!(spinner_step(14.0, 1.0, 0.0, false, egui::Modifiers::SHIFT, &[], &pt), 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);
|
||||
assert_eq!(spinner_step(14.0, 1.0, 0.0, true, egui::Modifiers::NONE, &[], &pt), 15.0);
|
||||
assert_eq!(spinner_step(14.0, 1.0, 0.0, false, egui::Modifiers::COMMAND, &[], &pt), 13.9);
|
||||
assert_eq!(spinner_step(0.0, 1.0, 0.0, false, egui::Modifiers::SHIFT, &[], &pt), 0.0);
|
||||
}
|
||||
|
||||
/// A plain click on the spinner stops at each stop; Shift and Ctrl/Cmd steps do not.
|
||||
@@ -294,12 +298,21 @@ mod spinner_modifier_tests {
|
||||
fn a_plain_spinner_step_stops_at_the_stops() {
|
||||
let stops = [0.25, 0.5, 0.75, 1.0, 2.0, 10.0, 20.0];
|
||||
let plain = egui::Modifiers::NONE;
|
||||
assert_eq!(spinner_step(0.52, 1.0, 0.0, false, plain, &stops), 0.5);
|
||||
assert_eq!(spinner_step(0.52, 1.0, 0.0, true, plain, &stops), 0.75);
|
||||
assert_eq!(spinner_step(0.1, 1.0, 0.0, false, plain, &stops), 0.0);
|
||||
assert_eq!(spinner_step(14.0, 1.0, 0.0, true, plain, &stops), 15.0);
|
||||
assert_eq!(spinner_step(14.0, 1.0, 0.0, false, egui::Modifiers::SHIFT, &stops), 4.0);
|
||||
assert!((spinner_step(0.52, 1.0, 0.0, false, egui::Modifiers::COMMAND, &stops) - 0.42).abs() < 1e-9);
|
||||
assert_eq!(spinner_step(0.52, 1.0, 0.0, false, plain, &stops, &pt), 0.5);
|
||||
assert_eq!(spinner_step(0.52, 1.0, 0.0, true, plain, &stops, &pt), 0.75);
|
||||
assert_eq!(spinner_step(0.1, 1.0, 0.0, false, plain, &stops, &pt), 0.0);
|
||||
assert_eq!(spinner_step(14.0, 1.0, 0.0, true, plain, &stops, &pt), 15.0);
|
||||
assert_eq!(spinner_step(14.0, 1.0, 0.0, false, egui::Modifiers::SHIFT, &stops, &pt), 4.0);
|
||||
assert!((spinner_step(0.52, 1.0, 0.0, false, egui::Modifiers::COMMAND, &stops, &pt) - 0.42).abs() < 1e-9);
|
||||
}
|
||||
|
||||
/// A stop the spinner's text shows as the value counts as reached: 2.004 pt and the 2 pt stop
|
||||
/// both show as `0.167 p`, so a click down goes on to 1 pt.
|
||||
#[test]
|
||||
fn a_spinner_stop_shown_as_the_value_is_passed() {
|
||||
let pc = |v: f64| Unit::Picas.format(v);
|
||||
assert_eq!(spinner_step(2.004, 1.0, 0.0, false, egui::Modifiers::NONE, &[1.0, 2.0, 3.0], &pc), 1.0);
|
||||
assert_eq!(spinner_step(2.004, 1.0, 0.0, true, egui::Modifiers::NONE, &[1.0, 2.0, 3.0], &pc), 3.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,6 +365,9 @@ mod step_through_tests {
|
||||
assert_eq!(step_through(0.167, 1.0, -1.0, &picas, shown(Unit::Picas)), 1.0 / 12.0);
|
||||
assert_eq!(step_through(0.083, 1.0, 1.0, &picas, shown(Unit::Picas)), 2.0 / 12.0);
|
||||
assert_eq!(step_through(0.016, 1.0, -1.0, &[0.0078, 0.0156], shown(Unit::Inches)), 0.0156);
|
||||
// The next whole step shown as the value is passed too: 2.0004 pt shows as `2 pt`.
|
||||
assert_eq!(step_through(2.0004, 1.0, -1.0, &[1.0, 2.0], shown(Unit::Points)), 1.0);
|
||||
assert_eq!(step_through(2.0004, 1.0, -1.0, &[], shown(Unit::Points)), 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -137,13 +137,17 @@ pub(super) fn step_to_multiple(v: f64, size: f64, n: f64) -> f64 {
|
||||
/// `v` moved `n` steps of `size` (down when negative), where each step goes to the nearer of the
|
||||
/// next multiple of `size` ([`step_to_multiple`]) and the next of `stops` in its direction. By 1 with
|
||||
/// stops at 0.25, 0.5 and 0.75, 0.52 goes down to 0.5 and up to 0.75, 1 goes down to 0.75, and 14
|
||||
/// goes up to 15. A stop that `same` finds equal to the value counts as reached: in a field, a stop
|
||||
/// shown with the same text as the value (`0.167 p` for 2 pt) moves on to the next.
|
||||
/// goes up to 15. A stop or multiple that `same` finds equal to the value counts as reached: a
|
||||
/// stop shown with the same text as the value (`0.167 p` for 2 pt, see [`shown_alike`]) moves on
|
||||
/// to the next.
|
||||
pub(super) fn step_through(v: f64, size: f64, n: f64, stops: &[f64], same: impl Fn(f64, f64) -> bool) -> f64 {
|
||||
let mut v = v;
|
||||
// One call takes at most 1,000 steps.
|
||||
for _ in 0..n.abs().min(1000.0) as u32 {
|
||||
let multiple = step_to_multiple(v, size, n.signum());
|
||||
let mut multiple = step_to_multiple(v, size, n.signum());
|
||||
if same(multiple, v) {
|
||||
multiple = step_to_multiple(multiple, size, n.signum());
|
||||
}
|
||||
let ahead = |s: f64| (if n > 0.0 { s > v } else { s < v }) && !same(s, v);
|
||||
let stop = stops.iter().copied().filter(|&s| ahead(s));
|
||||
v = if n > 0.0 { stop.fold(multiple, f64::min) } else { stop.fold(multiple, f64::max) };
|
||||
@@ -152,10 +156,17 @@ pub(super) fn step_through(v: f64, size: f64, n: f64, stops: &[f64], same: impl
|
||||
}
|
||||
|
||||
/// Whether `a` and `b` are within a millionth of `size` of each other.
|
||||
#[cfg(test)]
|
||||
pub(super) fn within_a_millionth(size: f64) -> impl Fn(f64, f64) -> bool {
|
||||
move |a, b| (a - b).abs() <= size.abs() * 1e-6
|
||||
}
|
||||
|
||||
/// Whether `show` gives `a` and `b` the same text, for [`step_through`]: the field's ↑/↓ and the
|
||||
/// spinner both treat a stop shown as the value as reached.
|
||||
pub(super) fn shown_alike(show: impl Fn(f64) -> String) -> impl Fn(f64, f64) -> bool {
|
||||
move |a, b| show(a) == show(b)
|
||||
}
|
||||
|
||||
/// ↑/↓ in focused numeric field `resp`, or the mouse wheel turned over it, step the number `buf`
|
||||
/// shows (`read` parses it, `show` formats it) per press or wheel notch ([`STEPS`]): a plain step
|
||||
/// goes to the next whole number in its direction (5.1 up to 6, down to 5), a Ctrl/Cmd step goes to
|
||||
@@ -231,7 +242,7 @@ fn step_value(
|
||||
// The field's own steps: ten at once with Shift, one with or without Ctrl/Cmd.
|
||||
Some(step) => step(v, if mods == egui::Modifiers::SHIFT { 10.0 * n } else { n }),
|
||||
None if mods == egui::Modifiers::SHIFT => v + size * n,
|
||||
None if mods == egui::Modifiers::NONE && !stops.is_empty() => step_through(v, size, n, stops, |a, b| show(a) == show(b)),
|
||||
None if mods == egui::Modifiers::NONE && !stops.is_empty() => step_through(v, size, n, stops, shown_alike(&show)),
|
||||
None => step_to_multiple(v, size, n),
|
||||
};
|
||||
// Each step stops at the bounds, so a step back in the same frame starts from there.
|
||||
@@ -604,7 +615,7 @@ fn spin_generic(
|
||||
&& let Some(v) = value
|
||||
{
|
||||
let modifiers = ui.input(|i| i.modifiers);
|
||||
out = Some(SpinPick::Value(spinner_step(v, step, min, up.contains(p), modifiers, stops)));
|
||||
out = Some(SpinPick::Value(spinner_step(v, step, min, up.contains(p), modifiers, stops, fmt)));
|
||||
}
|
||||
let fw = if presets.is_empty() { width - 15.0 } else { width - 34.0 };
|
||||
if let Some(v) = field(ui, fw, label) {
|
||||
@@ -633,11 +644,20 @@ fn spin_generic(
|
||||
|
||||
/// Spinner buttons follow the same modifier sizes as ↑/↓ in a focused number field:
|
||||
/// Shift ×10, Ctrl/Cmd ÷10, otherwise the caller's normal step, which also stops at each of
|
||||
/// `stops` ([`step_through`]).
|
||||
pub(super) fn spinner_step(value: f64, step: f64, min: f64, up: bool, modifiers: egui::Modifiers, stops: &[f64]) -> f64 {
|
||||
/// `stops` ([`step_through`]). A stop that `show` writes with the same text as the value counts as
|
||||
/// reached, as in the field.
|
||||
pub(super) fn spinner_step(
|
||||
value: f64,
|
||||
step: f64,
|
||||
min: f64,
|
||||
up: bool,
|
||||
modifiers: egui::Modifiers,
|
||||
stops: &[f64],
|
||||
show: &dyn Fn(f64) -> String,
|
||||
) -> f64 {
|
||||
let (mods, multiplier) = STEPS.iter().find(|(m, _)| modifiers.matches_logically(*m)).map_or((egui::Modifiers::NONE, 1.0), |&(m, f)| (m, f));
|
||||
if mods == egui::Modifiers::NONE && !stops.is_empty() {
|
||||
return step_through(value, step, if up { 1.0 } else { -1.0 }, stops, within_a_millionth(step)).max(min);
|
||||
return step_through(value, step, if up { 1.0 } else { -1.0 }, stops, shown_alike(show)).max(min);
|
||||
}
|
||||
let delta = step * multiplier;
|
||||
(value + if up { delta } else { -delta }).max(min)
|
||||
|
||||
Reference in New Issue
Block a user