Text: formatting tabs keep the edit; keys after a click away skip it (#831)
Two follow-ups to ending text editing on a click in another panel.
A press anywhere in a panel group focused the group's shown panel, even
a press on another of its tabs. Opening Properties or Character behind
Effects & Presets (as in Undocked Panels) therefore ended the edit on
the press, before the tab's click focused the panel chosen, and later
formatting went to the whole layer instead of the selected characters
(an empty Type-tool layer was even removed). A press on another tab now
waits for that tab's click.
The click away is resolved after the frame's panels, so keys that came
after it in the same frame's input were still typed into the text. The
text keyboard handler now takes the frame's keys in order up to a press
outside the viewer: keys before the click are typed, keys after it are
neither typed nor run as shortcuts, and from the next frame they are
the app's.
Tests: opening_a_formatting_tab_keeps_the_text_selection (a real click
on the Properties tab keeps the edit and its selection; Faux Bold then
styles only "Hello") and keys_after_a_click_away_in_the_same_frame_skip_
the_text ("A", a click on a Timeline row, then "x", Space and Delete in
one frame: only "A" is typed, nothing is played or deleted; Space in the
next frame plays). Both fail on the previous commit.
This commit is contained in:
parent
1b7ab07822
commit
f6d42e2d67
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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::<Option<u64>>(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}));
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
Loading…
Reference in New Issue