Plug-ins: a key or button held before the approval's OK took input approves nothing

The install approval's OK now also waits for these to be let go of:

- Enter or Space held down while OK was disabled, whose key repeat
  clicked OK once it had the keyboard focus.
- A pointer button held while OK was disabled, even after another
  button is pressed once OK takes input.
- A pointer button held on the question before, when a new question
  takes its place.

The wait also starts over when the window lost the focus since the
frame before, even if it has the focus again by the next frame.
This commit is contained in:
dotCooCoo
2026-11-11 19:49:06 -08:00
parent 6b8be09699
commit c792bdf0d3
2 changed files with 148 additions and 9 deletions
+21 -9
View File
@@ -367,7 +367,8 @@ pub(crate) struct Shown {
/// The pass number and time of the last frame that drew it.
pass: Option<u64>,
last: Option<f64>,
/// A pointer button went down before OK took input, and hasn't been let go of.
/// A pointer button, Enter or Space was down on a frame where OK was disabled, and they haven't
/// all been let go of since.
pressed_early: bool,
/// OK takes input: shown for [`APPROVAL_DELAY`], with no press from before that held.
ready: bool,
@@ -375,14 +376,19 @@ pub(crate) struct Shown {
impl Shown {
/// Notes a frame that draws the question in `ui` → whether OK is enabled on it. The showing
/// starts over when the frame before didn't draw it, when time went back, and on any frame
/// that is a measuring pass, is invisible or comes while the window doesn't have the focus.
/// starts over when the frame before didn't draw it, when time went back, when the window lost
/// the focus since the frame before, and on any frame that is a measuring pass, is invisible or
/// comes while the window doesn't have the focus.
fn drawn(&mut self, ui: &egui::Ui) -> bool {
let pass = ui.ctx().cumulative_pass_nr();
let (now, focused, pressed) = ui.input(|i| (i.time, i.focused, i.pointer.any_pressed()));
let (now, focused, left, pressed) = ui.input(|i| {
let left = i.events.iter().any(|e| matches!(e, egui::Event::WindowFocused(false)));
let pressed = i.pointer.any_pressed() || [egui::Key::Enter, egui::Key::Space].into_iter().any(|k| i.key_pressed(k));
(i.time, i.focused, left, pressed || Self::held(i))
});
let seen = focused && ui.is_visible() && !ui.is_sizing_pass() && now.is_finite();
let follows = self.pass.is_some_and(|p| p.checked_add(1) == Some(pass)) && self.last.is_some_and(|t| t <= now);
if !seen || !follows {
if !seen || !follows || left {
self.since = None;
}
if seen && self.since.is_none() {
@@ -390,8 +396,8 @@ impl Shown {
}
(self.pass, self.last) = (Some(pass), Some(now));
let enabled = self.since.is_some_and(|s| now - s >= APPROVAL_DELAY);
if pressed {
self.pressed_early = !enabled;
if pressed && !enabled {
self.pressed_early = true;
}
self.ready = enabled && !self.pressed_early;
if let Some(s) = self.since.filter(|_| !enabled) {
@@ -401,13 +407,19 @@ impl Shown {
}
/// After the frame's OK: a press from before OK took input stops counting once every pointer
/// button is up.
/// button, Enter and Space are up.
fn released(&mut self, ui: &egui::Ui) {
if !ui.input(|i| i.pointer.any_down()) {
if !ui.input(Self::held) {
self.pressed_early = false;
}
}
/// Whether a pointer button, Enter or Space is down: what can click OK when it is let go of or
/// repeats.
fn held(i: &egui::InputState) -> bool {
i.pointer.any_down() || i.key_down(egui::Key::Enter) || i.key_down(egui::Key::Space)
}
/// Whether OK takes input: the question was shown for [`APPROVAL_DELAY`] on the last frame
/// that drew it.
pub(crate) fn ready(&self) -> bool {
+127
View File
@@ -395,6 +395,133 @@ fn a_press_on_the_install_approval_from_before_ok_took_input_approves_nothing()
h.state_mut().run("plugin.remove", json!({"id": id})).unwrap();
}
/// A press of another button after OK took input doesn't let a primary press from before it
/// approve when that press is let go of.
#[test]
fn a_later_press_on_the_install_approval_keeps_an_early_press_from_approving() {
use egui_kittest::kittest::Queryable;
let id = "test.ui.approval.held.two";
let mut h = window(app());
let module = json!({"dataBase64": vectorcraft_format::base64_encode(&asking(id, "Two", r#"["doc.read"]"#))});
h.state_mut().run("ui.installPlugin", module).unwrap();
at(&mut h, 10.0);
at(&mut h, 10.0);
let pos = h.get_by_label("OK").rect().center();
let button = |button, pressed| egui::Event::PointerButton { pos, button, pressed, modifiers: Default::default() };
// The primary button goes down away from any widget and moves over OK.
let away = egui::pos2(2.0, 2.0);
h.input_mut().events.extend([
egui::Event::PointerMoved(away),
egui::Event::PointerButton { pos: away, button: egui::PointerButton::Primary, pressed: true, modifiers: Default::default() },
]);
at(&mut h, 10.1);
h.input_mut().events.push(egui::Event::PointerMoved(pos));
at(&mut h, 10.2);
at(&mut h, 10.5);
assert!(ok_enabled(&h));
h.input_mut().events.push(button(egui::PointerButton::Secondary, true));
at(&mut h, 10.55);
h.input_mut().events.push(button(egui::PointerButton::Primary, false));
at(&mut h, 10.6);
assert!(waiting(h.state(), id), "the primary press began before OK took input");
h.input_mut().events.push(button(egui::PointerButton::Secondary, false));
at(&mut h, 10.65);
click_ok(&mut h, 10.8);
assert!(h.state().ui.dialog.is_none(), "{}", h.state().ui.status);
h.state_mut().run("plugin.remove", json!({"id": id})).unwrap();
}
/// A press on the OK of one question that is still held when another question takes its place
/// doesn't approve the new one when it is let go of.
#[test]
fn a_press_held_from_the_question_before_approves_nothing() {
use egui_kittest::kittest::Queryable;
let (a, b) = ("test.ui.approval.carried.a", "test.ui.approval.carried.b");
let module = |id: &str| json!({"dataBase64": vectorcraft_format::base64_encode(&asking(id, "Carried", r#"["doc.read"]"#))});
let mut h = window(app());
h.state_mut().run("ui.installPlugin", module(a)).unwrap();
at(&mut h, 10.0);
at(&mut h, 10.0);
at(&mut h, 10.6);
assert!(ok_enabled(&h));
let pos = h.get_by_label("OK").rect().center();
let button = |pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
h.input_mut().events.extend([egui::Event::PointerMoved(pos), button(true)]);
at(&mut h, 10.65);
// While the button is down, a request closes the question and asks about another module.
dialogs::cancel(h.state_mut());
h.state_mut().run("ui.installPlugin", module(b)).unwrap();
at(&mut h, 10.7);
at(&mut h, 11.0);
at(&mut h, 11.2);
assert!(ok_enabled(&h));
h.input_mut().events.push(button(false));
at(&mut h, 11.25);
assert!(waiting(h.state(), b) && vectorcraft_plugins::registry::get(a).is_none(), "the press began on the question before");
click_ok(&mut h, 11.4);
assert!(h.state().ui.dialog.is_none(), "{}", h.state().ui.status);
assert!(vectorcraft_plugins::registry::get(b).is_some());
h.state_mut().run("plugin.remove", json!({"id": b})).unwrap();
}
/// Enter or Space held down while OK is disabled doesn't approve through its key repeat once OK
/// has the keyboard focus; a new press after it is let go of does.
#[test]
fn a_key_held_on_the_install_approval_from_before_ok_took_input_approves_nothing() {
use egui_kittest::kittest::Queryable;
for (n, key) in [egui::Key::Enter, egui::Key::Space].into_iter().enumerate() {
let id = format!("test.ui.approval.key.{n}");
let mut h = window(app());
let module = json!({"dataBase64": vectorcraft_format::base64_encode(&asking(&id, "Key", r#"["doc.read"]"#))});
h.state_mut().run("ui.installPlugin", module).unwrap();
at(&mut h, 10.0);
at(&mut h, 10.0);
let event = |pressed, repeat| egui::Event::Key { key, physical_key: Some(key), pressed, repeat, modifiers: Default::default() };
h.input_mut().events.push(event(true, false));
at(&mut h, 10.1);
at(&mut h, 10.6);
assert!(ok_enabled(&h));
h.get_by_label("OK").focus();
at(&mut h, 10.65);
h.input_mut().events.push(event(true, true));
at(&mut h, 10.7);
assert!(waiting(h.state(), &id), "{key:?}: the key went down before OK took input");
h.input_mut().events.push(event(false, false));
at(&mut h, 10.75);
h.get_by_label("OK").focus();
at(&mut h, 10.8);
h.key_press(key);
at(&mut h, 10.85);
assert!(h.state().ui.dialog.is_none(), "{key:?}: {}", h.state().ui.status);
h.state_mut().run("plugin.remove", json!({"id": id})).unwrap();
}
}
/// The wait starts over when the window loses the focus and gets it back between two frames.
#[test]
fn the_install_approval_waits_again_when_the_focus_leaves_and_comes_back_between_frames() {
let id = "test.ui.approval.blink";
let mut h = window(app());
let module = json!({"dataBase64": vectorcraft_format::base64_encode(&asking(id, "Blink", r#"["doc.read"]"#))});
h.state_mut().run("ui.installPlugin", module).unwrap();
at(&mut h, 10.0);
at(&mut h, 10.0);
at(&mut h, 10.6);
assert!(ok_enabled(&h));
h.input_mut().events.extend([egui::Event::WindowFocused(false), egui::Event::WindowFocused(true)]);
at(&mut h, 10.7);
assert!(!ok_enabled(&h), "the focus left since the frame before");
click_ok(&mut h, 10.8);
assert!(waiting(h.state(), id));
at(&mut h, 11.19);
assert!(!ok_enabled(&h));
at(&mut h, 11.2);
assert!(ok_enabled(&h), "back in front, it waits again");
click_ok(&mut h, 11.3);
assert!(h.state().ui.dialog.is_none(), "{}", h.state().ui.status);
h.state_mut().run("plugin.remove", json!({"id": id})).unwrap();
}
/// A module with other bytes than the one installed with its id asks before taking its place: in
/// the ordinary confirm when it needs nothing that only a person can approve, and in the approval
/// dialog, with its id and file hash, when it also asks for a heavy budget.