diff --git a/crates/ui-egui/src/canvas/tests.rs b/crates/ui-egui/src/canvas/tests.rs index c74320ce4..cf8d3635b 100644 --- a/crates/ui-egui/src/canvas/tests.rs +++ b/crates/ui-egui/src/canvas/tests.rs @@ -6234,6 +6234,141 @@ fn ime_edge_orders_never_eat_committed_text_or_lose_the_composition() { assert_eq!(plain(&app, id), "雅がくらくか"); } +/// Starts editing new point type with the Type tool; returns its id. +fn start_typing(app: &mut VectorcraftApp, ctx: &egui::Context) -> u64 { + app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap(); + typing_frame(app, ctx, vec![]); + app.select_tool("type"); + let xf = Xf::new(app.canvas_rect.unwrap(), app.view().unwrap()); + let at = xf.to_screen(Point::new(100.0, 100.0)); + let click = + |p: Pos2, pressed| egui::Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }; + typing_frame(app, ctx, vec![egui::Event::PointerMoved(at), click(at, true)]); + typing_frame(app, ctx, vec![click(at, false)]); + assert!(app.session.tool_wants_text()); + app.session.active().unwrap().selection.objects[0].0 +} + +/// Returns a key press and its release. +fn tap(key: egui::Key) -> [egui::Event; 2] { + [true, false].map(|pressed| egui::Event::Key { key, physical_key: None, pressed, repeat: false, modifiers: Default::default() }) +} + +/// Arrows, Home, End, Tab, Enter and clipboard events go to the Type tool in order with the text +/// of the same frame, and each Enter types a line break. +#[test] +fn the_type_tool_takes_text_and_editing_keys_in_the_order_they_came() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.prefs.placeholder_text = false; + let ctx = egui::Context::default(); + let id = start_typing(&mut app, &ctx); + let text = |t: &str| egui::Event::Text(t.into()); + typing_frame(&mut app, &ctx, vec![text("ax")]); + // ← moves the caret before the text that follows it goes in. + typing_frame(&mut app, &ctx, [tap(egui::Key::ArrowLeft).to_vec(), vec![text("b")]].concat()); + assert_eq!(plain(&app, id), "abx"); + // Home and End move the caret before the text that follows each goes in. + typing_frame(&mut app, &ctx, [tap(egui::Key::Home).to_vec(), vec![text("0")], tap(egui::Key::End).to_vec(), vec![text("9")]].concat()); + assert_eq!(plain(&app, id), "0abx9"); + // Tab and a paste go in between the texts around them. + let paste = egui::Event::Paste("p".into()); + typing_frame(&mut app, &ctx, [vec![text("t")], tap(egui::Key::Tab).to_vec(), vec![text("u"), paste, text("q")]].concat()); + assert_eq!(plain(&app, id), "0abx9t\tupq"); + // Enter types its line break between the texts around it, once for each press. + typing_frame(&mut app, &ctx, [vec![text("1")], tap(egui::Key::Enter).to_vec(), tap(egui::Key::Enter).to_vec(), vec![text("2")]].concat()); + assert_eq!(plain(&app, id), "0abx9t\tupq1\n\n2"); + // A key that comes before a composition starts in the same frame still moves the caret. + typing_frame(&mut app, &ctx, [tap(egui::Key::Home).to_vec(), vec![preedit("k", 1)]].concat()); + typing_frame(&mut app, &ctx, vec![preedit("", 0), egui::Event::Ime(egui::ImeEvent::Commit("k".into()))]); + assert_eq!(plain(&app, id), "0abx9t\tupq1\n\nk2"); +} + +/// Enter keeps each key event's modifiers while sharing a frame with typed text. +#[test] +fn shift_enter_types_a_soft_return_in_order_with_plain_enter() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.prefs.placeholder_text = false; + let ctx = egui::Context::default(); + let id = start_typing(&mut app, &ctx); + let text = |t: &str| egui::Event::Text(t.into()); + let enter = + |modifiers| [true, false].map(|pressed| egui::Event::Key { key: egui::Key::Enter, physical_key: None, pressed, repeat: false, modifiers }); + typing_frame( + &mut app, + &ctx, + [vec![text("a")], enter(egui::Modifiers::SHIFT).to_vec(), vec![text("b")], enter(egui::Modifiers::NONE).to_vec(), vec![text("c")]].concat(), + ); + assert_eq!(plain(&app, id), "a\u{2028}b\nc"); +} + +/// Within one frame, Home between two texts, Tab inside a line, and Backspace after a paste each +/// act on the text as the events before them left it. +#[test] +fn home_tab_and_backspace_act_on_the_text_before_them_in_the_frame() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.prefs.placeholder_text = false; + let ctx = egui::Context::default(); + let id = start_typing(&mut app, &ctx); + let text = |t: &str| egui::Event::Text(t.into()); + typing_frame(&mut app, &ctx, vec![text("abc")]); + typing_frame(&mut app, &ctx, [vec![text("d")], tap(egui::Key::Home).to_vec(), vec![text("0")]].concat()); + assert_eq!(plain(&app, id), "0abcd"); + typing_frame(&mut app, &ctx, [tap(egui::Key::ArrowRight).to_vec(), tap(egui::Key::Tab).to_vec(), vec![text("t")]].concat()); + assert_eq!(plain(&app, id), "0a\ttbcd"); + typing_frame(&mut app, &ctx, [tap(egui::Key::End).to_vec(), vec![egui::Event::Paste("pq".into())], tap(egui::Key::Backspace).to_vec()].concat()); + assert_eq!(plain(&app, id), "0a\ttbcdp"); +} + +/// In a frame that began composing, an editing key after the IME's commit applies before the text +/// that follows it; a key before the commit and an Enter after it stay the IME's. +#[test] +fn editing_keys_after_an_ime_commit_apply_in_the_same_frame() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.prefs.placeholder_text = false; + let ctx = egui::Context::default(); + let id = start_typing(&mut app, &ctx); + let text = |t: &str| egui::Event::Text(t.into()); + let commit = |t: &str| egui::Event::Ime(egui::ImeEvent::Commit(t.into())); + typing_frame(&mut app, &ctx, vec![text("ab")]); + typing_frame(&mut app, &ctx, vec![preedit("漢", 1)]); + typing_frame(&mut app, &ctx, [vec![preedit("", 0), commit("漢")], tap(egui::Key::ArrowLeft).to_vec(), vec![text("x")]].concat()); + assert_eq!(plain(&app, id), "abx漢"); + typing_frame(&mut app, &ctx, vec![preedit("字", 1)]); + let keys = [tap(egui::Key::ArrowLeft).to_vec(), vec![preedit("", 0), commit("字")], tap(egui::Key::Enter).to_vec()]; + typing_frame(&mut app, &ctx, keys.concat()); + assert_eq!(plain(&app, id), "abx字漢"); +} + +/// Cmd/Ctrl+Enter bound to a command runs the command and types no line break, also when a plain +/// Enter comes in the same frame. Unbound, it types a line break as Enter does. +#[test] +fn a_command_on_cmd_enter_runs_without_typing_a_line_break() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.prefs.placeholder_text = false; + let ctx = egui::Context::default(); + let id = start_typing(&mut app, &ctx); + let cmd_enter = [true, false].map(|pressed| egui::Event::Key { + key: egui::Key::Enter, + physical_key: None, + pressed, + repeat: false, + modifiers: egui::Modifiers::COMMAND, + }); + typing_frame(&mut app, &ctx, vec![egui::Event::Text("a".into())]); + typing_frame(&mut app, &ctx, cmd_enter.to_vec()); + assert_eq!(plain(&app, id), "a\n", "nothing is bound to Cmd+Enter"); + crate::keymap::set_override(&mut app.ui.shortcut_overrides, "view.grid", "Cmd+Enter"); + crate::shortcut_editor::sync(&app.ui); + let grid = app.ui.view.grid; + typing_frame(&mut app, &ctx, cmd_enter.to_vec()); + let ran = (plain(&app, id), app.ui.view.grid); + typing_frame(&mut app, &ctx, [tap(egui::Key::Enter).to_vec(), cmd_enter.to_vec()].concat()); + app.ui.shortcut_overrides.clear(); + crate::shortcut_editor::sync(&app.ui); + assert_eq!((ran.0.as_str(), ran.1), ("a\n", !grid), "Cmd+Enter runs Show Grid only"); + assert_eq!((plain(&app, id).as_str(), app.ui.view.grid), ("a\n\n", grid), "Enter types a line break and Cmd+Enter runs Show Grid"); +} + /// Reset to a blank document, select the Type tool and click document point (100, 100), returning /// the screen position used by IME fixtures. fn type_at(app: &mut VectorcraftApp, ctx: &egui::Context) -> egui::Pos2 { diff --git a/crates/ui-egui/src/panels/character.rs b/crates/ui-egui/src/panels/character.rs index 83d9e8434..7e68824c1 100644 --- a/crates/ui-egui/src/panels/character.rs +++ b/crates/ui-egui/src/panels/character.rs @@ -651,7 +651,7 @@ pub(crate) fn intercept_text_command(app: &mut VectorcraftApp, id: &str) -> Opti Some(Ok(Value::Null)) } -enum TextInput { +pub(crate) enum TextInput { Key(ToolKey, Mods), Copy, Cut, @@ -659,83 +659,87 @@ enum TextInput { Face(Face), } -/// Route editing keys (with modifiers), clipboard events and Cmd+C/X/V to the Type tool (Cmd+A is -/// left to the Select All shortcut, which selects the text being edited). -pub(crate) fn route_type_input(app: &mut VectorcraftApp, ctx: &egui::Context) { +/// Takes an editing key (with modifiers), a clipboard event or Cmd+C/X/V for the Type tool into +/// `todo` (Cmd+A is left to the Select All shortcut, which selects the text being edited). Returns +/// false for an event the tool takes, which is then removed from the input. +pub(crate) fn type_input(e: &egui::Event, todo: &mut Vec) -> bool { + match e { + egui::Event::Copy => { + todo.push(TextInput::Copy); + false + } + egui::Event::Cut => { + todo.push(TextInput::Cut); + false + } + egui::Event::Paste(s) => { + todo.push(TextInput::Paste(Some(s.clone()))); + false + } + egui::Event::Key { key, pressed, modifiers: m, .. } => { + let mods = Mods { shift: m.shift, alt: m.alt, cmd: m.command, ctrl: m.ctrl && !m.command, space: false }; + let tk = match key { + Key::ArrowLeft => Some(ToolKey::Left), + Key::ArrowRight => Some(ToolKey::Right), + Key::ArrowUp => Some(ToolKey::Up), + Key::ArrowDown => Some(ToolKey::Down), + Key::Home => Some(ToolKey::Home), + Key::End => Some(ToolKey::End), + Key::Backspace => Some(ToolKey::Backspace), + Key::Delete => Some(ToolKey::Delete), + Key::Tab if !m.command => Some(ToolKey::Tab), + _ => None, + }; + if let Some(tk) = tk { + if *pressed { + todo.push(TextInput::Key(tk, mods)); + } + return false; + } + // Type › Bold and Italic, as page layout apps have them while text is edited. + if m.command && m.shift && !m.alt && matches!(key, Key::B | Key::I) { + if *pressed { + todo.push(TextInput::Face(if *key == Key::B { Face::Bold } else { Face::Italic })); + } + return false; + } + if m.command && !m.shift && !m.alt && matches!(key, Key::C | Key::X | Key::V) { + if *pressed { + todo.push(match key { + Key::C => TextInput::Copy, + Key::X => TextInput::Cut, + _ => TextInput::Paste(None), + }); + } + return false; + } + true + } + _ => true, + } +} + +/// Keeps `ctx` for the Type tool's clipboard, which Edit › Copy and Cut also write to. +pub(crate) fn keep_ctx(ctx: &egui::Context) { CTX.get_or_init(|| ctx.clone()); - let mut todo = vec![]; - ctx.input_mut(|i| { - i.events.retain(|e| match e { - egui::Event::Copy => { - todo.push(TextInput::Copy); - false +} + +/// Applies an editing key or clipboard event taken by [`type_input`] to the Type tool. +pub(crate) fn apply_type_input(app: &mut VectorcraftApp, t: TextInput) { + match t { + TextInput::Key(k, m) => text_key(app, k, m), + TextInput::Copy => { + text_copy(app); + } + TextInput::Cut => { + if text_copy(app) { + text_key(app, ToolKey::Delete, Mods::default()); } - egui::Event::Cut => { - todo.push(TextInput::Cut); - false - } - egui::Event::Paste(s) => { - todo.push(TextInput::Paste(Some(s.clone()))); - false - } - egui::Event::Key { key, pressed, modifiers: m, .. } => { - let mods = Mods { shift: m.shift, alt: m.alt, cmd: m.command, ctrl: m.ctrl && !m.command, space: false }; - let tk = match key { - Key::ArrowLeft => Some(ToolKey::Left), - Key::ArrowRight => Some(ToolKey::Right), - Key::ArrowUp => Some(ToolKey::Up), - Key::ArrowDown => Some(ToolKey::Down), - Key::Home => Some(ToolKey::Home), - Key::End => Some(ToolKey::End), - Key::Backspace => Some(ToolKey::Backspace), - Key::Delete => Some(ToolKey::Delete), - Key::Tab if !m.command => Some(ToolKey::Tab), - _ => None, - }; - if let Some(tk) = tk { - if *pressed { - todo.push(TextInput::Key(tk, mods)); - } - return false; - } - // Type › Bold and Italic, as page layout apps have them while text is edited. - if m.command && m.shift && !m.alt && matches!(key, Key::B | Key::I) { - if *pressed { - todo.push(TextInput::Face(if *key == Key::B { Face::Bold } else { Face::Italic })); - } - return false; - } - if m.command && !m.shift && !m.alt && matches!(key, Key::C | Key::X | Key::V) { - if *pressed { - todo.push(match key { - Key::C => TextInput::Copy, - Key::X => TextInput::Cut, - _ => TextInput::Paste(None), - }); - } - return false; - } - true - } - _ => true, - }) - }); - for t in todo { - match t { - TextInput::Key(k, m) => text_key(app, k, m), - TextInput::Copy => { - text_copy(app); - } - TextInput::Cut => { - if text_copy(app) { - text_key(app, ToolKey::Delete, Mods::default()); - } - } - TextInput::Paste(s) => text_paste(app, s, false), - TextInput::Face(f) => { - if let Err(e) = toggle_face(app, f) { - app.status(e); - } + } + TextInput::Paste(s) => text_paste(app, s, false), + TextInput::Face(f) => { + if let Err(e) = toggle_face(app, f) { + app.status(e); } } } diff --git a/crates/ui-egui/src/shortcuts.rs b/crates/ui-egui/src/shortcuts.rs index c2e549740..1232aba36 100644 --- a/crates/ui-egui/src/shortcuts.rs +++ b/crates/ui-egui/src/shortcuts.rs @@ -139,13 +139,15 @@ fn runs_a_command(i: &egui::InputState, key: Key, cmd_only: bool) -> bool { _ => None, }) .collect(); - let shortcuts = all_shortcuts(); - let bound = |m: &Modifiers| { - shortcuts - .iter() - .any(|(sc, ..)| sc.logical_key == key && (sc.modifiers.command || (!cmd_only && sc.modifiers.any())) && m.matches_logically(sc.modifiers)) - }; - !presses.is_empty() && presses.iter().all(bound) + !presses.is_empty() && presses.iter().all(|m| chord_runs_a_command(key, *m, cmd_only)) +} + +/// Is a command shortcut bound to `key` held with `m`, matched logically? +/// `cmd_only`: only chords with Cmd count. +fn chord_runs_a_command(key: Key, m: Modifiers, cmd_only: bool) -> bool { + all_shortcuts() + .iter() + .any(|(sc, ..)| sc.logical_key == key && (sc.modifiers.command || (!cmd_only && sc.modifiers.any())) && m.matches_logically(sc.modifiers)) } /// Whether a key press runs a shortcut of the keymap in effect, the user's own included. The web @@ -233,7 +235,10 @@ pub fn handle(app: &mut VectorcraftApp, ctx: &egui::Context) { let composing = app.session.tool_composing(); // Tool keys first (Enter/Escape end paths; arrows change polygon sides while dragging). let busy = app.session.tool_busy(); - for (k, tk) in [(Key::Enter, ToolKey::Enter), (Key::Escape, ToolKey::Escape)] { + // Enter in the Type tool types a line break, so it goes to the tool with the frame's text, + // below: the loop then skips Enter, the first key. + let enter_types = app.session.tool_wants_text(); + for (k, tk) in [(Key::Enter, ToolKey::Enter), (Key::Escape, ToolKey::Escape)].into_iter().skip(usize::from(enter_types)) { if !typing && !composing && ctx.input(|i| i.key_pressed(k)) { // A chord a command is bound to (Cmd+Enter) runs only that command, in the shortcuts // below, also when the tool claims the key. While the Type tool edits text only Cmd @@ -507,27 +512,11 @@ fn type_tool_input(app: &mut VectorcraftApp, ctx: &egui::Context, composing: boo ctx.input_mut(|i| i.events.retain(|e| !matches!(e, egui::Event::Key { .. } | egui::Event::Copy | egui::Event::Cut | egui::Event::Paste(_)))); return; } - // A deferred pointer/navigation notice has not established its destination yet. Its - // clipboard suffix must not cut or paste at the old caret; only the notice's own actual - // handler (kept in the raw vector) may run. - if crate::ime::deferred(app, ctx) { - ctx.input_mut(|input| { - input.events.retain(|event| match event { - egui::Event::Cut | egui::Event::Paste(_) => false, - egui::Event::Key { key: egui::Key::X | egui::Key::V, pressed: true, modifiers, .. } - if modifiers.command && !modifiers.shift && !modifiers.alt => - { - false - } - _ => true, - }) - }); - } crate::panels::actions::play_function_key(app, ctx); - // Editing keys with modifiers and the clipboard (Cmd+A is Select All's, below: the text). - crate::panels::character::route_type_input(app, ctx); - // Enter was already delivered above as ToolKey::Enter (newline). Only chords with the - // command key run commands: the rest type or edit. + // Cmd+A is Select All's, below: the text. Enter went to the tool in `type_text`, unless a + // command is bound to its chord, and so did the editing keys and the clipboard events (a + // cut or paste after a deferred pointer or navigation notice is dropped there). Only chords + // with the command key run commands: the rest type or edit. let hits = dispatch(app, ctx, |hit| hit.trigger.chord.mods.cmd); for c in hits { run_hit(app, ctx, &c); diff --git a/crates/ui-egui/src/shortcuts/tests.rs b/crates/ui-egui/src/shortcuts/tests.rs index efe991749..80aa03365 100644 --- a/crates/ui-egui/src/shortcuts/tests.rs +++ b/crates/ui-egui/src/shortcuts/tests.rs @@ -1967,7 +1967,7 @@ fn parses() { /// An IME replacement on the Type tool arrives as `DeleteSurroundingBytes` + `Commit` events /// and is taken once, in order: the marked text is dropped, the committed text replaces it, and -/// the events never reach `route_type_input` as typed characters. +/// the events are never typed as characters. #[test] fn ime_delete_surrounding_bytes_edits_the_type_tool_once() { use vectorcraft_tools::{PointerEvent, PointerKind}; diff --git a/crates/ui-egui/src/shortcuts/typing.rs b/crates/ui-egui/src/shortcuts/typing.rs index 4ed15a6b3..cdd8c16f8 100644 --- a/crates/ui-egui/src/shortcuts/typing.rs +++ b/crates/ui-egui/src/shortcuts/typing.rs @@ -1,4 +1,5 @@ -use super::{EngineResult, Key, Mods, SurroundingUnit, ToolKey, UiRequest, VectorcraftApp, ViewInfo}; +use super::{EngineResult, Key, Mods, SurroundingUnit, ToolKey, UiRequest, VectorcraftApp, ViewInfo, chord_runs_a_command}; +use crate::panels::character::TextInput; /// Drop each key press made with Ctrl and Alt held that is followed by the text it typed. Firefox /// on Windows reports AltGr as Ctrl and Alt held, so AltGr+L on a Polish layout arrives as a @@ -51,9 +52,14 @@ enum Seq { InterruptNavigation, InterruptCommand, ResetCompleted(egui::ImeResetId), + /// Enter, which types a line break. + Enter(Mods), + /// An editing key with modifiers, an arrow, Home, End, Tab or a clipboard event. + Edit(TextInput), } -/// Typed text and IME events, in the order they came, to the Type tool. +/// Typed text, IME events, editing keys, Enter and clipboard events, in the order they came, to the +/// Type tool. Escape is handled before them, in [`handle`]. pub(super) fn type_text(app: &mut VectorcraftApp, ctx: &egui::Context) -> bool { let view = app.view_info(); // Switching apps mid-composition needs nothing: the macOS IME keeps its marked text and goes @@ -62,14 +68,18 @@ pub(super) fn type_text(app: &mut VectorcraftApp, ctx: &egui::Context) -> bool { // A frame that began composing keeps its editing keys for the IME: a Backspace that empties the // marked text must not then delete the committed character before it. let was_composing = app.session.tool_composing(); + crate::panels::character::keep_ctx(ctx); // Text, IME events and plain Backspace/Delete are taken together, in the order they arrived, so // a keyboard-hook IME (UniKey/OpenKey) whose tone key sends Backspace(s) and then re-injects // characters in one frame interleaves correctly. Taken as two passes (all text, then the keys) - // the deletes land after the inserts and the diacritics are lost. Each handled event is removed - // so `route_type_input` below does not process the same Backspace/Delete again. + // the deletes land after the inserts and the diacritics are lost. Enter, the other editing keys + // and the clipboard events join them: ← or a paste lands between the texts around it, and each + // Enter types its own line break. Each handled event is removed from the input. let seq = type_sequence(ctx); let viewport = ctx.viewport_id(); let mut old_composition_retired = false; + // Set once the IME commits text in this frame. + let mut committed = false; for e in seq { // Before the UI, a changed composition or guard is settled `pending`: a token bound here // could outlive a route this pass invalidates later (a palette/dialog/Home) and attach to @@ -91,10 +101,19 @@ pub(super) fn type_text(app: &mut VectorcraftApp, ctx: &egui::Context) -> bool { // erases after it belong to the old caret, not to the destination it will establish. if crate::ime::deferred(app, ctx) { match &e { - Seq::Text(_) | Seq::Erase(..) => continue, + // A cut or paste must not change the text at the old caret either. + Seq::Text(_) | Seq::Erase(..) | Seq::Edit(TextInput::Cut | TextInput::Paste(_)) => continue, _ => {} } } + // Editing keys and clipboard events belong to the IME while it composes, and in a frame + // that began composing until the IME commits or the composition is retired. + if matches!(&e, Seq::Edit(_)) && (app.session.tool_composing() || (was_composing && !committed && !old_composition_retired)) { + continue; + } + if let Seq::Commit(t) = &e { + committed |= !t.is_empty() && t != "\n" && t != "\r"; + } let preedit_empty = match &e { Seq::Preedit(text, _) => Some(text.is_empty()), _ => None, @@ -114,9 +133,11 @@ pub(super) fn type_text(app: &mut VectorcraftApp, ctx: &egui::Context) -> bool { old_composition_retired } -/// Take this frame's typed text, IME events and plain Backspace/Delete, in the order they arrived. +/// Take this frame's typed text, IME events, editing keys, Enter and clipboard events, in the order +/// they arrived. fn type_sequence(ctx: &egui::Context) -> Vec { let mut seq = vec![]; + let mut edits = vec![]; ctx.input_mut(|i| { // Move the events out instead of cloning their strings: a keyboard hook that reinjects a // batch keeps the batch in `seq` only, not in both the input list and a second copy. @@ -149,13 +170,20 @@ fn type_sequence(ctx: &egui::Context) -> Vec { // routed as a key or shortcut). egui::Event::MiddleClickPaste { text, .. } => seq.push(Seq::Text(text)), // Only unmodified Backspace/Delete interleave with injected text; word-delete and other - // modified forms stay with `route_type_input`. + // modified forms are taken with the other editing keys below. egui::Event::Key { key: key @ (Key::Backspace | Key::Delete), pressed: true, modifiers: m, .. } if !m.command && !m.alt && !m.ctrl => { let tk = if key == Key::Backspace { ToolKey::Backspace } else { ToolKey::Delete }; seq.push(Seq::Erase(tk, Mods { shift: m.shift, ..Mods::default() })); } + // A command bound to the chord (Cmd+Enter) runs instead of the line break, in the + // shortcuts after this. + egui::Event::Key { key: Key::Enter, pressed, modifiers: m, .. } if !chord_runs_a_command(Key::Enter, m, true) => { + if pressed { + seq.push(Seq::Enter(crate::canvas::mods(m, false))); + } + } e @ egui::Event::PointerButton { button: egui::PointerButton::Primary, pressed, .. } => { primary_down = pressed; if pressed { @@ -169,22 +197,25 @@ fn type_sequence(ctx: &egui::Context) -> Vec { } kept.push(e); } - e @ egui::Event::Key { key, pressed: true, modifiers, .. } - if matches!(key, Key::ArrowLeft | Key::ArrowRight | Key::ArrowUp | Key::ArrowDown | Key::Home | Key::End) - || key == Key::A && modifiers.command => - { - seq.push(Seq::InterruptNavigation); - kept.push(e); + e => { + let interrupt = match &e { + egui::Event::Key { key, pressed: true, modifiers, .. } + if matches!(key, Key::ArrowLeft | Key::ArrowRight | Key::ArrowUp | Key::ArrowDown | Key::Home | Key::End) + || *key == Key::A && modifiers.command => + { + Some(Seq::InterruptNavigation) + } + egui::Event::Key { pressed: true, modifiers, .. } if is_shortcut_chord(*modifiers) => Some(Seq::InterruptCommand), + egui::Event::Cut | egui::Event::Paste(_) => Some(Seq::InterruptCommand), + _ => None, + }; + seq.extend(interrupt); + // The other editing keys and the clipboard events apply in this order too. + if crate::panels::character::type_input(&e, &mut edits) { + kept.push(e); + } + seq.extend(edits.drain(..).map(Seq::Edit)); } - e @ egui::Event::Key { pressed: true, modifiers, .. } if is_shortcut_chord(modifiers) => { - seq.push(Seq::InterruptCommand); - kept.push(e); - } - e @ (egui::Event::Cut | egui::Event::Paste(_)) => { - seq.push(Seq::InterruptCommand); - kept.push(e); - } - other => kept.push(other), } } i.events = kept; @@ -287,8 +318,8 @@ fn apply_type_event( } // While composing (or in a frame that began composing), editing keys belong to the IME. Seq::Erase(tk, m) if !was_composing && !app.session.tool_composing() => { - // Mirror `route_type_input`/`text_key`: surface an erase error in the status bar - // instead of discarding it, and report the successful edit as application-origin. + // Mirror `text_key`: surface an erase error in the status bar instead of discarding it, + // and report the successful edit as application-origin. let target_before = crate::ime::target(&mut app.session, view); let before = crate::ime::stamp(&mut app.session, view); let r = app.session.tool_key(tk, m, view); @@ -312,5 +343,22 @@ fn apply_type_event( } r } + // An Enter that confirms the composition types no line break. + Seq::Enter(_) if was_composing || app.session.tool_composing() => Ok(vec![]), + Seq::Enter(m) => { + let target_before = crate::ime::target(&mut app.session, view); + let before = crate::ime::stamp(&mut app.session, view); + let r = app.session.tool_key(ToolKey::Enter, m, view); + if r.is_ok() { + crate::ime::note_application_edit(app, ctx, view, target_before, before); + } + crate::canvas::apply_requests(app, r); + Ok(vec![]) + } + // `text_key` and `text_paste` report the edit as application-origin. + Seq::Edit(t) => { + crate::panels::character::apply_type_input(app, t); + Ok(vec![]) + } } }