diff --git a/crates/ui-egui/src/header.rs b/crates/ui-egui/src/header.rs index c1ab3425f..64ab8b6a3 100644 --- a/crates/ui-egui/src/header.rs +++ b/crates/ui-egui/src/header.rs @@ -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 = 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::(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::(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() {