diff --git a/crates/ui-egui/src/dock.rs b/crates/ui-egui/src/dock.rs index 3e802487..d05a3a1f 100644 --- a/crates/ui-egui/src/dock.rs +++ b/crates/ui-egui/src/dock.rs @@ -1305,10 +1305,13 @@ pub fn draw_group_chrome(ui: &mut egui::Ui, g: &Group, t: &Tokens, reg: &mut cra } } } - // clicking anywhere in the panel focuses it + // clicking anywhere in the panel focuses it; a press on another of its tabs waits for that + // tab's click, which focuses the panel actually chosen (a text edit stays open in Character) + let at = ui.input(|i| i.pointer.interact_pos()); if let Some(p) = active_panel && ui.rect_contains_pointer(g.rect) && ui.input(|i| i.pointer.any_pressed()) + && !out.tabs.iter().any(|(q, r)| *q != p && at.is_some_and(|a| r.contains(a))) { out.actions.push(DockAction::Focus(p)); } diff --git a/crates/ui-egui/src/panels/viewer_text.rs b/crates/ui-egui/src/panels/viewer_text.rs index 0dd95c68..f4604f2e 100644 --- a/crates/ui-egui/src/panels/viewer_text.rs +++ b/crates/ui-egui/src/panels/viewer_text.rs @@ -359,7 +359,7 @@ pub fn hook( ui.memory_mut(|m| m.request_focus(focus_id())); } if ui.memory(|m| m.has_focus(focus_id())) { - keyboard(app, ui); + keyboard(app, ui, resp.rect); } } else { ui.data_mut(|d| d.insert_temp::>(egui::Id::new("viewer-text-seen"), None)); @@ -377,12 +377,16 @@ pub fn hook( owns } -fn keyboard(app: &mut EffectcraftApp, ui: &mut egui::Ui) { +/// This frame's keys, in order, into the edited text, up to a press outside the `viewer`: the +/// panel it lands in decides (after this frame's panels) whether the edit ends, so the keys after +/// it in this frame are neither typed nor run as shortcuts (#831). +fn keyboard(app: &mut EffectcraftApp, ui: &mut egui::Ui, viewer: Rect) { let events = ui.input(|i| i.events.clone()); let ctx = ui.ctx().clone(); for ev in events { let Some(sel) = app.session.state.text_edit.clone() else { return }; match ev { + egui::Event::PointerButton { pos, pressed: true, .. } if !viewer.contains(pos) => return, egui::Event::Text(t) if !t.chars().any(char::is_control) => { let k = typing_key(ui, false); exec(app, "text.insert", json!({"text": t, "merge": k})); diff --git a/crates/ui-egui/tests/ui_text_edit.rs b/crates/ui-egui/tests/ui_text_edit.rs index 4cd9ec99..08597882 100644 --- a/crates/ui-egui/tests/ui_text_edit.rs +++ b/crates/ui-egui/tests/ui_text_edit.rs @@ -222,6 +222,75 @@ fn clicking_another_panel_stops_typing_into_text() { } } +/// #831: opening a formatting panel's tab behind Effects & Presets in its group (Properties, in +/// Undocked Panels) keeps the edit and its selected characters: the press on the tab used to +/// focus the shown panel and end the edit, after which Faux Bold styled the whole layer. +#[test] +fn opening_a_formatting_tab_keeps_the_text_selection() { + let mut s = Session::default(); + s.execute("comp.new", json!({"name": "Type", "width": 640, "height": 360, "duration": 2})).unwrap(); + let lid = + s.execute("layer.newText", json!({"text": "Hello world", "size": 80, "position": [60, 200], "justify": "left"})).unwrap()["layer"].as_u64().unwrap(); + let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).with_step_dt(1.0 / 60.0).build_eframe(move |_| EffectcraftApp::new(s)); + h.run_steps(3); + let ctx = h.ctx.clone(); + // Effects & Presets shown, Properties, Character and Paragraph behind it. + effectcraft_ui_egui::menus::invoke(h.state_mut(), &ctx, "window.workspace", json!({"name": "Undocked Panels"})).unwrap(); + h.state_mut().session.execute("text.edit", json!({"layer": lid})).unwrap(); + h.state_mut().session.execute("text.setSelection", json!({"anchor": 0, "caret": 5})).unwrap(); + h.run_steps(3); + assert!(h.state().auto.find("properties.text.style").is_none(), "Properties is behind Effects & Presets"); + let tab = rect(&h, "panel.tab.Properties").center(); + click(&mut h, tab, Modifiers::NONE); + assert!(h.state().auto.find("properties.text.style").is_some(), "the tab opened Properties"); + assert_eq!(edited(&h), (lid, "Hello world".into(), 0, 5), "the edit and its selection are kept"); + h.state_mut().show_panel(PanelKind::Character); + h.run_steps(3); + let bold = rect(&h, "character.fauxBold").center(); + click(&mut h, bold, Modifiers::NONE); + let doc = layer_doc(&h.state().session, LayerId(lid)).unwrap(); + assert!(doc.style_at(1).faux_bold && !doc.style_at(7).faux_bold, "only \"Hello\" is bold"); +} + +/// #831: a click away and keys in one frame's input. The text gets the keys typed before the +/// click and none after it; those later keys don't run as shortcuts either (Delete would remove a +/// layer). From the next frame the keys are the app's: Space plays. +#[test] +fn keys_after_a_click_away_in_the_same_frame_skip_the_text() { + let mut s = Session::default(); + s.execute("comp.new", json!({"name": "Type", "width": 640, "height": 360, "duration": 2})).unwrap(); + let solid = s.execute("layer.newSolid", json!({"name": "Plate", "color": "#406080"})).unwrap()["layer"].as_u64().unwrap(); + let lid = s.execute("layer.newText", json!({"text": "Hello", "size": 80, "position": [60, 200], "justify": "left"})).unwrap()["layer"].as_u64().unwrap(); + let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).with_step_dt(1.0 / 60.0).build_eframe(move |_| EffectcraftApp::new(s)); + h.state_mut().show_panel(PanelKind::Composition); + h.run_steps(3); + h.state_mut().session.execute("text.edit", json!({"layer": lid, "caret": 5})).unwrap(); + h.run_steps(2); + let row = rect(&h, &format!("timeline.layer.{solid}.row")).center(); + let key = |key: Key, pressed: bool| Event::Key { key, physical_key: None, pressed, repeat: false, modifiers: Modifiers::NONE }; + h.input_mut().events.extend([ + Event::Text("A".into()), + Event::PointerMoved(row), + Event::PointerButton { pos: row, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE }, + Event::PointerButton { pos: row, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE }, + Event::Text("x".into()), + key(Key::Space, true), + Event::Text(" ".into()), + key(Key::Space, false), + key(Key::Delete, true), + key(Key::Delete, false), + ]); + h.run_steps(3); + let comp = |h: &Harness<'_, EffectcraftApp>| h.state().session.active_comp().unwrap().layers.len(); + assert_eq!(layer_doc(&h.state().session, LayerId(lid)).unwrap().text, "HelloA", "only the key before the click"); + assert!(h.state().session.state.text_edit.is_none(), "the click ended the edit"); + assert_eq!((comp(&h), h.state().playback.playing), (2, false), "the keys after it ran no shortcut"); + h.input_mut().events.extend([key(Key::Space, true), Event::Text(" ".into()), key(Key::Space, false)]); + h.run_steps(2); + assert!(h.state().playback.playing, "Space plays"); + assert_eq!(layer_doc(&h.state().session, LayerId(lid)).unwrap().text, "HelloA"); +} + #[test] fn switching_from_type_to_selection_commits_and_moves_text_layer() { let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).with_step_dt(1.0 / 60.0).build_eframe(|_| app());