mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-31 13:17:19 -06:00
Spinners: a held arrow repeats after a pause (#4267)
This commit is contained in:
@@ -427,3 +427,44 @@ fn a_popover_stays_open_for_a_click_on_its_anchor_or_during_a_panel_drag() {
|
||||
let open = [open_after_click(far, None), open_after_click(None, None), open_after_click(far, panel_drag())];
|
||||
assert_eq!(open, [false, true, true]);
|
||||
}
|
||||
|
||||
/// #4267: a click on a spinner arrow steps once; an arrow held down steps again and again after a
|
||||
/// pause, and letting go of it doesn't step once more.
|
||||
#[test]
|
||||
fn a_held_spinner_arrow_repeats() {
|
||||
let ctx = themed(Default::default());
|
||||
let value = std::cell::Cell::new(5.0);
|
||||
let mut add = |ui: &mut Ui| {
|
||||
ui.horizontal(|ui| {
|
||||
if let Some(v) = spin_plain(ui, "repeat", value.get(), "", 0, 100.0, 1.0, 0.0, &[]) {
|
||||
value.set(v);
|
||||
}
|
||||
});
|
||||
};
|
||||
let shapes = frame(&ctx, 0.0, vec![], &mut add);
|
||||
let spinner = shapes
|
||||
.iter()
|
||||
.filter_map(|s| match s {
|
||||
egui::Shape::Rect(r) if r.rect.height() > 20.0 && r.fill != Color32::TRANSPARENT => Some(r.rect),
|
||||
_ => None,
|
||||
})
|
||||
.min_by(|a, b| a.min.x.total_cmp(&b.min.x))
|
||||
.expect("the spinner box");
|
||||
let up = spinner.center_top() + vec2(0.0, 6.0);
|
||||
let button = |pressed| egui::Event::PointerButton { pos: up, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
// A click: one step, on release.
|
||||
frame(&ctx, 0.1, vec![egui::Event::PointerMoved(up), button(true)], &mut add);
|
||||
frame(&ctx, 0.2, vec![button(false)], &mut add);
|
||||
assert_eq!(value.get(), 6.0, "a click steps once");
|
||||
// Held: nothing more until the pause is over, then a step every repeat.
|
||||
frame(&ctx, 1.0, vec![button(true)], &mut add);
|
||||
frame(&ctx, 1.3, vec![], &mut add);
|
||||
assert_eq!(value.get(), 6.0, "no step before the pause");
|
||||
for (i, t) in [1.5, 1.6, 1.7].into_iter().enumerate() {
|
||||
frame(&ctx, t, vec![], &mut add);
|
||||
assert_eq!(value.get(), 7.0 + i as f64, "a step every repeat at {t}");
|
||||
}
|
||||
frame(&ctx, 1.75, vec![button(false)], &mut add);
|
||||
frame(&ctx, 1.8, vec![], &mut add);
|
||||
assert_eq!(value.get(), 9.0, "letting go doesn't step once more");
|
||||
}
|
||||
|
||||
@@ -620,8 +620,10 @@ 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.
|
||||
if sresp.clicked()
|
||||
// A blank field (values that differ) has nothing to step from. 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
|
||||
{
|
||||
@@ -653,6 +655,29 @@ fn spin_generic(
|
||||
out
|
||||
}
|
||||
|
||||
/// How long a spinner arrow is held before it repeats, and how often it repeats then, in seconds.
|
||||
const REPEAT_DELAY: f64 = 0.45;
|
||||
const REPEAT_EVERY: f64 = 0.06;
|
||||
|
||||
/// A spinner arrow (`resp`) held down. `None` until it has been held for [`REPEAT_DELAY`]: a
|
||||
/// click then steps once, on release. After that, `Some(true)` on each frame it steps again
|
||||
/// (every [`REPEAT_EVERY`]) and `Some(false)` on the frames between and on the release, which
|
||||
/// must not step once more.
|
||||
fn held_repeat(ui: &Ui, resp: &egui::Response) -> Option<bool> {
|
||||
let id = resp.id.with("repeat");
|
||||
let now = ui.input(|i| i.time);
|
||||
let state: Option<(f64, f64, bool)> = ui.data(|d| d.get_temp(id));
|
||||
if !resp.is_pointer_button_down_on() {
|
||||
ui.data_mut(|d| d.remove::<(f64, f64, bool)>(id));
|
||||
return state.filter(|(_, _, repeated)| *repeated).map(|_| false);
|
||||
}
|
||||
let (start, last, repeated) = state.unwrap_or((now, now, false));
|
||||
let due = now - start >= REPEAT_DELAY && now - last >= REPEAT_EVERY;
|
||||
ui.data_mut(|d| d.insert_temp(id, (start, if due { now } else { last }, repeated || due)));
|
||||
ui.ctx().request_repaint_after(std::time::Duration::from_millis(30));
|
||||
(repeated || due).then_some(due)
|
||||
}
|
||||
|
||||
/// 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`]). A stop that `show` writes with the same text as the value counts as
|
||||
|
||||
Reference in New Issue
Block a user