🎉 Feat apply styles to selection

This commit is contained in:
Aitor Moreno 2026-03-12 14:55:28 +01:00
parent c7f63c4155
commit d9073b1828
8 changed files with 133 additions and 96 deletions

View File

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

View File

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

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

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

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"
@ -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)

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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
})
}