mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
Text: stop capturing keyboard input after clicking away (#831)
After typing into a text layer, clicking another panel (a solid in the Timeline, say) left the edit open, and the viewer took the keyboard back as soon as nothing had the focus: typed characters went into the text and Space added a space instead of playing. A click in any panel other than the viewers and the panels that format the selected characters (Character, Paragraph, Properties) now ends the edit with text.endEdit, so the text is kept and an empty layer the Type tool started is removed, and the viewer's text focus is surrendered. The editor takes the keyboard back only after a click in the viewer or one of those panels, no longer merely because nothing has the focus. A click elsewhere in the viewer still commits, and with the Type tool still starts new text. Test: clicking_another_panel_stops_typing_into_text, with the Type tool and with an edit started by command: Character > Faux Bold keeps the edit, its selection and typing; clicking the solid's Timeline row ends it, selects the solid, typed characters leave the text unchanged and Space plays; an empty Type-tool layer is removed. Fails on main.
This commit is contained in:
1 parent
67434e9038
commit
1b7ab07822
3 files changed
+84
-3
No files matched your search
@@ -264,6 +264,7 @@ impl EffectcraftApp {
|
||||
}
|
||||
DockAction::Focus(p) => {
|
||||
self.ui.focused = p;
|
||||
panels::viewer_text::panel_clicked(self, &ctx, p);
|
||||
// A click in (or on the tab of) another Composition viewer makes it active.
|
||||
if let Some(id) = panels::viewers::id_of(p) {
|
||||
panels::viewers::activate(self, &ctx, id);
|
||||
|
||||
@@ -26,6 +26,7 @@ use serde_json::{Value, json};
|
||||
|
||||
use super::viewer::ViewerMap;
|
||||
use crate::EffectcraftApp;
|
||||
use crate::dock::PanelKind;
|
||||
use crate::state::Tool;
|
||||
|
||||
/// Pointer interaction owned by text editing (kept across frames while the button is down).
|
||||
@@ -135,6 +136,21 @@ fn exec(app: &mut EffectcraftApp, id: &str, p: Value) -> Option<Value> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The panels a text edit stays open across: the viewers, and Character, Paragraph and
|
||||
/// Properties, which format the selected characters.
|
||||
fn keeps_text_edit(p: PanelKind) -> bool {
|
||||
super::viewers::id_of(p).is_some() || matches!(p, PanelKind::Character | PanelKind::Paragraph | PanelKind::Properties)
|
||||
}
|
||||
|
||||
/// A click in panel `p`. Any other panel (the Timeline, the Project panel, …) ends the edit, as a
|
||||
/// click outside the text does, and gives the keyboard back to the app (#831).
|
||||
pub fn panel_clicked(app: &mut EffectcraftApp, ctx: &egui::Context, p: PanelKind) {
|
||||
if app.session.state.text_edit.is_some() && !keeps_text_edit(p) {
|
||||
exec(app, "text.endEdit", json!({}));
|
||||
ctx.memory_mut(|m| m.surrender_focus(focus_id()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Typing merge key: consecutive keystrokes fold into one undo step until the caret moves.
|
||||
fn typing_key(ui: &egui::Ui, bump: bool) -> String {
|
||||
let id = egui::Id::new("viewer-text-typing");
|
||||
@@ -332,11 +348,13 @@ pub fn hook(
|
||||
ui.memory_mut(|m| {
|
||||
m.set_focus_lock_filter(focus_id(), egui::EventFilter { tab: true, horizontal_arrows: true, vertical_arrows: true, escape: true });
|
||||
});
|
||||
// A session started elsewhere (menu, agent, Properties) takes the keyboard once.
|
||||
// A session started elsewhere (menu, agent, Properties) takes the keyboard once…
|
||||
let seen = egui::Id::new("viewer-text-seen");
|
||||
let key = app.session.state.text_edit.as_ref().map(|e| e.layer.0);
|
||||
// …and gets it back whenever nothing else holds it (after a panel click, say).
|
||||
if ui.data(|d| d.get_temp::<Option<u64>>(seen)).flatten() != key || ui.memory(|m| m.focused().is_none()) {
|
||||
// …and gets it back when a click in the viewer or a formatting panel left nothing holding
|
||||
// it; not merely because nothing has the focus (#831).
|
||||
let regain = ui.memory(|m| m.focused().is_none()) && keeps_text_edit(app.ui.focused);
|
||||
if ui.data(|d| d.get_temp::<Option<u64>>(seen)).flatten() != key || regain {
|
||||
ui.data_mut(|d| d.insert_temp(seen, key));
|
||||
ui.memory_mut(|m| m.request_focus(focus_id()));
|
||||
}
|
||||
|
||||
@@ -160,6 +160,68 @@ fn type_tool_click_type_edit_and_commit() {
|
||||
assert!(h.state().session.state.text_edit.is_none());
|
||||
}
|
||||
|
||||
/// #831: clicking another panel (a solid's row in the Timeline) ends text editing and gives the
|
||||
/// keyboard back, whether the edit began with the Type tool (which stays selected) or by command:
|
||||
/// typed characters change no text and Space plays. The Character panel keeps the edit, and a
|
||||
/// text layer clicked into being and left empty goes away.
|
||||
#[test]
|
||||
fn clicking_another_panel_stops_typing_into_text() {
|
||||
for type_tool in [false, true] {
|
||||
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 title =
|
||||
s.execute("layer.newText", json!({"text": "Title", "size": 80, "position": [100, 120], "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);
|
||||
let layers = |h: &Harness<'_, EffectcraftApp>| h.state().session.active_comp().unwrap().layers.len();
|
||||
// (Its row moves down as text layers are added.)
|
||||
let row = |h: &Harness<'_, EffectcraftApp>| rect(h, &format!("timeline.layer.{solid}.row")).center();
|
||||
if type_tool {
|
||||
h.state_mut().ui.tool = effectcraft_ui_egui::state::Tool::Type;
|
||||
h.run_steps(1);
|
||||
let at = rect(&h, "viewer.comp").center();
|
||||
click(&mut h, at, Modifiers::NONE);
|
||||
assert_eq!(layers(&h), 3, "the Type tool started a text layer");
|
||||
let at_row = row(&h);
|
||||
click(&mut h, at_row, Modifiers::NONE);
|
||||
assert_eq!(layers(&h), 2, "left empty, it is removed");
|
||||
click(&mut h, at, Modifiers::NONE);
|
||||
} else {
|
||||
h.state_mut().session.execute("text.edit", json!({"layer": title})).unwrap();
|
||||
h.run_steps(2);
|
||||
}
|
||||
type_text(&mut h, "Hello");
|
||||
// The Character panel formats the selection and keeps the edit: typing carries on.
|
||||
key(&mut h, Key::ArrowLeft, Modifiers::SHIFT);
|
||||
let range = edited(&h);
|
||||
h.state_mut().show_panel(PanelKind::Character);
|
||||
h.run_steps(3);
|
||||
let bold = rect(&h, "character.fauxBold").center();
|
||||
click(&mut h, bold, Modifiers::NONE);
|
||||
assert_eq!(edited(&h), range, "type_tool {type_tool}: Character keeps the edit and its selection");
|
||||
key(&mut h, Key::End, Modifiers::NONE);
|
||||
type_text(&mut h, "!");
|
||||
let (lid, text, ..) = edited(&h);
|
||||
assert!(text.ends_with("Hello!"), "typing goes into the text: {text:?}");
|
||||
let at_row = row(&h);
|
||||
click(&mut h, at_row, Modifiers::NONE);
|
||||
assert!(h.state().session.state.text_edit.is_none(), "type_tool {type_tool}: clicking the Timeline ends the edit");
|
||||
assert_eq!(h.state().session.state.selected_layers, vec![LayerId(solid)]);
|
||||
assert!(!h.ctx.memory(|m| m.focused().is_some()), "the keyboard is the app's again");
|
||||
type_text(&mut h, "abc");
|
||||
assert!(!h.state().playback.playing);
|
||||
h.input_mut().events.push(Event::Key { key: Key::Space, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE });
|
||||
h.input_mut().events.push(Event::Text(" ".into()));
|
||||
h.input_mut().events.push(Event::Key { key: Key::Space, physical_key: None, pressed: false, repeat: false, modifiers: Modifiers::NONE });
|
||||
h.run_steps(2);
|
||||
assert_eq!(layer_doc(&h.state().session, LayerId(lid)).unwrap().text, text, "type_tool {type_tool}: the text is unchanged");
|
||||
assert!(h.state().playback.playing, "type_tool {type_tool}: Space plays");
|
||||
assert_eq!(type_tool, matches!(h.state().ui.tool, effectcraft_ui_egui::state::Tool::Type), "the tool is kept");
|
||||
}
|
||||
}
|
||||
|
||||
#[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());
|
||||
|
||||
Reference in new issue
Block a user