diff --git a/crates/op-host-web/src/canvaskit/mount.rs b/crates/op-host-web/src/canvaskit/mount.rs index d99674ccf..2bb5c43cc 100644 --- a/crates/op-host-web/src/canvaskit/mount.rs +++ b/crates/op-host-web/src/canvaskit/mount.rs @@ -612,6 +612,7 @@ pub(super) async fn mount_ck(canvas_id: String) -> Result<(), JsValue> { "ArrowRight" if is_mod => consumed = b.host.apply_text_edit_line_edge(true), "ArrowLeft" if !is_mod => { consumed = b.host.apply_prompt_center_caret(false, shift) + || b.host.apply_scene_template_caret(false, shift) || b.host.apply_image_panel_caret(false, shift) || b.host.apply_settings_caret(false) || b.host.apply_chat_model_picker_caret(false) @@ -623,6 +624,7 @@ pub(super) async fn mount_ck(canvas_id: String) -> Result<(), JsValue> { } "ArrowRight" if !is_mod => { consumed = b.host.apply_prompt_center_caret(true, shift) + || b.host.apply_scene_template_caret(true, shift) || b.host.apply_image_panel_caret(true, shift) || b.host.apply_settings_caret(true) || b.host.apply_chat_model_picker_caret(true) diff --git a/crates/op-host-web/src/widget_host/keyboard.rs b/crates/op-host-web/src/widget_host/keyboard.rs index ad2edef15..853240039 100644 --- a/crates/op-host-web/src/widget_host/keyboard.rs +++ b/crates/op-host-web/src/widget_host/keyboard.rs @@ -138,6 +138,17 @@ impl WidgetHost { } return true; } + // Native parity (`op-host-native/widget_host/keyboard_delete.rs`). The + // Asset Center owns Backspace while it is open; without this arm the + // key fell through to the canvas and either did nothing visible in the + // search box or deleted the selection behind the overlay. + if let Some(changed) = shared::scene_template_backspace(&mut self.editor_state, self.now_ms) + { + if changed { + self.mark_dirty(); + } + return true; + } if self.editor_state.editor_ui.collab_join_input_active() { let changed = op_editor_ui::widgets::collab_ui::join_address_backspace( &mut self.editor_state.editor_ui, @@ -347,6 +358,16 @@ impl WidgetHost { } return true; } + // Native parity: forward-Delete belongs to the Asset Center's focused + // field while it is open (same gap as Backspace above). + if let Some(changed) = + shared::scene_template_delete_forward(&mut self.editor_state, self.now_ms) + { + if changed { + self.mark_dirty(); + } + return true; + } if self.apply_image_panel_delete() { return true; } diff --git a/crates/op-host-web/src/widget_host/keyboard_edit_ops.rs b/crates/op-host-web/src/widget_host/keyboard_edit_ops.rs index 076029b1f..df5ae1af9 100644 --- a/crates/op-host-web/src/widget_host/keyboard_edit_ops.rs +++ b/crates/op-host-web/src/widget_host/keyboard_edit_ops.rs @@ -315,6 +315,17 @@ impl WidgetHost { false } + /// Left / Right arrow in the Scene Template Center's focused input + /// (native parity, `keyboard_caret.rs`). Without this the arrow keys + /// nudged the canvas behind the open gallery instead of the caret. + pub fn apply_scene_template_caret(&mut self, forward: bool, extend: bool) -> bool { + if shared::scene_template_caret(&mut self.editor_state, forward, extend, self.now_ms) { + self.mark_dirty(); + return true; + } + false + } + /// Cmd/Ctrl+C — copy the selection into the clipboard. pub fn apply_copy(&mut self) -> bool { if self.editor_state.editor_ui.image_panel.search_open diff --git a/crates/op-host-web/src/widget_host/scene_template_ime_tests.rs b/crates/op-host-web/src/widget_host/scene_template_ime_tests.rs index bcb6dba7a..1b250a666 100644 --- a/crates/op-host-web/src/widget_host/scene_template_ime_tests.rs +++ b/crates/op-host-web/src/widget_host/scene_template_ime_tests.rs @@ -47,6 +47,98 @@ fn plain_characters_reach_the_panel() { assert_eq!(search(&host), "rect"); } +/// Backspace had the same native-parity gap as `apply_text` did: the web +/// `apply_backspace` ladder skipped the Asset Center, so the key fell through +/// to the canvas and could not edit the search box (and would delete the +/// selection behind the overlay). +#[test] +fn backspace_edits_the_search_field() { + let mut host = gallery_host(); + for c in "rect".chars() { + host.apply_text(c); + } + assert_eq!(search(&host), "rect"); + + assert!(host.apply_backspace()); + assert_eq!(search(&host), "rec"); + + assert!(host.apply_backspace()); + assert_eq!(search(&host), "re"); +} + +/// Backspace is swallowed even when the search box is empty — the open +/// gallery owns the key and it must never fall through to node deletion. +#[test] +fn backspace_on_an_empty_search_is_swallowed() { + let mut host = gallery_host(); + host.editor_state.set_single_selection(op_editor_core::NodeId::new("n1")); + + assert!(host.apply_backspace()); + assert!(search(&host).is_empty()); + assert!( + !host.editor_state.selection.is_empty(), + "Backspace behind the open gallery must not delete the selection" + ); +} + +/// Forward-Delete edits the focused field (native parity); it must not fall +/// through to the canvas selection either. +#[test] +fn delete_edits_the_search_field() { + let mut host = gallery_host(); + for c in "rect".chars() { + host.apply_text(c); + } + { + let input = &mut host.editor_state.editor_ui.scene_template_center.search; + input.set_caret(0, 0); + } + + assert!(host.apply_delete()); + assert_eq!(search(&host), "ect"); + + assert!(host.apply_delete()); + assert_eq!(search(&host), "ct"); +} + +/// Left / Right arrows move the search caret instead of nudging the canvas +/// behind the open gallery (native parity, `apply_scene_template_caret`). +#[test] +fn arrows_move_the_search_caret() { + let mut host = gallery_host(); + for c in "rect".chars() { + host.apply_text(c); + } + assert_eq!( + host.editor_state + .editor_ui + .scene_template_center + .search + .caret(), + 4 + ); + + assert!(host.apply_scene_template_caret(false, false)); + assert_eq!( + host.editor_state + .editor_ui + .scene_template_center + .search + .caret(), + 3 + ); + + assert!(host.apply_scene_template_caret(true, false)); + assert_eq!( + host.editor_state + .editor_ui + .scene_template_center + .search + .caret(), + 4 + ); +} + /// The gate the tool shortcuts read, so a letter typed into the gallery is /// never also a tool switch. #[test]