mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-29 20:27:05 -06:00
Stroke Weight: a step from a weight between the presets goes to the next preset
A weight between the presets stepped to the next whole point, so 0.52 pt went down to 0 pt. In the Stroke panel, the Control bar and the Properties panel, a plain step of the spinner, the arrow keys or the wheel now goes to the nearer of the next whole point and the next preset in its direction: 0.52 pt goes down to 0.5 pt and up to 0.75 pt, 0.4 pt goes down to 0.25 pt, and 1 pt goes down to 0.75 pt. From 1 pt up a step is still a whole point (14 pt to 15 pt), and Shift and Ctrl/Cmd steps are unchanged. In the field, a preset keeps its decimal places (0.0156 in), and a preset the field shows with the same text as the value (2 pt as 0.167 p) counts as reached, so the next press goes on 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,31 @@ 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
@@ -278,15 +278,80 @@ mod spinner_modifier_tests {
|
||||
|
||||
#[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, &[]), 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);
|
||||
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);
|
||||
}
|
||||
|
||||
/// 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), 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);
|
||||
}
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,34 @@ 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 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.
|
||||
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 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.
|
||||
pub(super) fn within_a_millionth(size: f64) -> impl Fn(f64, f64) -> bool {
|
||||
move |a, b| (a - b).abs() <= size.abs() * 1e-6
|
||||
}
|
||||
|
||||
/// ↑/↓ 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 +173,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 +231,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, |a, b| show(a) == show(b)),
|
||||
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 +268,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 +375,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 +439,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 +510,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 +549,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 +567,7 @@ fn spin_generic(
|
||||
step: f64,
|
||||
min: f64,
|
||||
presets: &[f64],
|
||||
stops: &[f64],
|
||||
fmt: &dyn Fn(f64) -> String,
|
||||
field: &mut SpinField<'_>,
|
||||
) -> Option<SpinPick> {
|
||||
@@ -553,7 +604,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)));
|
||||
}
|
||||
let fw = if presets.is_empty() { width - 15.0 } else { width - 34.0 };
|
||||
if let Some(v) = field(ui, fw, label) {
|
||||
@@ -581,9 +632,13 @@ 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`]).
|
||||
pub(super) fn spinner_step(value: f64, step: f64, min: f64, up: bool, modifiers: egui::Modifiers, stops: &[f64]) -> 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);
|
||||
}
|
||||
let delta = step * multiplier;
|
||||
(value + if up { delta } else { -delta }).max(min)
|
||||
}
|
||||
@@ -645,6 +700,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