From d9073b1828939fbd5d32c5f3614d86f51c000491 Mon Sep 17 00:00:00 2001 From: Aitor Moreno Date: Thu, 12 Mar 2026 14:55:28 +0100 Subject: [PATCH] :tada: Feat apply styles to selection --- .../src/app/main/data/workspace/texts.cljs | 17 +++-- .../ui/workspace/shapes/text/v3_editor.cljs | 16 ++-- .../main/ui/workspace/viewport/actions.cljs | 2 +- frontend/src/app/render_wasm/api.cljs | 31 ++++---- frontend/src/app/render_wasm/text_editor.cljs | 58 ++++++++------ render-wasm/src/render/text_editor.rs | 6 +- render-wasm/src/state/text_editor.rs | 24 ++++-- render-wasm/src/wasm/text_editor.rs | 75 +++++++++++-------- 8 files changed, 133 insertions(+), 96 deletions(-) diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index b1903ec5c5..3f70955e82 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -784,14 +784,15 @@ (when (features/active-feature? state "render-wasm/v1") (rx/concat ;; Apply style to selected spans and sync content - (when (wasm.api/text-editor-is-active?) - (let [span-attrs (select-keys attrs txt/text-node-attrs)] - (when (not (empty? span-attrs)) - (let [result (wasm.api/apply-style-to-selection span-attrs)] - (when result - (rx/of (v2-update-text-shape-content - (:shape-id result) (:content result) - :update-name? true))))))) + (let [has-selection? (wasm.api/text-editor-has-selection?)] + (when has-selection? + (let [span-attrs (select-keys attrs txt/text-node-attrs)] + (when (not (empty? span-attrs)) + (let [result (wasm.api/apply-style-to-selection span-attrs)] + (when result + (rx/of (v2-update-text-shape-content + (:shape-id result) (:content result) + :update-name? true)))))))) ;; Resize (with delay for font-id changes) (cond->> (rx/of (dwwt/resize-wasm-text id)) (contains? attrs :font-id) 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 576854c163..6f9a574e46 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 @@ -105,7 +105,7 @@ on-copy (mf/use-fn (fn [^js event] - (when (text-editor/text-editor-is-active?) + (when (text-editor/text-editor-has-focus?) (dom/prevent-default event) (when (text-editor/text-editor-get-selection) (let [text (text-editor/text-editor-export-selection)] @@ -114,7 +114,7 @@ on-cut (mf/use-fn (fn [^js event] - (when (text-editor/text-editor-is-active?) + (when (text-editor/text-editor-has-focus?) (dom/prevent-default event) (when (text-editor/text-editor-get-selection) (let [text (text-editor/text-editor-export-selection)] @@ -129,7 +129,7 @@ on-key-down (mf/use-fn (fn [^js event] - (when (and (text-editor/text-editor-is-active?) + (when (and (text-editor/text-editor-has-focus?) (not @composing?)) (let [key (.-key event) ctrl? (or (.-ctrlKey event) (.-metaKey event)) @@ -268,13 +268,13 @@ on-focus (mf/use-fn (fn [^js _event] - (wasm.api/text-editor-start shape-id))) + (wasm.api/text-editor-focus shape-id))) on-blur (mf/use-fn (fn [^js _event] (sync-wasm-text-editor-content! {:finalize? true}) - (wasm.api/text-editor-stop))) + (wasm.api/text-editor-blur))) style #js {:pointerEvents "all" "--editor-container-width" (dm/str width "px") @@ -297,11 +297,15 @@ (fn [] (let [timeout-id (atom nil) schedule-blink (fn schedule-blink [] - (when (text-editor/text-editor-is-active?) + (when (text-editor/text-editor-has-focus?) (wasm.api/request-render "cursor-blink")) (reset! timeout-id (js/setTimeout schedule-blink caret-blink-interval-ms)))] (schedule-blink) (fn [] + ;; ESTO ES JUSTO LO QUE NO QUIERO, NO QUIERO QUE SE HAGA + ;; DISPOSE CUANDO SE DESMONTA EL COMPONENTE. + #_(when (text-editor/text-editor-dispose) + (wasm.api/request-render "text-editor-dispose")) (when @timeout-id (js/clearTimeout @timeout-id)))))) diff --git a/frontend/src/app/main/ui/workspace/viewport/actions.cljs b/frontend/src/app/main/ui/workspace/viewport/actions.cljs index 50cd0acec5..e28d1fdd23 100644 --- a/frontend/src/app/main/ui/workspace/viewport/actions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/actions.cljs @@ -292,7 +292,7 @@ (when left-click? (st/emit! (mse/->MouseEvent :up ctrl? shift? alt? meta?)) - (when (wasm.api/text-editor-is-active?) + (when (wasm.api/text-editor-has-focus?) (wasm.api/text-editor-pointer-up (.-x off-pt) (.-y off-pt)))) (when middle-click? diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 82fb16ebaa..1efa35e50f 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -84,14 +84,15 @@ (def clear-canvas-pixels webgl/clear-canvas-pixels) ;; Re-export public text editor functions -(def text-editor-start text-editor/text-editor-start) -(def text-editor-stop text-editor/text-editor-stop) +(def text-editor-focus text-editor/text-editor-focus) +(def text-editor-blur text-editor/text-editor-blur) (def text-editor-set-cursor-from-offset text-editor/text-editor-set-cursor-from-offset) (def text-editor-set-cursor-from-point text-editor/text-editor-set-cursor-from-point) (def text-editor-pointer-down text-editor/text-editor-pointer-down) (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-is-active? text-editor/text-editor-is-active?) +(def text-editor-has-focus? text-editor/text-editor-has-focus?) +(def text-editor-has-selection? text-editor/text-editor-has-selection?) (def text-editor-select-all text-editor/text-editor-select-all) (def text-editor-select-word-boundary text-editor/text-editor-select-word-boundary) (def text-editor-sync-content text-editor/text-editor-sync-content) @@ -125,6 +126,13 @@ :fill "none"} [:& shape-wrapper {:shape shape}]])) +(defn is-text-editor-wasm-enabled + [state] + (let [runtime-features (get state :features-runtime) + enabled-features (get state :features)] + (or (contains? runtime-features "text-editor-wasm/v1") + (contains? enabled-features "text-editor-wasm/v1")))) + (defn get-static-markup [shape] (-> @@ -144,20 +152,17 @@ ;; Update text editor blink (so cursor toggles) using the same timestamp (try (when wasm/context-initialized? - (text-editor/text-editor-update-blink timestamp) ;; Render text editor overlay on top of main canvas (only if feature enabled) ;; Determine if text-editor-wasm feature is active without requiring ;; app.main.features to avoid circular dependency: check runtime and ;; persisted feature sets in the store state. - (let [runtime-features (get @st/state :features-runtime) - enabled-features (get @st/state :features)] - (when (or (contains? runtime-features "text-editor-wasm/v1") - (contains? enabled-features "text-editor-wasm/v1")) - (text-editor/text-editor-render-overlay))) - ;; Poll for editor events; if any event occurs, trigger a re-render - (let [ev (text-editor/text-editor-poll-event)] - (when (and ev (not= ev 0)) - (request-render "text-editor-event")))) + (when (is-text-editor-wasm-enabled @st/state) + (text-editor/text-editor-update-blink timestamp) + (text-editor/text-editor-render-overlay) + ;; Poll for editor events; if any event occurs, trigger a re-render + (let [ev (text-editor/text-editor-poll-event)] + (when (and ev (not= ev 0)) + (request-render "text-editor-event"))))) (catch :default e (js/console.error "text-editor overlay/update failed:" e))) diff --git a/frontend/src/app/render_wasm/text_editor.cljs b/frontend/src/app/render_wasm/text_editor.cljs index e8e540b8f1..b94ff16033 100644 --- a/frontend/src/app/render_wasm/text_editor.cljs +++ b/frontend/src/app/render_wasm/text_editor.cljs @@ -12,16 +12,16 @@ [app.render-wasm.mem :as mem] [app.render-wasm.wasm :as wasm])) -(defn text-editor-start +(defn text-editor-focus [id] (when wasm/context-initialized? (let [buffer (uuid/get-u32 id)] - (when-not (h/call wasm/internal-module "_text_editor_start" + (when-not (h/call wasm/internal-module "_text_editor_focus" (aget buffer 0) (aget buffer 1) (aget buffer 2) (aget buffer 3)) - (throw (js/Error. "TextEditor initialization failed")))))) + (throw (js/Error. "TextEditor focus failed")))))) (defn text-editor-set-cursor-from-offset "Sets caret position from shape relative coordinates" @@ -111,19 +111,29 @@ (when wasm/context-initialized? (h/call wasm/internal-module "_text_editor_select_word_boundary" x y))) -(defn text-editor-stop +(defn text-editor-blur [] (when wasm/context-initialized? - (when-not (h/call wasm/internal-module "_text_editor_stop") - (throw (js/Error. "TextEditor finalization failed"))))) + (when-not (h/call wasm/internal-module "_text_editor_blur") + (throw (js/Error. "TextEditor blur failed"))))) -(defn text-editor-is-active? +(defn text-editor-dispose + [] + (when wasm/context-initialized? + (h/call wasm/internal-module "_text_editor_dispose"))) + +(defn text-editor-has-focus? ([id] (when wasm/context-initialized? - (not (zero? (h/call wasm/internal-module "_text_editor_is_active_with_id" id))))) + (not (zero? (h/call wasm/internal-module "_text_editor_has_focus_with_id" id))))) ([] (when wasm/context-initialized? - (not (zero? (h/call wasm/internal-module "_text_editor_is_active")))))) + (not (zero? (h/call wasm/internal-module "_text_editor_has_focus")))))) + +(defn text-editor-has-selection? + ([] + (when wasm/context-initialized? + (not (zero? (h/call wasm/internal-module "_text_editor_has_selection")))))) (defn text-editor-export-content [] @@ -167,18 +177,20 @@ (defn text-editor-get-selection [] (when wasm/context-initialized? - (let [byte-offset (mem/alloc 16) - u32-offset (mem/->offset-32 byte-offset) - heap (mem/get-heap-u32) - active? (h/call wasm/internal-module "_text_editor_get_selection" byte-offset)] - (try - (when (= active? 1) - {:anchor-para (aget heap u32-offset) - :anchor-offset (aget heap (+ u32-offset 1)) - :focus-para (aget heap (+ u32-offset 2)) - :focus-offset (aget heap (+ u32-offset 3))}) - (finally - (mem/free)))))) + (let [byte-offset (mem/alloc 16) + u32-offset (mem/->offset-32 byte-offset) + heap (mem/get-heap-u32) + has-selection? (h/call wasm/internal-module "_text_editor_get_selection" byte-offset)] + (if has-selection? + (let [result {:anchor-para (aget heap u32-offset) + :anchor-offset (aget heap (+ u32-offset 1)) + :focus-para (aget heap (+ u32-offset 2)) + :focus-offset (aget heap (+ u32-offset 3))}] + (mem/free) + result) + (do + (mem/free) + nil))))) ;; This is used as a intermediate cache between Clojure global state and WASM state. (def ^:private shape-text-contents (atom {})) @@ -229,7 +241,7 @@ shape-id and the fully merged content map ready for v2-update-text-shape-content." [] - (when (and wasm/context-initialized? (text-editor-is-active?)) + (when (and wasm/context-initialized? (text-editor-has-focus?)) (let [shape-id (text-editor-get-active-shape-id) new-texts (text-editor-export-content)] (when (and shape-id new-texts) @@ -301,7 +313,7 @@ (defn apply-style-to-selection [attrs use-shape-fn set-shape-text-content-fn] - (when (and wasm/context-initialized? (text-editor-is-active?)) + (when wasm/context-initialized? (let [shape-id (text-editor-get-active-shape-id) sel (text-editor-get-selection)] (when (and shape-id sel) diff --git a/render-wasm/src/render/text_editor.rs b/render-wasm/src/render/text_editor.rs index beb1c1384b..e356e99685 100644 --- a/render-wasm/src/render/text_editor.rs +++ b/render-wasm/src/render/text_editor.rs @@ -9,10 +9,6 @@ pub fn render_overlay( shape: &Shape, transform: &Matrix, ) { - if !editor_state.is_active { - return; - } - let Type::Text(text_content) = &shape.shape_type else { return; }; @@ -24,7 +20,7 @@ pub fn render_overlay( render_selection(canvas, editor_state, text_content, shape); } - if editor_state.cursor_visible { + if editor_state.has_focus && editor_state.cursor_visible { render_cursor(canvas, editor_state, text_content, shape); } diff --git a/render-wasm/src/state/text_editor.rs b/render-wasm/src/state/text_editor.rs index 65ffbbc19a..464a0a39f5 100644 --- a/render-wasm/src/state/text_editor.rs +++ b/render-wasm/src/state/text_editor.rs @@ -106,7 +106,7 @@ pub struct TextEditorTheme { pub struct TextEditorState { pub theme: TextEditorTheme, pub selection: TextSelection, - pub is_active: bool, + pub has_focus: bool, // This property indicates that we've started // selecting something with the pointer. pub is_pointer_selection_active: bool, @@ -125,7 +125,7 @@ impl TextEditorState { cursor_color: CURSOR_COLOR, }, selection: TextSelection::new(), - is_active: false, + has_focus: false, is_pointer_selection_active: false, active_shape_id: None, cursor_visible: true, @@ -134,8 +134,8 @@ impl TextEditorState { } } - pub fn start(&mut self, shape_id: Uuid) { - self.is_active = true; + pub fn focus(&mut self, shape_id: Uuid) { + self.has_focus = true; self.active_shape_id = Some(shape_id); self.cursor_visible = true; self.last_blink_time = 0.0; @@ -144,8 +144,18 @@ impl TextEditorState { self.pending_events.clear(); } - pub fn stop(&mut self) { - self.is_active = false; + pub fn blur(&mut self) { + self.has_focus = false; + // self.active_shape_id = None; + self.cursor_visible = false; + self.last_blink_time = 0.0; + // self.selection.reset(); + self.is_pointer_selection_active = false; + self.pending_events.clear(); + } + + pub fn dispose(&mut self) { + self.has_focus = false; self.active_shape_id = None; self.cursor_visible = false; self.last_blink_time = 0.0; @@ -284,7 +294,7 @@ impl TextEditorState { } pub fn update_blink(&mut self, timestamp_ms: f64) { - if !self.is_active { + if !self.has_focus { return; } diff --git a/render-wasm/src/wasm/text_editor.rs b/render-wasm/src/wasm/text_editor.rs index 25182c5050..3e09bfaabe 100644 --- a/render-wasm/src/wasm/text_editor.rs +++ b/render-wasm/src/wasm/text_editor.rs @@ -42,7 +42,7 @@ pub extern "C" fn text_editor_apply_theme( } #[no_mangle] -pub extern "C" fn text_editor_start(a: u32, b: u32, c: u32, d: u32) -> bool { +pub extern "C" fn text_editor_focus(a: u32, b: u32, c: u32, d: u32) -> bool { with_state_mut!(state, { let shape_id = uuid_from_u32_quartet(a, b, c, d); @@ -54,35 +54,48 @@ pub extern "C" fn text_editor_start(a: u32, b: u32, c: u32, d: u32) -> bool { return false; } - state.text_editor_state.start(shape_id); + state.text_editor_state.focus(shape_id); true }) } #[no_mangle] -pub extern "C" fn text_editor_stop() -> bool { +pub extern "C" fn text_editor_blur() -> bool { with_state_mut!(state, { - if !state.text_editor_state.is_active { + if !state.text_editor_state.has_focus { return false; } - state.text_editor_state.stop(); + state.text_editor_state.blur(); true }) } #[no_mangle] -pub extern "C" fn text_editor_is_active() -> bool { - with_state!(state, { state.text_editor_state.is_active }) +pub extern "C" fn text_editor_dispose() -> bool { + with_state_mut!(state, { + state.text_editor_state.dispose(); + true + }) } #[no_mangle] -pub extern "C" fn text_editor_is_active_with_id(a: u32, b: u32, c: u32, d: u32) -> bool { +pub extern "C" fn text_editor_has_selection() -> bool { + with_state!(state, { state.text_editor_state.selection.is_selection() }) +} + +#[no_mangle] +pub extern "C" fn text_editor_has_focus() -> bool { + with_state!(state, { state.text_editor_state.has_focus }) +} + +#[no_mangle] +pub extern "C" fn text_editor_has_focus_with_id(a: u32, b: u32, c: u32, d: u32) -> bool { with_state!(state, { let shape_id = uuid_from_u32_quartet(a, b, c, d); let Some(active_shape_id) = state.text_editor_state.active_shape_id else { return false; }; - state.text_editor_state.is_active && active_shape_id == shape_id + state.text_editor_state.has_focus && active_shape_id == shape_id }) } @@ -104,7 +117,7 @@ pub extern "C" fn text_editor_get_active_shape_id(buffer_ptr: *mut u32) { #[no_mangle] pub extern "C" fn text_editor_select_all() -> bool { with_state_mut!(state, { - if !state.text_editor_state.is_active { + if !state.text_editor_state.has_focus { return false; } @@ -126,7 +139,7 @@ pub extern "C" fn text_editor_select_all() -> bool { #[no_mangle] pub extern "C" fn text_editor_select_word_boundary(x: f32, y: f32) { with_state_mut!(state, { - if !state.text_editor_state.is_active { + if !state.text_editor_state.has_focus { return; } @@ -163,7 +176,7 @@ pub extern "C" fn text_editor_poll_event() -> u8 { #[no_mangle] pub extern "C" fn text_editor_pointer_down(x: f32, y: f32) { with_state_mut!(state, { - if !state.text_editor_state.is_active { + if !state.text_editor_state.has_focus { return; } let Some(shape_id) = state.text_editor_state.active_shape_id else { @@ -186,7 +199,7 @@ pub extern "C" fn text_editor_pointer_down(x: f32, y: f32) { #[no_mangle] pub extern "C" fn text_editor_pointer_move(x: f32, y: f32) { with_state_mut!(state, { - if !state.text_editor_state.is_active { + if !state.text_editor_state.has_focus { return; } let point = Point::new(x, y); @@ -214,7 +227,7 @@ pub extern "C" fn text_editor_pointer_move(x: f32, y: f32) { #[no_mangle] pub extern "C" fn text_editor_pointer_up(x: f32, y: f32) { with_state_mut!(state, { - if !state.text_editor_state.is_active { + if !state.text_editor_state.has_focus { return; } let point = Point::new(x, y); @@ -242,7 +255,7 @@ pub extern "C" fn text_editor_pointer_up(x: f32, y: f32) { #[no_mangle] pub extern "C" fn text_editor_set_cursor_from_offset(x: f32, y: f32) { with_state_mut!(state, { - if !state.text_editor_state.is_active { + if !state.text_editor_state.has_focus { return; } @@ -265,7 +278,7 @@ pub extern "C" fn text_editor_set_cursor_from_offset(x: f32, y: f32) { #[no_mangle] pub extern "C" fn text_editor_set_cursor_from_point(x: f32, y: f32) { with_state_mut!(state, { - if !state.text_editor_state.is_active { + if !state.text_editor_state.has_focus { return; } @@ -304,7 +317,7 @@ pub extern "C" fn text_editor_insert_text() -> Result<()> { }; with_state_mut!(state, { - if !state.text_editor_state.is_active { + if !state.text_editor_state.has_focus { return Ok(()); } @@ -357,7 +370,7 @@ pub extern "C" fn text_editor_insert_text() -> Result<()> { #[no_mangle] pub extern "C" fn text_editor_delete_backward(word_boundary: bool) { with_state_mut!(state, { - if !state.text_editor_state.is_active { + if !state.text_editor_state.has_focus { return; } @@ -410,7 +423,7 @@ pub extern "C" fn text_editor_delete_backward(word_boundary: bool) { #[no_mangle] pub extern "C" fn text_editor_delete_forward(word_boundary: bool) { with_state_mut!(state, { - if !state.text_editor_state.is_active { + if !state.text_editor_state.has_focus { return; } @@ -463,7 +476,7 @@ pub extern "C" fn text_editor_delete_forward(word_boundary: bool) { #[no_mangle] pub extern "C" fn text_editor_insert_paragraph() { with_state_mut!(state, { - if !state.text_editor_state.is_active { + if !state.text_editor_state.has_focus { return; } @@ -521,7 +534,7 @@ pub extern "C" fn text_editor_move_cursor( extend_selection: bool, ) { with_state_mut!(state, { - if !state.text_editor_state.is_active { + if !state.text_editor_state.has_focus { return; } @@ -604,7 +617,7 @@ pub extern "C" fn text_editor_move_cursor( #[no_mangle] pub extern "C" fn text_editor_get_cursor_rect() -> *mut u8 { with_state_mut!(state, { - if !state.text_editor_state.is_active || !state.text_editor_state.cursor_visible { + if !state.text_editor_state.has_focus || !state.text_editor_state.cursor_visible { return std::ptr::null_mut(); } @@ -638,7 +651,7 @@ pub extern "C" fn text_editor_get_cursor_rect() -> *mut u8 { #[no_mangle] pub extern "C" fn text_editor_get_selection_rects() -> *mut u8 { with_state_mut!(state, { - if !state.text_editor_state.is_active { + if !state.text_editor_state.has_focus { return std::ptr::null_mut(); } @@ -687,10 +700,6 @@ pub extern "C" fn text_editor_update_blink(timestamp_ms: f64) { #[no_mangle] pub extern "C" fn text_editor_render_overlay() { with_state_mut!(state, { - if !state.text_editor_state.is_active { - return; - } - let Some(shape_id) = state.text_editor_state.active_shape_id else { return; }; @@ -735,7 +744,7 @@ pub extern "C" fn text_editor_render_overlay() { #[no_mangle] pub extern "C" fn text_editor_export_content() -> *mut u8 { with_state!(state, { - if !state.text_editor_state.is_active { + if !state.text_editor_state.has_focus { return std::ptr::null_mut(); } @@ -778,7 +787,7 @@ pub extern "C" fn text_editor_export_content() -> *mut u8 { pub extern "C" fn text_editor_export_selection() -> *mut u8 { use std::ptr; with_state!(state, { - if !state.text_editor_state.is_active { + if !state.text_editor_state.has_focus { return ptr::null_mut(); } let Some(shape_id) = state.text_editor_state.active_shape_id else { @@ -853,10 +862,10 @@ pub extern "C" fn text_editor_export_selection() -> *mut u8 { } #[no_mangle] -pub extern "C" fn text_editor_get_selection(buffer_ptr: *mut u32) -> u32 { +pub extern "C" fn text_editor_get_selection(buffer_ptr: *mut u32) -> bool { with_state!(state, { - if !state.text_editor_state.is_active { - return 0; + if !state.text_editor_state.selection.is_selection() { + return false; } let sel = &state.text_editor_state.selection; unsafe { @@ -865,7 +874,7 @@ pub extern "C" fn text_editor_get_selection(buffer_ptr: *mut u32) -> u32 { *buffer_ptr.add(2) = sel.focus.paragraph as u32; *buffer_ptr.add(3) = sel.focus.offset as u32; } - 1 + true }) }