mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 05:37:23 -06:00
Merge pull request #4363 from dotCooCoo/fix/stroke-weight-steps-off-list
Stroke Weight: a step from a weight between the presets goes to the next preset
This commit is contained in:
@@ -127,10 +127,11 @@ pub(crate) fn shown_weight(app: &VectorcraftApp, st: Option<&StrokeLayer>, mixed
|
||||
(!mixed.weight).then(|| st.map_or(app.session.paint.stroke_width, |s| s.width))
|
||||
}
|
||||
|
||||
/// The weight spinner (Stroke panel, Control bar): Units > Stroke, presets, blank when mixed.
|
||||
/// The weight spinner (Stroke panel, Control bar, Properties panel): Units > Stroke, presets, blank
|
||||
/// when mixed. A plain step stops at each preset: 0.52 pt goes down to 0.5 pt, not to 0 pt.
|
||||
pub(crate) fn weight_field(app: &mut VectorcraftApp, ui: &mut Ui, id: &str, weight: Option<f64>, width: f32) {
|
||||
let unit = app.session.stroke_unit();
|
||||
if let Some(w) = widgets::spin_field_named(ui, id, Some(tl!("Weight:")), weight, unit, width, 1.0, 0.0, &weight_presets(unit)) {
|
||||
if let Some(w) = widgets::spin_field_stops_named(ui, id, Some(tl!("Weight:")), weight, unit, width, 1.0, 0.0, &weight_presets(unit)) {
|
||||
set(app, json!({"weight": w}));
|
||||
}
|
||||
}
|
||||
@@ -742,7 +743,8 @@ mod tests {
|
||||
};
|
||||
// The weight and the limit.
|
||||
let stroke = |app: &VectorcraftApp| current_stroke(app).map(|s| [s.width, s.miter_limit]).unwrap();
|
||||
for (shown, after, which, low) in [("1 pt", "0 pt", 0, 0.0), ("2 x", "1 x", 1, 1.0)] {
|
||||
// A weight steps through the presets below 1 pt: one step above 0 pt is 0.25 pt.
|
||||
for (shown, after, which, low, back) in [("1 pt", "0 pt", 0, 0.0, "0.25 pt"), ("2 x", "1 x", 1, 1.0, "2 x")] {
|
||||
let texts = frame(&mut app, vec![]);
|
||||
let at = texts.iter().find(|(t, _)| t == shown).map(|(_, r)| r.center()).unwrap_or_else(|| panic!("no {shown:?} in {texts:?}"));
|
||||
let button = |pressed| Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE };
|
||||
@@ -766,7 +768,104 @@ mod tests {
|
||||
let key = |key| Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE };
|
||||
frame(&mut app, vec![key(egui::Key::ArrowDown), key(egui::Key::ArrowUp)]);
|
||||
let texts = frame(&mut app, vec![]);
|
||||
assert!(texts.iter().any(|(t, _)| t == shown), "{after}, down and up → {shown}: {texts:?}");
|
||||
assert!(texts.iter().any(|(t, _)| t == back), "{after}, down and up → {back}: {texts:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// ↑/↓ in the Weight field go from a weight between the listed weights to the next listed
|
||||
/// weight in their direction, typed with a decimal comma too: 0,52 pt went down to 0 pt and the
|
||||
/// stroke was removed. From 1 pt and up a step is still a whole point.
|
||||
#[test]
|
||||
fn the_arrows_step_a_weight_to_the_next_listed_weight() {
|
||||
use egui::{Event, Key, Modifiers, PointerButton, Pos2};
|
||||
use vectorcraft_engine::Session;
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
|
||||
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 100, "height": 80})).unwrap();
|
||||
app.run("stroke.set", json!({"weight": 1})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let mut time = 0.0;
|
||||
let mut frame = |app: &mut VectorcraftApp, events: Vec<Event>| {
|
||||
time += 0.05;
|
||||
let screen_rect = Some(Rect::from_min_size(Pos2::ZERO, vec2(300.0, 600.0)));
|
||||
let raw = egui::RawInput { events, time: Some(time), screen_rect, focused: true, ..Default::default() };
|
||||
let mut out = ctx.run_ui(raw, |ui| show(app, ui));
|
||||
out.textures_delta.clear();
|
||||
out.shapes
|
||||
.into_iter()
|
||||
.filter_map(|s| match s.shape {
|
||||
egui::Shape::Text(t) => Some((t.galley.text().to_string(), Rect::from_min_size(t.pos, t.galley.size()))),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let texts = frame(&mut app, vec![]);
|
||||
let at = texts.iter().find(|(t, _)| t == "1 pt").map(|(_, r)| r.center()).unwrap_or_else(|| panic!("no 1 pt in {texts:?}"));
|
||||
let key = |key| Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE };
|
||||
let click = |pos| {
|
||||
let button = |pressed| Event::PointerButton { pos, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE };
|
||||
[vec![Event::PointerMoved(pos), button(true)], vec![button(false)], vec![]]
|
||||
};
|
||||
let away = Pos2::new(290.0, 590.0);
|
||||
// (Units > Stroke, points per unit, typed, arrow, the weight after in that unit). A preset
|
||||
// with more places than a step keeps them: 0.0156 in goes down to 0.0078 in.
|
||||
for (units, per_unit, typed, arrow, after) in [
|
||||
("points", 1.0, "0,52", Key::ArrowDown, 0.5),
|
||||
("points", 1.0, "0,51", Key::ArrowDown, 0.5),
|
||||
("points", 1.0, "0,4", Key::ArrowDown, 0.25),
|
||||
("points", 1.0, "0,52", Key::ArrowUp, 0.75),
|
||||
("points", 1.0, "0,1", Key::ArrowUp, 0.25),
|
||||
("points", 1.0, "0,75", Key::ArrowDown, 0.5),
|
||||
("points", 1.0, "0,5", Key::ArrowDown, 0.25),
|
||||
("points", 1.0, "1", Key::ArrowDown, 0.75),
|
||||
("points", 1.0, "1", Key::ArrowUp, 2.0),
|
||||
("points", 1.0, "1,5", Key::ArrowDown, 1.0),
|
||||
("points", 1.0, "14", Key::ArrowDown, 13.0),
|
||||
("points", 1.0, "14,5", Key::ArrowUp, 15.0),
|
||||
("inches", 72.0, "0,02", Key::ArrowDown, 0.0156),
|
||||
("inches", 72.0, "0,0156", Key::ArrowDown, 0.0078),
|
||||
("inches", 72.0, "0,016", Key::ArrowDown, 0.0156),
|
||||
] {
|
||||
app.session.prefs.units_stroke = units.into();
|
||||
for events in click(at) {
|
||||
frame(&mut app, events);
|
||||
}
|
||||
frame(&mut app, vec![Event::Text(typed.into()), key(Key::Enter)]);
|
||||
for events in click(away) {
|
||||
frame(&mut app, events);
|
||||
}
|
||||
let typed_weight = typed.replace(',', ".").parse::<f64>().unwrap();
|
||||
assert!((current_stroke(&app).unwrap().width - typed_weight * per_unit).abs() < 1e-9, "typed {typed}");
|
||||
for events in click(at) {
|
||||
frame(&mut app, events);
|
||||
}
|
||||
frame(&mut app, vec![key(arrow)]);
|
||||
frame(&mut app, vec![]);
|
||||
let w = current_stroke(&app).unwrap().width / per_unit;
|
||||
assert!((w - after).abs() < 1e-9, "{typed} {units} and {arrow:?}: {w}");
|
||||
for events in click(away) {
|
||||
frame(&mut app, events);
|
||||
}
|
||||
}
|
||||
// In picas the field shows 2 pt as 0.167 p; the next press goes on to 1 pt.
|
||||
app.session.prefs.units_stroke = "picas".into();
|
||||
for events in click(at) {
|
||||
frame(&mut app, events);
|
||||
}
|
||||
frame(&mut app, vec![Event::Text("0p3".into()), key(Key::Enter)]);
|
||||
for events in click(away) {
|
||||
frame(&mut app, events);
|
||||
}
|
||||
assert_eq!(current_stroke(&app).unwrap().width, 3.0, "typed 0p3");
|
||||
for events in click(at) {
|
||||
frame(&mut app, events);
|
||||
}
|
||||
for (arrow, after) in [(Key::ArrowDown, 2.0), (Key::ArrowDown, 1.0), (Key::ArrowUp, 2.0), (Key::ArrowUp, 3.0)] {
|
||||
frame(&mut app, vec![key(arrow)]);
|
||||
frame(&mut app, vec![]);
|
||||
let w = current_stroke(&app).unwrap().width;
|
||||
assert!((w - after).abs() < 1e-9, "picas, {arrow:?} → {after} pt: {w}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,3 +194,52 @@ fn stroke_weight_stepper_uses_ten_point_shift_steps() {
|
||||
click_step(&mut app, r.center().y + 5.0, egui::Modifiers::COMMAND);
|
||||
assert!((app.session.shown_stroke().unwrap().width - 12.9).abs() < 1e-6);
|
||||
}
|
||||
|
||||
/// The weight spinner in the Stroke panel, the Control bar and the Properties panel steps from a
|
||||
/// weight between the presets to the next preset: 0.52 pt went down to 0 pt.
|
||||
#[test]
|
||||
fn every_weight_spinner_steps_to_the_next_preset() {
|
||||
use super::tests_appearance::frame_raw;
|
||||
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
type Show = fn(&mut VectorcraftApp, &mut Ui);
|
||||
let surfaces: [(&str, Show); 3] =
|
||||
[("Stroke panel", stroke::show), ("Control bar", crate::chrome::control_bar), ("Properties panel", properties::show)];
|
||||
for (surface, show) in surfaces {
|
||||
for (up, after) in [(false, 0.5), (true, 0.75)] {
|
||||
let mut app = app_with_rect();
|
||||
run(&mut app, "stroke.set", json!({"weight": 0.52}));
|
||||
let t = texts(&ctx, &mut app, show);
|
||||
let r = text_rect(&t, "0.52 pt");
|
||||
let pos = egui::pos2(r.left() - 14.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() }, show);
|
||||
}
|
||||
assert_eq!(app.session.shown_stroke().unwrap().width, after, "{surface}, up: {up}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
|
||||
@@ -49,11 +49,11 @@ pub use labels::{dim_label, dim_name, divider, field_label, label_row, link_labe
|
||||
pub(crate) use labels::{label_field, take_previous_label};
|
||||
pub use numeric::{
|
||||
SpinPick, mixed_field, mixed_field_named, num_field, num_field_named, opt_field, opt_field_named, range_field, range_field_named,
|
||||
range_field_stepped, size_field, size_field_named, slider, spin_field, spin_field_auto, spin_field_auto_named, spin_field_named, spin_plain,
|
||||
spin_plain_named,
|
||||
range_field_stepped, size_field, size_field_named, slider, spin_field, spin_field_auto, spin_field_auto_named, spin_field_named,
|
||||
spin_field_stops_named, spin_plain, spin_plain_named,
|
||||
};
|
||||
#[cfg(test)]
|
||||
use numeric::{decimal_point, share_of, spinner_step, step_to_multiple};
|
||||
use numeric::{decimal_point, share_of, spinner_step, step_through, step_to_multiple, within_a_millionth};
|
||||
pub(crate) use numeric::{round_to, set_bare_numbers_are_points, typed_unit};
|
||||
pub use panel_chrome::{bottom_bar, layout_button, list_box, orientation_button, tab_bar};
|
||||
pub(crate) use text::{dialog_focus_flag, enter_modifiers, focus_field, own_field_shift_enter, set_dialog_open, take_dialog_focus};
|
||||
@@ -276,17 +276,98 @@ 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.
|
||||
#[test]
|
||||
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, &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);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod step_through_tests {
|
||||
use super::*;
|
||||
|
||||
/// Each step goes to the nearer of the next whole step and the next stop in its direction.
|
||||
#[test]
|
||||
fn a_step_goes_to_the_nearer_of_the_next_multiple_and_the_next_stop() {
|
||||
let stops = [0.25, 0.5, 0.75, 1.0, 2.0, 3.0, 10.0, 20.0, 100.0];
|
||||
for (v, n, want) in [
|
||||
(0.52, -1.0, 0.5),
|
||||
(0.51, -1.0, 0.5),
|
||||
(0.4, -1.0, 0.25),
|
||||
(0.52, 1.0, 0.75),
|
||||
(0.1, 1.0, 0.25),
|
||||
(0.1, -1.0, 0.0),
|
||||
(0.25, -1.0, 0.0),
|
||||
(0.75, -1.0, 0.5),
|
||||
(1.0, -1.0, 0.75),
|
||||
(1.0, 1.0, 2.0),
|
||||
(1.5, -1.0, 1.0),
|
||||
(14.0, 1.0, 15.0),
|
||||
(14.0, -1.0, 13.0),
|
||||
(14.5, 1.0, 15.0),
|
||||
(0.4, -2.0, 0.0),
|
||||
(0.4, 3.0, 1.0),
|
||||
(100.0, 1.0, 101.0),
|
||||
(0.52, 0.0, 0.52),
|
||||
] {
|
||||
let got = step_through(v, 1.0, n, &stops, within_a_millionth(1.0));
|
||||
assert!((got - want).abs() < 1e-9, "{v} × {n}: {got}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A value a float error away from a stop moves on to the next one.
|
||||
#[test]
|
||||
fn a_value_next_to_a_stop_moves_past_it() {
|
||||
assert_eq!(step_through(0.5 + 1e-9, 1.0, -1.0, &[0.25, 0.5], within_a_millionth(1.0)), 0.25);
|
||||
assert_eq!(step_through(0.5 - 1e-9, 1.0, 1.0, &[0.5, 0.75], within_a_millionth(1.0)), 0.75);
|
||||
}
|
||||
|
||||
/// A stop shown with the same text as the value moves on to the next one: 2 pt shows as
|
||||
/// `0.167 p`. A stop shown differently is still reached: `0.016 in` goes down to `0.0156 in`.
|
||||
#[test]
|
||||
fn a_stop_shown_as_the_value_moves_past_it() {
|
||||
let shown = |unit: Unit| move |a: f64, b: f64| unit.number(unit.to_pt(a)) == unit.number(unit.to_pt(b));
|
||||
let picas = [1.0 / 12.0, 2.0 / 12.0, 3.0 / 12.0];
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ pub fn num_field(ui: &mut Ui, id: impl std::hash::Hash, value: Option<f64>, unit
|
||||
/// its label painted separately): screen readers name the field `name` (already translated).
|
||||
pub fn num_field_named(ui: &mut Ui, id: impl std::hash::Hash, name: Option<&str>, value: Option<f64>, unit: Unit, width: f32) -> Option<f64> {
|
||||
let label = take_previous_label(ui);
|
||||
unit_field(ui, id, label, name, value, unit, width, false, ANY)
|
||||
unit_field(ui, id, label, name, value, unit, width, false, ANY, &[])
|
||||
}
|
||||
|
||||
/// Any number: the bounds of a field that has none.
|
||||
@@ -64,7 +64,8 @@ fn within(v: f64, bounds: &RangeInclusive<f64>) -> f64 {
|
||||
|
||||
/// [`num_field`]; with `share_of_value` a percentage typed alone is that share of `value`
|
||||
/// ([`share_of`]). ↑/↓, the wheel and a scrub stop at `bounds` (points), and the field shows where
|
||||
/// they stopped.
|
||||
/// they stopped. A plain ↑/↓ or wheel step also stops at each of `stops` (points; see
|
||||
/// [`step_through`]).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn unit_field(
|
||||
ui: &mut Ui,
|
||||
@@ -76,6 +77,7 @@ fn unit_field(
|
||||
width: f32,
|
||||
share_of_value: bool,
|
||||
bounds: RangeInclusive<f64>,
|
||||
stops: &[f64],
|
||||
) -> Option<f64> {
|
||||
let id = id_with(ui.scope_id(), id);
|
||||
let shown = value.map(|v| unit.format(v)).unwrap_or_default();
|
||||
@@ -86,7 +88,8 @@ fn unit_field(
|
||||
let typed = typed_unit(ui.ctx(), unit);
|
||||
let read = |b: &str| typed.parse(&decimal_point(b));
|
||||
let bounds = unit.from_pt(*bounds.start())..=unit.from_pt(*bounds.end());
|
||||
let stepped = step_value(ui, &resp, &mut buf, &bounds, (|b: &str| Some(unit.from_pt(read(b)?)), |v| unit.format(unit.to_pt(v))), None);
|
||||
let stops: Vec<f64> = stops.iter().map(|&s| unit.from_pt(s)).collect();
|
||||
let stepped = step_value(ui, &resp, &mut buf, &bounds, (|b: &str| Some(unit.from_pt(read(b)?)), |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);
|
||||
let commit = commit && buf != shown;
|
||||
ui.data_mut(|d| d.insert_temp(id, buf.clone()));
|
||||
@@ -131,11 +134,45 @@ pub(super) fn step_to_multiple(v: f64, size: f64, n: f64) -> f64 {
|
||||
(from + n) * size
|
||||
}
|
||||
|
||||
/// `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 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 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) };
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// the next tenth, and a Shift step adds or takes ten. Steps stop at the field's `bounds` (#3370:
|
||||
/// a weight stops at 0, a miter limit at 1). The new value is applied at once, as in
|
||||
/// a weight stops at 0, a miter limit at 1), and a plain step also stops at each of `stops`
|
||||
/// ([`step_through`]). The new value is applied at once, as in
|
||||
/// Illustrator's panels and dialogs, and the new text replaces `buf`, all selected so typing
|
||||
/// replaces it. Returns the new number when a step was taken. While the pointer is over the focused
|
||||
/// field the wheel is the field's (the panel under it doesn't scroll); an unfocused field leaves the
|
||||
@@ -147,6 +184,7 @@ fn step_value(
|
||||
bounds: &RangeInclusive<f64>,
|
||||
(read, show): (impl Fn(&str) -> Option<f64>, impl Fn(f64) -> String),
|
||||
step: Option<fn(f64, f64) -> f64>,
|
||||
stops: &[f64],
|
||||
) -> Option<f64> {
|
||||
// Memory focus, as the field's highlight and its typing use: `Response::has_focus` also needs the
|
||||
// window to report keyboard focus.
|
||||
@@ -204,13 +242,14 @@ 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, 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.
|
||||
v = within(v, bounds);
|
||||
}
|
||||
// Rounded as fields show it, within the field's bounds.
|
||||
let v = within(round_to(v, STEP_DECIMALS), bounds);
|
||||
// Rounded as fields show it, within the field's bounds; a stop keeps its places (0.0156 in).
|
||||
let v = within(if stops.contains(&v) { v } else { round_to(v, STEP_DECIMALS) }, bounds);
|
||||
*buf = show(v);
|
||||
ui.data_mut(|d| d.insert_temp(resp.id, buf.clone()));
|
||||
select_all(ui, resp.id, buf);
|
||||
@@ -240,7 +279,7 @@ pub fn size_field_named(ui: &mut Ui, id: impl std::hash::Hash, name: Option<&str
|
||||
}
|
||||
});
|
||||
}
|
||||
unit_field(ui, id, label, name, value, unit, width, true, ANY)
|
||||
unit_field(ui, id, label, name, value, unit, width, true, ANY, &[])
|
||||
}
|
||||
|
||||
/// [`plain_field`] for a number kept within `range` (the values of dialogs, preferences and panels
|
||||
@@ -347,7 +386,7 @@ fn bounded_field(
|
||||
// ↑/↓ and the wheel step at the field's precision (a count ignores Ctrl/Cmd's tenth); a drag on
|
||||
// its label scrubs it.
|
||||
let decimals = decimals.min(6) as i32;
|
||||
let stepped = step_value(ui, &resp, &mut buf, bounds, (read, show), step).map(|v| round_to(v, decimals));
|
||||
let stepped = step_value(ui, &resp, &mut buf, bounds, (read, show), step, &[]).map(|v| round_to(v, decimals));
|
||||
let scrubbed = scrub::field(ui, id, rect, &buf, value, decimals, bounds);
|
||||
if commit && buf != shown { read(&buf) } else { stepped.or(scrubbed).filter(|&v| Some(v) != value) }
|
||||
}
|
||||
@@ -411,6 +450,28 @@ pub fn spin_field_named(
|
||||
spin_field_auto_named(ui, id, name, value, None, unit, width, step, min, presets).and_then(SpinPick::value)
|
||||
}
|
||||
|
||||
/// [`spin_field_named`] whose plain steps (the spinner, ↑/↓ and the wheel without a modifier) also
|
||||
/// stop at each of its presets ([`step_through`]), for the Stroke Weight: by 1 pt with presets at
|
||||
/// 0.25, 0.5 and 0.75 pt, 0.52 pt goes down to 0.5 pt and up to 0.75 pt.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn spin_field_stops_named(
|
||||
ui: &mut Ui,
|
||||
id: impl std::hash::Hash + Copy,
|
||||
name: Option<&str>,
|
||||
value: Option<f64>,
|
||||
unit: Unit,
|
||||
width: f32,
|
||||
step: f64,
|
||||
min: f64,
|
||||
presets: &[f64],
|
||||
) -> Option<f64> {
|
||||
let label = take_previous_label(ui);
|
||||
spin_generic(ui, label, value, 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)
|
||||
})
|
||||
.and_then(SpinPick::value)
|
||||
}
|
||||
|
||||
/// What a [`spin_field_auto`] returns: a value (points), or its Auto entry.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum SpinPick {
|
||||
@@ -460,8 +521,8 @@ pub fn spin_field_auto_named(
|
||||
presets: &[f64],
|
||||
) -> Option<SpinPick> {
|
||||
let label = take_previous_label(ui);
|
||||
spin_generic(ui, label, value, auto, 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)
|
||||
spin_generic(ui, label, value, auto, 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, &[])
|
||||
})
|
||||
}
|
||||
|
||||
@@ -499,7 +560,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, width, step, min, presets, &fmt, &mut |ui, fw, label| {
|
||||
spin_generic(ui, label, value, None, width, step, min, presets, &[], &fmt, &mut |ui, fw, label| {
|
||||
bounded_field(ui, id, label, name, value, suffix, decimals, fw, &(min..=f64::INFINITY), None)
|
||||
})
|
||||
.and_then(SpinPick::value)
|
||||
@@ -517,6 +578,7 @@ fn spin_generic(
|
||||
step: f64,
|
||||
min: f64,
|
||||
presets: &[f64],
|
||||
stops: &[f64],
|
||||
fmt: &dyn Fn(f64) -> String,
|
||||
field: &mut SpinField<'_>,
|
||||
) -> Option<SpinPick> {
|
||||
@@ -553,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)));
|
||||
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) {
|
||||
@@ -581,9 +643,22 @@ 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.
|
||||
pub(super) 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);
|
||||
/// Shift ×10, Ctrl/Cmd ÷10, otherwise the caller's normal step, which also stops at each of
|
||||
/// `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, shown_alike(show)).max(min);
|
||||
}
|
||||
let delta = step * multiplier;
|
||||
(value + if up { delta } else { -delta }).max(min)
|
||||
}
|
||||
@@ -645,6 +720,7 @@ pub fn opt_field_named(ui: &mut Ui, id: impl std::hash::Hash, name: Option<&str>
|
||||
&lengths,
|
||||
(|b: &str| Some(unit.from_pt(unit.parse(&decimal_point(b))?)), |v| unit.number(unit.to_pt(v))),
|
||||
None,
|
||||
&[],
|
||||
);
|
||||
let scrubbed = scrub::field(ui, id, framed.response.rect, &buf, value.map(|v| unit.from_pt(v)), STEP_DECIMALS, &lengths);
|
||||
let focused = ui.memory(|m| m.has_focus(id));
|
||||
|
||||
Reference in New Issue
Block a user