diff --git a/frontend/src/app/main/ui/workspace/viewport/actions.cljs b/frontend/src/app/main/ui/workspace/viewport/actions.cljs index f0a3bc3600..041cb6f53a 100644 --- a/frontend/src/app/main/ui/workspace/viewport/actions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/actions.cljs @@ -19,7 +19,6 @@ [app.main.data.workspace.media :as dwm] [app.main.data.workspace.path :as dwdp] [app.main.data.workspace.specialized-panel :as-alias dwsp] - [app.main.data.workspace.texts :as dwt] [app.main.features :as features] [app.main.refs :as refs] [app.main.store :as st] @@ -50,41 +49,42 @@ (mf/deps id blocked hidden type selected edition drawing-tool text-editing? node-editing? grid-editing? drawing-path? create-comment? @z? @space? panning read-only?) - (fn [bevent] + (fn [event] ;; We need to handle editor related stuff here because ;; handling on editor dom node does not works properly. - (let [target (dom/get-target bevent) + (let [target (dom/get-target event) editor (txu/closest-text-editor-content target)] ;; Capture mouse pointer to detect the movements even if cursor ;; leaves the viewport or the browser itself ;; https://developer.mozilla.org/en-US/docs/Web/API/Element/setPointerCapture (if editor - (.setPointerCapture editor (.-pointerId bevent)) - (.setPointerCapture target (.-pointerId bevent)))) + (.setPointerCapture editor (.-pointerId event)) + (.setPointerCapture target (.-pointerId event)))) - (when (or (dom/class? (dom/get-target bevent) "viewport-controls") - (dom/class? (dom/get-target bevent) "viewport-selrect") - (dom/child? (dom/get-target bevent) (dom/query ".grid-layout-editor"))) + (when (or (dom/class? (dom/get-target event) "viewport-controls") + (dom/class? (dom/get-target event) "viewport-selrect") + (dom/child? (dom/get-target event) (dom/query ".grid-layout-editor"))) - (dom/stop-propagation bevent) + (dom/stop-propagation event) (when-not @z? - (let [event (dom/event->native-event bevent) - ctrl? (kbd/ctrl? event) - meta? (kbd/meta? event) - shift? (kbd/shift? event) - alt? (kbd/alt? event) - mod? (kbd/mod? event) + (let [native-event (dom/event->native-event event) + ctrl? (kbd/ctrl? native-event) + meta? (kbd/meta? native-event) + shift? (kbd/shift? native-event) + alt? (kbd/alt? native-event) + mod? (kbd/mod? native-event) + off-pt (dom/get-offset-position native-event) - left-click? (and (not panning) (dom/left-mouse? bevent)) - middle-click? (and (not panning) (dom/middle-mouse? bevent))] + left-click? (and (not panning) (dom/left-mouse? event)) + middle-click? (and (not panning) (dom/middle-mouse? event))] (cond (or middle-click? (and left-click? @space?)) (do - (dom/prevent-default bevent) + (dom/prevent-default event) (if mod? - (let [raw-pt (dom/get-client-position event) + (let [raw-pt (dom/get-client-position native-event) pt (uwvv/point->viewport raw-pt)] (st/emit! (dw/start-zooming pt))) (st/emit! (dw/start-panning)))) @@ -94,18 +94,23 @@ (st/emit! (mse/->MouseEvent :down ctrl? shift? alt? meta?) ::dwsp/interrupt) + (when (wasm.api/text-editor-is-active?) + (wasm.api/text-editor-pointer-down (.-x off-pt) (.-y off-pt))) + (when (and (not= edition id) (or text-editing? grid-editing?)) (st/emit! (dw/clear-edition-mode)) + ;; FIXME: I think this is not completely correct because this + ;; is going to happen even when clicking or selecting text. ;; Sync and stop WASM text editor when exiting edit mode - (when (and text-editing? - (features/active-feature? @st/state "render-wasm/v1") - wasm.wasm/context-initialized?) - (when-let [{:keys [shape-id content]} (wasm.api/text-editor-sync-content)] - (st/emit! (dwt/v2-update-text-shape-content - shape-id content - :update-name? true - :finalize? true))) - (wasm.api/text-editor-stop))) + #_(when (and text-editing? + (features/active-feature? @st/state "render-wasm/v1") + wasm.wasm/context-initialized?) + (when-let [{:keys [shape-id content]} (wasm.api/text-editor-sync-content)] + (st/emit! (dwt/v2-update-text-shape-content + shape-id content + :update-name? true + :finalize? true))) + (wasm.api/text-editor-stop))) (when (and (not text-editing?) (not blocked) @@ -187,10 +192,14 @@ alt? (kbd/alt? event) meta? (kbd/meta? event) hovering? (some? @hover) + native-event (dom/event->native-event event) + off-pt (dom/get-offset-position native-event) raw-pt (dom/get-client-position event) pt (uwvv/point->viewport raw-pt)] (st/emit! (mse/->MouseEvent :click ctrl? shift? alt? meta?)) + ;; FIXME: Maybe we can transform this into a cond instead + ;; of multiple (when)s. (when (and hovering? (not @space?) (not edition) @@ -198,6 +207,8 @@ (not drawing-tool)) (st/emit! (dw/select-shape (:id @hover) shift?))) + ;; FIXME: Maybe we can move into a function of the kind + ;; "text-editor-on-click" ;; If clicking on a text shape and wasm render is enabled, forward cursor position (when (and hovering? (not @space?) @@ -208,9 +219,7 @@ (when (and (= :text (:type hover-shape)) (features/active-feature? @st/state "text-editor-wasm/v1") wasm.wasm/context-initialized?) - (let [raw-pt (dom/get-client-position event)] - ;; FIXME - (wasm.api/text-editor-set-cursor-from-point (.-x raw-pt) (.-y raw-pt)))))) + (wasm.api/text-editor-set-cursor-from-point (.-x off-pt) (.-y off-pt))))) (when (and @z? (not @space?) @@ -261,6 +270,12 @@ wasm.wasm/context-initialized?) (wasm.api/text-editor-start id))) + (and editable? (= id edition) (not read-only?) + (= type :text) + (features/active-feature? @st/state "text-editor-wasm/v1") + wasm.wasm/context-initialized?) + (wasm.api/text-editor-select-all) + (some? selected-shape) (do (reset! hover selected-shape) @@ -310,20 +325,24 @@ ;; Release pointer on mouse up (.releasePointerCapture target (.-pointerId event))) - (let [event (dom/event->native-event event) - ctrl? (kbd/ctrl? event) - shift? (kbd/shift? event) - alt? (kbd/alt? event) - meta? (kbd/meta? event) + (let [native-event (dom/event->native-event event) + off-pt (dom/get-offset-position native-event) + ctrl? (kbd/ctrl? native-event) + shift? (kbd/shift? native-event) + alt? (kbd/alt? native-event) + meta? (kbd/meta? native-event) - left-click? (= 1 (.-which event)) - middle-click? (= 2 (.-which event))] + left-click? (= 1 (.-which native-event)) + middle-click? (= 2 (.-which native-event))] (when left-click? - (st/emit! (mse/->MouseEvent :up ctrl? shift? alt? meta?))) + (st/emit! (mse/->MouseEvent :up ctrl? shift? alt? meta?)) + + (when (wasm.api/text-editor-is-active?) + (wasm.api/text-editor-pointer-up (.-x off-pt) (.-y off-pt)))) (when middle-click? - (dom/prevent-default event) + (dom/prevent-default native-event) ;; We store this so in Firefox the middle button won't do a paste of the content (mf/set-ref-val! disable-paste-ref true) @@ -381,7 +400,9 @@ (let [last-position (mf/use-var nil)] (mf/use-fn (fn [event] - (let [raw-pt (dom/get-client-position event) + (let [native-event (unchecked-get event "nativeEvent") + off-pt (dom/get-offset-position native-event) + raw-pt (dom/get-client-position event) pt (uwvv/point->viewport raw-pt) ;; We calculate the delta because Safari's MouseEvent.movementX/Y drop @@ -390,6 +411,12 @@ (gpt/subtract raw-pt @last-position) (gpt/point 0 0))] + ;; IMPORTANT! This function, right now it's called on EVERY pointermove. I think + ;; in the future (when we handle the UI in the render) should be better to + ;; have a "wasm.api/pointer-move" function that works as an entry point for + ;; all the pointer-move events. + (wasm.api/text-editor-pointer-move (.-x off-pt) (.-y off-pt)) + (rx/push! move-stream pt) (reset! last-position raw-pt) (st/emit! (mse/->PointerEvent :delta delta diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 849be1089b..e3f55b0d37 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -87,7 +87,11 @@ (def text-editor-start text-editor/text-editor-start) (def text-editor-stop text-editor/text-editor-stop) (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-select-all text-editor/text-editor-select-all) (def text-editor-sync-content text-editor/text-editor-sync-content) (def dpr @@ -263,22 +267,6 @@ [attrs] (text-editor/apply-style-to-selection attrs use-shape set-shape-text-content)) -(defn update-text-rect! - [id] - (when wasm/context-initialized? - (mw/emit! - {:cmd :index/update-text-rect - :page-id (:current-page-id @st/state) - :shape-id id - :dimensions (get-text-dimensions id)}))) - -(defn- ensure-text-content - "Guarantee that the shape always sends a valid text tree to WASM. When the - content is nil (freshly created text) we fall back to - tc/default-text-content so the renderer receives typography information." - [content] - (or content (tc/v2-default-text-content))) - (defn set-parent-id [id] (let [buffer (uuid/get-u32 id)] @@ -996,6 +984,22 @@ (render-finish) (perf/end-measure "set-view-box::zoom"))))) +(defn update-text-rect! + [id] + (when wasm/context-initialized? + (mw/emit! + {:cmd :index/update-text-rect + :page-id (:current-page-id @st/state) + :shape-id id + :dimensions (get-text-dimensions id)}))) + +(defn- ensure-text-content + "Guarantee that the shape always sends a valid text tree to WASM. When the + content is nil (freshly created text) we fall back to + tc/default-text-content so the renderer receives typography information." + [content] + (or content (tc/v2-default-text-content))) + (defn set-object [shape] (perf/begin-measure "set-object") diff --git a/frontend/src/app/render_wasm/text_editor.cljs b/frontend/src/app/render_wasm/text_editor.cljs index 882f24f890..21bcca45d2 100644 --- a/frontend/src/app/render_wasm/text_editor.cljs +++ b/frontend/src/app/render_wasm/text_editor.cljs @@ -27,6 +27,21 @@ (when wasm/context-initialized? (h/call wasm/internal-module "_text_editor_set_cursor_from_point" x y))) +(defn text-editor-pointer-down + [x y] + (when wasm/context-initialized? + (h/call wasm/internal-module "_text_editor_pointer_down" x y))) + +(defn text-editor-pointer-move + [x y] + (when wasm/context-initialized? + (h/call wasm/internal-module "_text_editor_pointer_move" x y))) + +(defn text-editor-pointer-up + [x y] + (when wasm/context-initialized? + (h/call wasm/internal-module "_text_editor_pointer_up" x y))) + (defn text-editor-update-blink [timestamp-ms] (when wasm/context-initialized? @@ -83,9 +98,12 @@ (h/call wasm/internal-module "_text_editor_stop"))) (defn text-editor-is-active? - [] - (when wasm/context-initialized? - (not (zero? (h/call wasm/internal-module "_text_editor_is_active"))))) + ([id] + (when wasm/context-initialized? + (not (zero? (h/call wasm/internal-module "_text_editor_is_active_with_id" id))))) + ([] + (when wasm/context-initialized? + (not (zero? (h/call wasm/internal-module "_text_editor_is_active")))))) (defn text-editor-export-content [] diff --git a/frontend/text-editor/src/editor/content/dom/Color.js b/frontend/text-editor/src/editor/content/dom/Color.js index ba798dd67a..69f4805495 100644 --- a/frontend/text-editor/src/editor/content/dom/Color.js +++ b/frontend/text-editor/src/editor/content/dom/Color.js @@ -76,3 +76,4 @@ export function getFills(fillStyle) { const [color, opacity] = getColor(fillStyle); return `[["^ ","~:fill-color","${color}","~:fill-opacity",${opacity}]]`; } + diff --git a/frontend/text-editor/src/playground.js b/frontend/text-editor/src/playground.js index ba36cfb046..0bdf72db8c 100644 --- a/frontend/text-editor/src/playground.js +++ b/frontend/text-editor/src/playground.js @@ -162,12 +162,15 @@ class TextEditorPlayground { } this.#module.call("use_shape", ...textShape.id); + // FIXME: This function doesn't exists anymore. + /* const caretPosition = this.#module.call( "get_caret_position_at", e.offsetX, e.offsetY, ); console.log("caretPosition", caretPosition); + */ }; #onResize = (_entries) => { diff --git a/render-wasm/pnpm-workspace.yaml b/render-wasm/pnpm-workspace.yaml new file mode 100644 index 0000000000..efc037aa84 --- /dev/null +++ b/render-wasm/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +onlyBuiltDependencies: + - esbuild diff --git a/render-wasm/src/render/text_editor.rs b/render-wasm/src/render/text_editor.rs index be37ce627d..8d4243f80b 100644 --- a/render-wasm/src/render/text_editor.rs +++ b/render-wasm/src/render/text_editor.rs @@ -45,7 +45,11 @@ fn render_cursor( paint.set_color(editor_state.theme.cursor_color); paint.set_anti_alias(true); + let shape_matrix = shape.get_matrix(); + canvas.save(); + canvas.concat(&shape_matrix); canvas.draw_rect(rect, &paint); + canvas.restore(); } fn render_selection( @@ -65,9 +69,14 @@ fn render_selection( paint.set_blend_mode(BlendMode::Multiply); paint.set_color(editor_state.theme.selection_color); paint.set_anti_alias(true); + + let shape_matrix = shape.get_matrix(); + canvas.save(); + canvas.concat(&shape_matrix); for rect in rects { canvas.draw_rect(rect, &paint); } + canvas.restore(); } fn vertical_align_offset( @@ -99,8 +108,6 @@ fn calculate_cursor_rect( return None; } - let selrect = shape.selrect(); - let mut y_offset = vertical_align_offset(shape, &layout_paragraphs); for (idx, laid_out_para) in layout_paragraphs.iter().enumerate() { if idx == cursor.paragraph { @@ -157,8 +164,8 @@ fn calculate_cursor_rect( }; return Some(Rect::from_xywh( - selrect.x() + cursor_x, - selrect.y() + y_offset, + cursor_x, + y_offset, editor_state.theme.cursor_width, cursor_height, )); @@ -182,7 +189,6 @@ fn calculate_selection_rects( let paragraphs = text_content.paragraphs(); let layout_paragraphs: Vec<_> = text_content.layout.paragraphs.iter().flatten().collect(); - let selrect = shape.selrect(); let mut y_offset = vertical_align_offset(shape, &layout_paragraphs); for (para_idx, laid_out_para) in layout_paragraphs.iter().enumerate() { @@ -225,8 +231,8 @@ fn calculate_selection_rects( for text_box in text_boxes { let r = text_box.rect; rects.push(Rect::from_xywh( - selrect.x() + r.left(), - selrect.y() + y_offset + r.top(), + r.left(), + y_offset + r.top(), r.width(), r.height(), )); diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index 8967b6ee49..9605dabe87 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -258,6 +258,18 @@ pub fn all_with_ancestors( } impl Shape { + pub fn get_relative_point( + point: &Point, + view_matrix: &Matrix, + shape_matrix: &Matrix, + ) -> Option { + let inv_view_matrix = view_matrix.invert()?; + let inv_shape_matrix = shape_matrix.invert()?; + let transform_matrix: Matrix = Matrix::concat(&inv_shape_matrix, &inv_view_matrix); + let shape_relative_point = transform_matrix.map_point(*point); + Some(shape_relative_point) + } + pub fn new(id: Uuid) -> Self { Self { id, diff --git a/render-wasm/src/shapes/text.rs b/render-wasm/src/shapes/text.rs index feaab039fb..1cad369768 100644 --- a/render-wasm/src/shapes/text.rs +++ b/render-wasm/src/shapes/text.rs @@ -112,12 +112,15 @@ impl TextContentSize { } } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Copy, Clone)] pub struct TextPositionWithAffinity { + #[allow(dead_code)] pub position_with_affinity: PositionWithAffinity, pub paragraph: i32, #[allow(dead_code)] pub span: i32, + #[allow(dead_code)] + pub span_relative_offset: i32, pub offset: i32, } @@ -126,12 +129,14 @@ impl TextPositionWithAffinity { position_with_affinity: PositionWithAffinity, paragraph: i32, span: i32, + span_relative_offset: i32, offset: i32, ) -> Self { Self { position_with_affinity, paragraph, span, + span_relative_offset, offset, } } @@ -421,7 +426,10 @@ impl TextContent { self.bounds = Rect::from_ltrb(p1.x, p1.y, p2.x, p2.y); } - pub fn get_caret_position_at(&self, point: &Point) -> Option { + pub fn get_caret_position_from_shape_coords( + &self, + point: &Point, + ) -> Option { let mut offset_y = 0.0; let layout_paragraphs = self.layout.paragraphs.iter().flatten(); @@ -487,6 +495,7 @@ impl TextContent { paragraph_index, span_index, span_offset, + position_with_affinity.position, )); } } @@ -509,12 +518,23 @@ impl TextContent { 0, // paragraph 0 0, // span 0 0, // offset 0 + 0, )); } None } + pub fn get_caret_position_from_screen_coords( + &self, + point: &Point, + view_matrix: &Matrix, + shape_matrix: &Matrix, + ) -> Option { + let shape_rel_point = Shape::get_relative_point(point, view_matrix, shape_matrix)?; + self.get_caret_position_from_shape_coords(&shape_rel_point) + } + /// Builds the ParagraphBuilders necessary to render /// this text. pub fn paragraph_builder_group_from_text( diff --git a/render-wasm/src/state/text_editor.rs b/render-wasm/src/state/text_editor.rs index d7474cc92f..8c384db2b3 100644 --- a/render-wasm/src/state/text_editor.rs +++ b/render-wasm/src/state/text_editor.rs @@ -1,8 +1,11 @@ #![allow(dead_code)] -use crate::shapes::TextPositionWithAffinity; +use crate::shapes::{TextContent, TextPositionWithAffinity}; use crate::uuid::Uuid; -use skia_safe::Color; +use skia_safe::{ + textlayout::{Affinity, PositionWithAffinity}, + Color, +}; /// Cursor position within text content. /// Uses character offsets for precise positioning. @@ -122,6 +125,9 @@ pub struct TextEditorState { pub theme: TextEditorTheme, pub selection: TextSelection, pub is_active: bool, + // This property indicates that we've started + // selecting something with the pointer. + pub is_pointer_selection_active: bool, pub active_shape_id: Option, pub cursor_visible: bool, pub last_blink_time: f64, @@ -138,6 +144,7 @@ impl TextEditorState { }, selection: TextSelection::new(), is_active: false, + is_pointer_selection_active: false, active_shape_id: None, cursor_visible: true, last_blink_time: 0.0, @@ -151,6 +158,7 @@ impl TextEditorState { self.cursor_visible = true; self.last_blink_time = 0.0; self.selection = TextSelection::new(); + self.is_pointer_selection_active = false; self.pending_events.clear(); } @@ -158,7 +166,65 @@ impl TextEditorState { self.is_active = false; self.active_shape_id = None; self.cursor_visible = false; + self.is_pointer_selection_active = false; self.pending_events.clear(); + self.reset_blink(); + } + + pub fn start_pointer_selection(&mut self) -> bool { + if self.is_pointer_selection_active { + return false; + } + self.is_pointer_selection_active = true; + true + } + + pub fn stop_pointer_selection(&mut self) -> bool { + if !self.is_pointer_selection_active { + return false; + } + self.is_pointer_selection_active = false; + true + } + + pub fn select_all(&mut self, content: &TextContent) -> bool { + self.is_pointer_selection_active = false; + self.set_caret_from_position(TextPositionWithAffinity::new( + PositionWithAffinity { + position: 0, + affinity: Affinity::Downstream, + }, + 0, + 0, + 0, + 0, + )); + let num_paragraphs = (content.paragraphs().len() - 1) as i32; + let Some(last_paragraph) = content.paragraphs().last() else { + return false; + }; + let num_spans = (last_paragraph.children().len() - 1) as i32; + let Some(last_text_span) = last_paragraph.children().last() else { + return false; + }; + let mut offset = 0; + for span in last_paragraph.children() { + offset += span.text.len(); + } + self.extend_selection_from_position(TextPositionWithAffinity::new( + PositionWithAffinity { + position: offset as i32, + affinity: Affinity::Upstream, + }, + num_paragraphs, + num_spans, + last_text_span.text.len() as i32, + offset as i32, + )); + self.reset_blink(); + self.push_event(crate::state::EditorEvent::SelectionChanged); + + true } pub fn set_caret_from_position(&mut self, position: TextPositionWithAffinity) { diff --git a/render-wasm/src/wasm/text.rs b/render-wasm/src/wasm/text.rs index e4617575aa..ab4b14541e 100644 --- a/render-wasm/src/wasm/text.rs +++ b/render-wasm/src/wasm/text.rs @@ -1,16 +1,13 @@ use macros::ToJs; use super::{fills::RawFillData, fonts::RawFontStyle}; -use crate::math::{Matrix, Point}; + use crate::mem::{self, SerializableResult}; use crate::shapes::{ self, GrowType, Shape, TextAlign, TextDecoration, TextDirection, TextTransform, Type, }; use crate::utils::{uuid_from_u32, uuid_from_u32_quartet}; -use crate::{ - with_current_shape, with_current_shape_mut, with_state, with_state_mut, - with_state_mut_current_shape, STATE, -}; +use crate::{with_current_shape, with_current_shape_mut, with_state, with_state_mut, STATE}; const RAW_SPAN_DATA_SIZE: usize = std::mem::size_of::(); const RAW_PARAGRAPH_DATA_SIZE: usize = std::mem::size_of::(); @@ -388,32 +385,6 @@ pub extern "C" fn update_shape_text_layout_for(a: u32, b: u32, c: u32, d: u32) { }); } -#[no_mangle] -pub extern "C" fn get_caret_position_at(x: f32, y: f32) -> i32 { - with_state_mut_current_shape!(state, |shape: &Shape| { - if let Type::Text(text_content) = &shape.shape_type { - let mut matrix = Matrix::new_identity(); - let shape_matrix = shape.get_concatenated_matrix(&state.shapes); - let view_matrix = state.render_state.viewbox.get_matrix(); - if let Some(inv_view_matrix) = view_matrix.invert() { - matrix.post_concat(&inv_view_matrix); - matrix.post_concat(&shape_matrix); - - let mapped_point = matrix.map_point(Point::new(x, y)); - - if let Some(position_with_affinity) = - text_content.get_caret_position_at(&mapped_point) - { - return position_with_affinity.position_with_affinity.position; - } - } - } else { - panic!("Trying to get caret position of a shape that it's not a text shape"); - } - }); - -1 -} - const RAW_POSITION_DATA_SIZE: usize = size_of::(); impl From<[u8; RAW_POSITION_DATA_SIZE]> for shapes::PositionData { diff --git a/render-wasm/src/wasm/text_editor.rs b/render-wasm/src/wasm/text_editor.rs index 37758f5bb1..771ee82627 100644 --- a/render-wasm/src/wasm/text_editor.rs +++ b/render-wasm/src/wasm/text_editor.rs @@ -1,5 +1,3 @@ -use macros::ToJs; - use crate::math::{Matrix, Point, Rect}; use crate::mem; use crate::shapes::{Paragraph, Shape, TextContent, Type, VerticalAlign}; @@ -7,6 +5,7 @@ use crate::state::{TextCursor, TextSelection}; use crate::utils::uuid_from_u32_quartet; use crate::utils::uuid_to_u32_quartet; use crate::{with_state, with_state_mut, STATE}; +use macros::ToJs; #[derive(PartialEq, ToJs)] #[repr(u8)] @@ -54,6 +53,17 @@ pub extern "C" fn text_editor_is_active() -> bool { with_state!(state, { state.text_editor_state.is_active }) } +#[no_mangle] +pub extern "C" fn text_editor_is_active_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 + }) +} + #[no_mangle] pub extern "C" fn text_editor_get_active_shape_id(buffer_ptr: *mut u32) { with_state!(state, { @@ -70,45 +80,25 @@ pub extern "C" fn text_editor_get_active_shape_id(buffer_ptr: *mut u32) { } #[no_mangle] -pub extern "C" fn text_editor_select_all() { +pub extern "C" fn text_editor_select_all() -> bool { with_state_mut!(state, { if !state.text_editor_state.is_active { - return; + return false; } let Some(shape_id) = state.text_editor_state.active_shape_id else { - return; + return false; }; let Some(shape) = state.shapes.get(&shape_id) else { - return; + return false; }; let Type::Text(text_content) = &shape.shape_type else { - return; + return false; }; - - let paragraphs = text_content.paragraphs(); - if paragraphs.is_empty() { - return; - } - - let last_para_idx = paragraphs.len() - 1; - let last_para = ¶graphs[last_para_idx]; - let total_chars: usize = last_para - .children() - .iter() - .map(|span| span.text.chars().count()) - .sum(); - - use crate::state::TextCursor; - state.text_editor_state.selection.anchor = TextCursor::new(0, 0); - state.text_editor_state.selection.focus = TextCursor::new(last_para_idx, total_chars); - state.text_editor_state.reset_blink(); - state - .text_editor_state - .push_event(crate::state::EditorEvent::SelectionChanged); - }); + state.text_editor_state.select_all(text_content) + }) } #[no_mangle] @@ -121,146 +111,127 @@ pub extern "C" fn text_editor_poll_event() -> u8 { // ============================================================================ #[no_mangle] -pub extern "C" fn text_editor_set_cursor_from_point(x: f32, y: f32) { +pub extern "C" fn text_editor_pointer_down(x: f32, y: f32) { 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; }; - - let (shape_matrix, view_matrix, selrect, vertical_align) = { - let Some(shape) = state.shapes.get(&shape_id) else { - return; - }; - ( - shape.get_concatenated_matrix(&state.shapes), - state.render_state.viewbox.get_matrix(), - shape.selrect(), - shape.vertical_align(), - ) - }; - - let Some(inv_view_matrix) = view_matrix.invert() else { + let Some(shape) = state.shapes.get(&shape_id) else { return; }; - - let Some(inv_shape_matrix) = shape_matrix.invert() else { + let Type::Text(text_content) = &shape.shape_type else { return; }; - - let mut matrix = Matrix::new_identity(); - matrix.post_concat(&inv_view_matrix); - matrix.post_concat(&inv_shape_matrix); - - let mapped_point = matrix.map_point(Point::new(x, y)); - - let Some(shape) = state.shapes.get_mut(&shape_id) else { - return; - }; - - let Type::Text(text_content) = &mut shape.shape_type else { - return; - }; - - if text_content.layout.paragraphs.is_empty() && !text_content.paragraphs().is_empty() { - let bounds = text_content.bounds; - text_content.update_layout(bounds); - } - - // Calculate vertical alignment offset (same as in render/text_editor.rs) - let layout_paragraphs: Vec<_> = text_content.layout.paragraphs.iter().flatten().collect(); - let total_height: f32 = layout_paragraphs.iter().map(|p| p.height()).sum(); - let vertical_offset = match vertical_align { - crate::shapes::VerticalAlign::Center => (selrect.height() - total_height) / 2.0, - crate::shapes::VerticalAlign::Bottom => selrect.height() - total_height, - _ => 0.0, - }; - - // Adjust point: subtract selrect offset and vertical alignment - // The text layout expects coordinates where (0, 0) is the top-left of the text content - let adjusted_point = Point::new( - mapped_point.x - selrect.x(), - mapped_point.y - selrect.y() - vertical_offset, - ); - - if let Some(position) = text_content.get_caret_position_at(&adjusted_point) { + let point = Point::new(x, y); + let view_matrix: Matrix = state.render_state.viewbox.get_matrix(); + let shape_matrix = shape.get_matrix(); + state.text_editor_state.start_pointer_selection(); + if let Some(position) = + text_content.get_caret_position_from_screen_coords(&point, &view_matrix, &shape_matrix) + { state.text_editor_state.set_caret_from_position(position); } }); } #[no_mangle] -pub extern "C" fn text_editor_extend_selection_to_point(x: f32, y: f32) { +pub extern "C" fn text_editor_pointer_move(x: f32, y: f32) { + with_state_mut!(state, { + if !state.text_editor_state.is_active { + return; + } + let view_matrix: Matrix = state.render_state.viewbox.get_matrix(); + let point = Point::new(x, y); + let Some(shape_id) = state.text_editor_state.active_shape_id else { + return; + }; + let Some(shape) = state.shapes.get(&shape_id) else { + return; + }; + let shape_matrix = shape.get_matrix(); + let Some(_shape_rel_point) = Shape::get_relative_point(&point, &view_matrix, &shape_matrix) + else { + return; + }; + if !state.text_editor_state.is_pointer_selection_active { + return; + } + let Type::Text(text_content) = &shape.shape_type else { + return; + }; + + if let Some(position) = + text_content.get_caret_position_from_screen_coords(&point, &view_matrix, &shape_matrix) + { + state + .text_editor_state + .extend_selection_from_position(position); + } + }); +} + +#[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 { + return; + } + let view_matrix: Matrix = state.render_state.viewbox.get_matrix(); + let point = Point::new(x, y); + let Some(shape_id) = state.text_editor_state.active_shape_id else { + return; + }; + let Some(shape) = state.shapes.get(&shape_id) else { + return; + }; + let shape_matrix = shape.get_matrix(); + let Some(_shape_rel_point) = Shape::get_relative_point(&point, &view_matrix, &shape_matrix) + else { + return; + }; + if !state.text_editor_state.is_pointer_selection_active { + return; + } + let Type::Text(text_content) = &shape.shape_type else { + return; + }; + if let Some(position) = + text_content.get_caret_position_from_screen_coords(&point, &view_matrix, &shape_matrix) + { + state + .text_editor_state + .extend_selection_from_position(position); + } + state.text_editor_state.stop_pointer_selection(); + }); +} + +#[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 { return; } + let view_matrix: Matrix = state.render_state.viewbox.get_matrix(); + let point = Point::new(x, y); let Some(shape_id) = state.text_editor_state.active_shape_id else { return; }; - - let (shape_matrix, view_matrix, selrect, vertical_align) = { - let Some(shape) = state.shapes.get(&shape_id) else { - return; - }; - ( - shape.get_concatenated_matrix(&state.shapes), - state.render_state.viewbox.get_matrix(), - shape.selrect(), - shape.vertical_align(), - ) - }; - - let Some(inv_view_matrix) = view_matrix.invert() else { + let Some(shape) = state.shapes.get(&shape_id) else { return; }; - - let Some(inv_shape_matrix) = shape_matrix.invert() else { + let shape_matrix = shape.get_matrix(); + let Type::Text(text_content) = &shape.shape_type else { return; }; - - let mut matrix = Matrix::new_identity(); - matrix.post_concat(&inv_view_matrix); - matrix.post_concat(&inv_shape_matrix); - - let mapped_point = matrix.map_point(Point::new(x, y)); - - let Some(shape) = state.shapes.get_mut(&shape_id) else { - return; - }; - - let Type::Text(text_content) = &mut shape.shape_type else { - return; - }; - - if text_content.layout.paragraphs.is_empty() && !text_content.paragraphs().is_empty() { - let bounds = text_content.bounds; - text_content.update_layout(bounds); - } - - // Calculate vertical alignment offset (same as in render/text_editor.rs) - let layout_paragraphs: Vec<_> = text_content.layout.paragraphs.iter().flatten().collect(); - let total_height: f32 = layout_paragraphs.iter().map(|p| p.height()).sum(); - let vertical_offset = match vertical_align { - crate::shapes::VerticalAlign::Center => (selrect.height() - total_height) / 2.0, - crate::shapes::VerticalAlign::Bottom => selrect.height() - total_height, - _ => 0.0, - }; - - // Adjust point: subtract selrect offset and vertical alignment - let adjusted_point = Point::new( - mapped_point.x - selrect.x(), - mapped_point.y - selrect.y() - vertical_offset, - ); - - if let Some(position) = text_content.get_caret_position_at(&adjusted_point) { - state - .text_editor_state - .extend_selection_from_position(position); + if let Some(position) = + text_content.get_caret_position_from_screen_coords(&point, &view_matrix, &shape_matrix) + { + state.text_editor_state.set_caret_from_position(position); } }); }