Tools bar: take first-click modifiers from same-frame double-click releases.

When two primary releases land on a slot in one frame, derive prev_modified from the earlier release so an Alt-then-plain pair cannot create-on-double-click.
This commit is contained in:
tareq committed 2026-10-30 13:22:38 +01:00
1 parent 56974e7262
commit 55a6154ac7
1 file changed
+53 -7
+53 -7
View File
@@ -105,28 +105,36 @@ fn tool_slot(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, si:
// Plain click selects the shown tool; Alt/Option-click cycles the flyout slot
// (After Effects / Photoshop parity with the Shift+letter key cycle).
// Prefer the primary-release event's modifiers: Alt may already be up by the
// time this frame reads i.modifiers.
let click_mods = ui.input(|i| {
// time this frame reads i.modifiers. When a double-click lands as two releases
// in one frame, the earlier release is the first click.
let releases: Vec<egui::Modifiers> = ui.input(|i| {
i.events
.iter()
.rev()
.find_map(|e| match e {
.filter_map(|e| match e {
egui::Event::PointerButton {
pos,
button: egui::PointerButton::Primary,
pressed: false,
modifiers,
..
} => Some(*modifiers),
} if resp.rect.contains(*pos) => Some(*modifiers),
_ => None,
})
.unwrap_or(i.modifiers)
.collect()
});
let click_mods = releases.last().copied().unwrap_or_else(|| ui.input(|i| i.modifiers));
let alt = click_mods.alt;
let modified = click_mods.alt || click_mods.shift || click_mods.ctrl || click_mods.command;
// egui's double-click ignores modifiers: remember whether the previous click
// was modified so create-on-double-click only runs for two plain clicks.
let prev_mod_id = id.with("click-mod");
let prev_modified = ui.data(|d| d.get_temp::<bool>(prev_mod_id)).unwrap_or(false);
let prev_modified = match releases.len() {
n if n >= 2 => {
let m = releases[n - 2];
m.alt || m.shift || m.ctrl || m.command
}
_ => ui.data(|d| d.get_temp::<bool>(prev_mod_id)).unwrap_or(false),
};
let slot_click = resp.clicked() && !held;
if slot_click {
let pick = if slot.len() > 1 && alt {
@@ -971,6 +979,44 @@ mod tests {
assert_eq!(app.ui.tool, Tool::Type, "wraps back to Type");
}
/// An Alt-click and a plain click that land as two releases in one frame must not
/// create a text layer: the first click was modified.
#[test]
fn mixed_modifier_same_frame_double_click_does_not_create() {
let mut s = effectcraft_engine::Session::default();
s.execute("comp.new", json!({"width": 640, "height": 360, "duration": 2})).unwrap();
let mut app = EffectcraftApp::new(s);
let ctx = egui::Context::default();
crate::theme::install(&ctx, &app.tokens, crate::i18n::language(&app));
frame(&mut app, &ctx, 0.0, vec![]);
frame(&mut app, &ctx, 0.1, vec![]);
let e = app.auto.elements.iter().find(|w| w.id == "tools.Type").expect("Type slot");
let at = egui::pos2(e.rect[0] + e.rect[2] / 2.0, e.rect[1] + e.rect[3] / 2.0);
let alt = egui::Modifiers { alt: true, ..Default::default() };
let plain = egui::Modifiers::default();
let pointer = |pos, pressed, modifiers| egui::Event::PointerButton {
pos,
button: egui::PointerButton::Primary,
pressed,
modifiers,
};
app.ui.tool = Tool::Type;
app.ui.slot_tools[10] = Tool::Type;
frame(
&mut app,
&ctx,
1.0,
vec![
egui::Event::PointerMoved(at),
pointer(at, true, alt),
pointer(at, false, alt),
pointer(at, true, plain),
pointer(at, false, plain),
],
);
assert!(app.session.active_comp().unwrap().layers.is_empty(), "Alt then plain in one frame must not create");
assert!(app.session.state.text_edit.is_none());
}
#[test]
fn double_clicking_creation_tools_makes_instances() {