🎉 Feat apply styles to selection

This commit is contained in:
Aitor Moreno 2026-03-12 14:55:28 +01:00
parent 0f389fe3ad
commit 12382cfbb9
8 changed files with 133 additions and 89 deletions

View File

@ -788,14 +788,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)

View File

@ -120,7 +120,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)]
@ -129,7 +129,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)]
@ -144,7 +144,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))
@ -283,13 +283,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")
@ -312,11 +312,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))))))

View File

@ -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?

View File

@ -92,14 +92,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)
@ -136,6 +137,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]
(->
@ -155,17 +163,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.
(when (text-editor-wasm?)
(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)))

View File

@ -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"
@ -142,19 +142,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
[]
@ -198,18 +208,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 {}))
@ -260,7 +272,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)
@ -332,7 +344,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)

View File

@ -4,7 +4,7 @@ use skia_safe::textlayout::{RectHeightStyle, RectWidthStyle};
use skia_safe::{BlendMode, Canvas, Paint, Rect};
pub fn render_overlay(canvas: &Canvas, editor_state: &TextEditorState, shape: &Shape) {
if !editor_state.is_active {
if !editor_state.has_focus {
return;
}

View File

@ -165,7 +165,7 @@ pub struct TextEditorState {
pub theme: TextEditorTheme,
pub selection: TextSelection,
pub composition: TextComposition,
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,
@ -185,7 +185,7 @@ impl TextEditorState {
},
selection: TextSelection::new(),
composition: TextComposition::new(),
is_active: false,
has_focus: false,
is_pointer_selection_active: false,
active_shape_id: None,
cursor_visible: true,
@ -194,8 +194,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;
@ -204,8 +204,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;
@ -344,7 +354,7 @@ impl TextEditorState {
}
pub fn update_blink(&mut self, timestamp_ms: f64) {
if !self.is_active {
if !self.has_focus {
return;
}

View File

@ -44,7 +44,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);
@ -56,35 +56,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
})
}
@ -106,7 +119,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;
}
@ -128,7 +141,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;
}
@ -165,7 +178,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 {
@ -188,7 +201,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);
@ -216,7 +229,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);
@ -244,7 +257,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;
}
@ -267,7 +280,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;
}
@ -435,7 +448,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(());
}
@ -488,7 +501,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;
}
@ -541,7 +554,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;
}
@ -594,7 +607,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;
}
@ -652,7 +665,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;
}
@ -735,7 +748,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();
}
@ -769,7 +782,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();
}
@ -818,10 +831,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;
};
@ -858,7 +867,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();
}
@ -901,7 +910,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 {
@ -976,10 +985,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 {
@ -988,7 +997,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
})
}