From 14a6ea5c525f8295742b79c1de71cd0008bd8461 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Torr=C3=B3?= Date: Tue, 4 Aug 2026 15:30:26 +0200 Subject: [PATCH] :wrench: Support text style shortcuts (#11002) --- .../main/data/workspace/text/shortcuts.cljs | 3 ++ .../ui/workspace/shapes/text/v3_editor.cljs | 20 ++++++++++--- frontend/src/app/render_wasm/api.cljs | 1 + frontend/src/app/render_wasm/text_editor.cljs | 6 ++++ render-wasm/src/wasm/text_editor.rs | 29 +++++++++++++++++++ 5 files changed, 55 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/main/data/workspace/text/shortcuts.cljs b/frontend/src/app/main/data/workspace/text/shortcuts.cljs index fddfaf357a..9943c809e0 100644 --- a/frontend/src/app/main/data/workspace/text/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/text/shortcuts.cljs @@ -11,6 +11,7 @@ [app.common.types.text :as txt] [app.main.data.shortcuts :as ds] [app.main.data.workspace.texts :as dwt] + [app.main.data.workspace.texts-v3 :as dwt-v3] [app.main.data.workspace.undo :as dwu] [app.main.features :as features] [app.main.fonts :as fonts] @@ -170,6 +171,8 @@ :else props)] (when (and shape props) + (when (features/active-feature? @st/state "text-editor-wasm/v1") + (st/emit! (dwt-v3/v3-update-text-editor-styles (:id shape) props))) (st/emit! (dwt/update-attrs (:id shape) props))))) (defn blend-props diff --git a/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs b/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs index fb65be820e..e626fa2b74 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs @@ -99,6 +99,18 @@ (or (.-isComposing native) (= 229 (.-keyCode event))))) +(defn- input-surface-class + "Class list for the contenteditable capture surface. + + Mousetrap's `stopCallback` drops every keystroke whose target is + contentEditable, so without the `mousetrap` class (as in V1/V2) the text + shortcuts (Ctrl+B, Ctrl+I, …) never reach the dispatcher." + [rotation] + (dm/str "mousetrap " + (cur/get-dynamic "text" rotation) + " " + (stl/css :text-editor-container))) + (mf/defc text-editor* "Contenteditable element positioned over the text shape to capture input events." [{:keys [shape]}] @@ -359,7 +371,9 @@ (let [native-event (dom/event->native-event event) off-pt (dom/get-offset-position native-event)] (mf/set-ref-val! dragging-ref true) - (wasm.api/text-editor-pointer-down off-pt) + (if (.-shiftKey event) + (wasm.api/text-editor-pointer-down-extend off-pt) + (wasm.api/text-editor-pointer-down off-pt)) ;; Repaint the caret over the cached tiles instead of a full render, ;; which flashes at high zoom (see `render-text-editor-overlay!`). (wasm.api/render-text-editor-overlay!)))) @@ -505,7 +519,5 @@ :on-focus on-focus :on-blur on-blur :id "text-editor-wasm-input" - :class (dm/str (cur/get-dynamic "text" (:rotation shape)) - " " - (stl/css :text-editor-container)) + :class (input-surface-class (:rotation shape)) :data-testid "text-editor-container"}]]]])) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index aa86d52701..b8601102ee 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -281,6 +281,7 @@ (def text-editor-set-cursor-from-point text-editor/text-editor-set-cursor-from-point) (def text-editor-toggle-overtype-mode text-editor/text-editor-toggle-overtype-mode) (def text-editor-pointer-down text-editor/text-editor-pointer-down) +(def text-editor-pointer-down-extend text-editor/text-editor-pointer-down-extend) (def text-editor-pointer-move text-editor/text-editor-pointer-move) (def text-editor-pointer-up text-editor/text-editor-pointer-up) (def text-editor-get-current-styles text-editor/text-editor-get-current-styles) diff --git a/frontend/src/app/render_wasm/text_editor.cljs b/frontend/src/app/render_wasm/text_editor.cljs index 1794c1ae22..c2853dad9e 100644 --- a/frontend/src/app/render_wasm/text_editor.cljs +++ b/frontend/src/app/render_wasm/text_editor.cljs @@ -222,6 +222,12 @@ (when (wasm/ready?) (h/call wasm/internal-module "_text_editor_pointer_down" x y))) +(defn text-editor-pointer-down-extend + "Extends the selection up to the pointer instead of collapsing the caret." + [{:keys [x y]}] + (when (wasm/ready?) + (h/call wasm/internal-module "_text_editor_pointer_down_extend" x y))) + (defn text-editor-pointer-move [{:keys [x y]}] (when (wasm/ready?) diff --git a/render-wasm/src/wasm/text_editor.rs b/render-wasm/src/wasm/text_editor.rs index af37164af9..6bb03f9f5e 100644 --- a/render-wasm/src/wasm/text_editor.rs +++ b/render-wasm/src/wasm/text_editor.rs @@ -192,6 +192,35 @@ pub extern "C" fn text_editor_pointer_down(x: f32, y: f32) { }); } +/// Like `text_editor_pointer_down`, but keeps the current anchor and moves the +/// focus to the pointer instead of collapsing the caret there (Shift+click). +#[no_mangle] +pub extern "C" fn text_editor_pointer_down_extend(x: f32, y: f32) { + with_state!(state, { + if !get_text_editor_state().has_focus { + return; + } + let Some(shape_id) = get_text_editor_state().active_shape_id else { + return; + }; + let Some(shape) = state.shapes.get(&shape_id) else { + return; + }; + let Type::Text(text_content) = &shape.shape_type else { + return; + }; + let point = Point::new(x, y); + get_text_editor_state().start_pointer_selection(); + if let Some(position) = text_content.get_caret_position_from_shape_coords(&point) { + get_text_editor_state().extend_selection_from_position(&position); + // The click after pointerup would collapse the caret and drop the + // selection we just extended. + get_text_editor_state().is_click_event_skipped = true; + get_text_editor_state().update_styles(text_content); + } + }); +} + #[no_mangle] pub extern "C" fn text_editor_pointer_move(x: f32, y: f32) { with_state!(state, {