diff --git a/common/src/app/common/files/helpers.cljc b/common/src/app/common/files/helpers.cljc index 2e6ae5dc5f..2e6b9f688e 100644 --- a/common/src/app/common/files/helpers.cljc +++ b/common/src/app/common/files/helpers.cljc @@ -72,9 +72,11 @@ (= :bool (dm/get-prop shape :type)))) (defn text-shape? - [shape] - (and (some? shape) - (= :text (dm/get-prop shape :type)))) + ([shape] + (and (some? shape) + (= :text (dm/get-prop shape :type)))) + ([objects id] + (text-shape? (get objects id)))) (defn rect-shape? [shape] diff --git a/common/src/app/common/geom/shapes.cljc b/common/src/app/common/geom/shapes.cljc index dce125f366..926eae0e96 100644 --- a/common/src/app/common/geom/shapes.cljc +++ b/common/src/app/common/geom/shapes.cljc @@ -162,6 +162,7 @@ (dm/export gtr/inverse-transform-matrix) (dm/export gtr/transform-rect) (dm/export gtr/calculate-geometry) +(dm/export gtr/calculate-selrect) (dm/export gtr/update-group-selrect) (dm/export gtr/update-mask-selrect) (dm/export gtr/apply-transform) diff --git a/frontend/src/app/main/errors.cljs b/frontend/src/app/main/errors.cljs index ed8462058f..9c0d050a7a 100644 --- a/frontend/src/app/main/errors.cljs +++ b/frontend/src/app/main/errors.cljs @@ -16,6 +16,7 @@ [app.main.data.workspace :as-alias dw] [app.main.router :as rt] [app.main.store :as st] + [app.main.worker] [app.util.globals :as glob] [app.util.i18n :refer [tr]] [app.util.timers :as ts] @@ -94,6 +95,9 @@ (let [data (exception->error-data error)] (ptk/handle-error data)))) +;; Inject dependency to remove circular dependency +(set! app.main.worker/on-error on-error) + ;; Set the main potok error handler (reset! st/on-error on-error) diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs index f573e405bf..190e05700e 100644 --- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs @@ -29,6 +29,7 @@ [app.main.ui.workspace.viewport.actions :as actions] [app.main.ui.workspace.viewport.utils :as utils] [app.main.worker :as mw] + [app.render-wasm.api :as wasm.api] [app.util.debug :as dbg] [app.util.dom :as dom] [app.util.globals :as globals] @@ -311,7 +312,9 @@ ids)] (filter #(or (root-frame-with-data? %) (and (cfh/group-shape? objects %) - (not (contains? child-parent? %))))))) + (not (contains? child-parent? %))) + (and (cfh/text-shape? objects %) + (not (wasm.api/intersect-position % @last-point-ref))))))) remove-measure-xf (cond diff --git a/frontend/src/app/main/worker.cljs b/frontend/src/app/main/worker.cljs index aae9000556..82592252d0 100644 --- a/frontend/src/app/main/worker.cljs +++ b/frontend/src/app/main/worker.cljs @@ -8,15 +8,17 @@ "Interface to communicate with the web worker" (:require [app.config :as cf] - [app.main.errors :as errors] [app.util.worker :as uw] [beicon.v2.core :as rx])) +;; Injected from `app.main.errors` to remove circular dependency +(defonce on-error nil) + (defonce instance nil) (defn init! [] - (let [worker (uw/init cf/worker-uri errors/on-error)] + (let [worker (uw/init cf/worker-uri on-error)] (uw/ask! worker {:cmd :configure :config {:public-uri cf/public-uri :build-data cf/build-date diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index aa2a9dad48..f528fb262e 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -23,6 +23,8 @@ [app.main.fonts :as fonts] [app.main.refs :as refs] [app.main.render :as render] + [app.main.store :as st] + [app.main.worker :as mw] [app.render-wasm.api.fonts :as f] [app.render-wasm.api.texts :as t] [app.render-wasm.deserializers :as dr] @@ -107,6 +109,15 @@ (reset! pending-render false) (render ts))))) +(declare get-text-dimensions) + +(defn update-text-rect! + [id] + (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 @@ -813,9 +824,25 @@ heapf32 (mem/get-heap-f32) width (aget heapf32 (+ offset 0)) height (aget heapf32 (+ offset 1)) - max-width (aget heapf32 (+ offset 2))] + max-width (aget heapf32 (+ offset 2)) + + x (aget heapf32 (+ offset 3)) + y (aget heapf32 (+ offset 4))] (mem/free) - {:width width :height height :max-width max-width}))) + {:x x :y y :width width :height height :max-width max-width}))) + +(defn intersect-position + [id position] + (let [buffer (uuid/get-u32 id) + result + (h/call wasm/internal-module "_intersect_position" + (aget buffer 0) + (aget buffer 1) + (aget buffer 2) + (aget buffer 3) + (:x position) + (:y position))] + (= result 1))) (defn set-view-box [zoom vbox] @@ -922,7 +949,13 @@ (->> shapes (filter cfh/text-shape?) (map :id) - (run! f/update-text-layout))) + (run! + (fn [id] + (f/update-text-layout id) + (mw/emit! {:cmd :index/update-text-rect + :page-id (:current-page-id @st/state) + :shape-id id + :dimensions (get-text-dimensions id)}))))) (defn process-pending! [shapes thumbnails full] diff --git a/frontend/src/app/render_wasm/shape.cljs b/frontend/src/app/render_wasm/shape.cljs index 0887a0e2ac..9a919772f1 100644 --- a/frontend/src/app/render_wasm/shape.cljs +++ b/frontend/src/app/render_wasm/shape.cljs @@ -8,6 +8,7 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] [app.common.transit :as t] [app.common.types.shape :as shape] [app.common.types.shape.layout :as ctl] @@ -131,66 +132,116 @@ (let [v (get shape k) id (get shape :id)] (case k - :parent-id (api/set-parent-id v) - :type (do - (api/set-shape-type v) - (when (or (= v :path) (= v :bool)) - (api/set-shape-path-content (:content shape)))) - :bool-type (api/set-shape-bool-type v) - :selrect (do - (api/set-shape-selrect v) - (when (= (:type shape) :svg-raw) - (api/set-shape-svg-raw-content (api/get-static-markup shape)))) - :show-content (if (= (:type shape) :frame) - (api/set-shape-clip-content (not v)) - (api/set-shape-clip-content false)) - :rotation (api/set-shape-rotation v) - :transform (api/set-shape-transform v) - :fills (into [] (api/set-shape-fills id v false)) - :strokes (into [] (api/set-shape-strokes id v false)) - :blend-mode (api/set-shape-blend-mode v) - :opacity (api/set-shape-opacity v) - :hidden (api/set-shape-hidden v) - :shapes (api/set-shape-children v) - :blur (api/set-shape-blur v) - :shadow (api/set-shape-shadows v) - :constraints-h (api/set-constraints-h v) - :constraints-v (api/set-constraints-v v) + :parent-id + (api/set-parent-id v) + + :type + (do + (api/set-shape-type v) + (when (or (= v :path) (= v :bool)) + (api/set-shape-path-content (:content shape)))) + + :bool-type + (api/set-shape-bool-type v) + + :selrect + (do + (api/set-shape-selrect v) + (when (cfh/svg-raw-shape? shape) + (api/set-shape-svg-raw-content (api/get-static-markup shape)))) + + :show-content + (if (cfh/frame-shape? shape) + (api/set-shape-clip-content (not v)) + (api/set-shape-clip-content false)) + + :rotation + (api/set-shape-rotation v) + + :transform + (api/set-shape-transform v) + + :fills + (into [] (api/set-shape-fills id v false)) + + :strokes + (into [] (api/set-shape-strokes id v false)) + + :blend-mode + (api/set-shape-blend-mode v) + + :opacity + (api/set-shape-opacity v) + + :hidden + (api/set-shape-hidden v) + + :shapes + (api/set-shape-children v) + + :blur + (api/set-shape-blur v) + + :shadow + (api/set-shape-shadows v) + + :constraints-h + (api/set-constraints-h v) + + :constraints-v + (api/set-constraints-v v) :r1 - (api/set-shape-corners [v (dm/get-prop shape :r2) (dm/get-prop shape :r3) (dm/get-prop shape :r4)]) + (api/set-shape-corners + [v + (dm/get-prop shape :r2) + (dm/get-prop shape :r3) + (dm/get-prop shape :r4)]) :r2 - (api/set-shape-corners [(dm/get-prop shape :r1) v (dm/get-prop shape :r3) (dm/get-prop shape :r4)]) + (api/set-shape-corners + [(dm/get-prop shape :r1) + v + (dm/get-prop shape :r3) + (dm/get-prop shape :r4)]) :r3 - (api/set-shape-corners [(dm/get-prop shape :r1) (dm/get-prop shape :r2) v (dm/get-prop shape :r4)]) + (api/set-shape-corners + [(dm/get-prop shape :r1) + (dm/get-prop shape :r2) + v + (dm/get-prop shape :r4)]) :r4 - (api/set-shape-corners [(dm/get-prop shape :r1) (dm/get-prop shape :r2) (dm/get-prop shape :r3) v]) + (api/set-shape-corners + [(dm/get-prop shape :r1) + (dm/get-prop shape :r2) + (dm/get-prop shape :r3) + v]) :svg-attrs - (when (= (:type shape) :path) + (when (cfh/path-shape? shape) (api/set-shape-path-attrs v)) :masked-group - (when (and (= (:type shape) :group) (:masked-group shape)) + (when (cfh/mask-shape? shape) (api/set-masked (:masked-group shape))) :content (cond - (or (= (:type shape) :path) - (= (:type shape) :bool)) + (or (cfh/path-shape? shape) + (cfh/bool-shape? shape)) (api/set-shape-path-content v) - (= (:type shape) :svg-raw) + (cfh/svg-raw-shape? shape) (api/set-shape-svg-raw-content (api/get-static-markup shape)) - (= (:type shape) :text) + (cfh/text-shape? shape) (let [pending-thumbnails (into [] (concat (api/set-shape-text-content id v))) pending-full (into [] (concat (api/set-shape-text-images id v)))] - ;; FIXME: this is a hack to process the pending tasks asynchronously - ;; we should probably modify set-wasm-attr! to return a list of callbacks to be executed in a second pass. + ;; FIXME: this is a hack to process the pending tasks asynchronously + ;; we should probably modify set-wasm-attr! to return a list of callbacks + ;;to be executed in a second pass. (api/process-pending! [shape] pending-thumbnails pending-full) nil)) @@ -239,7 +290,7 @@ (ctl/flex-layout? shape) (api/set-flex-layout shape))) - ;; Property not in WASM + ;; Property not in WASM nil)))) (defn process-shape! @@ -254,7 +305,11 @@ (vals) (rx/from) (rx/mapcat (fn [callback] (callback))) - (rx/reduce conj []))) + (rx/reduce conj []) + (rx/tap + (fn [] + (when (cfh/text-shape? shape) + (api/update-text-rect! (:id shape))))))) (rx/empty)))) (defn process-shape-changes! diff --git a/frontend/src/app/worker/index.cljs b/frontend/src/app/worker/index.cljs index b89fad06ee..0de440f7c1 100644 --- a/frontend/src/app/worker/index.cljs +++ b/frontend/src/app/worker/index.cljs @@ -7,9 +7,12 @@ (ns app.worker.index "Page index management within the worker." (:require + [app.common.data :as d] [app.common.data.macros :as dm] [app.common.files.changes :as ch] + [app.common.geom.matrix :as gmt] [app.common.geom.rect :as grc] + [app.common.geom.shapes :as gsh] [app.common.logging :as log] [app.common.time :as ct] [app.worker.impl :as impl] @@ -41,7 +44,17 @@ (let [old-page (dm/get-in @state [:pages-index page-id]) new-page (-> state (swap! ch/process-changes changes false) - (dm/get-in [:pages-index page-id]))] + (dm/get-in [:pages-index page-id])) + + text-rects (dm/get-in @state [::text-rect page-id]) + + ;; Update page objects with the text data + new-page + (reduce-kv + (fn [page id data] + (update-in page [:objects id] d/patch-object data)) + new-page + text-rects)] (swap! state update ::snap snap/update-page old-page new-page) (swap! state update ::selection selection/update-page old-page new-page)) @@ -50,6 +63,31 @@ (log/dbg :hint "page index updated" :id page-id :elapsed elapsed ::log/sync? true)))) nil)) +(defmethod impl/handler :index/update-text-rect + [{:keys [page-id shape-id dimensions]}] + (let [page (dm/get-in @state [:pages-index page-id]) + objects (get page :objects) + shape (get objects shape-id) + center (gsh/shape->center shape) + transform (:transform shape (gmt/matrix)) + rect (-> (grc/make-rect dimensions) + (grc/rect->points)) + points (gsh/transform-points rect center transform) + selrect (gsh/calculate-selrect points (gsh/points->center points)) + + data {:position-data nil + :points points + :selrect selrect} + + shape (d/patch-object shape data) + + objects + (assoc objects shape-id shape)] + + (swap! state update-in [::text-rect page-id] assoc shape-id data) + (swap! state update-in [::selection page-id] selection/update-index-single objects shape) + nil)) + ;; FIXME: schema (defmethod impl/handler :index/query-snap diff --git a/frontend/src/app/worker/selection.cljs b/frontend/src/app/worker/selection.cljs index e48fbfc2c9..36b1119d08 100644 --- a/frontend/src/app/worker/selection.cljs +++ b/frontend/src/app/worker/selection.cljs @@ -105,7 +105,7 @@ index (reduce-kv #(index-shape objects parents-index clip-index %1 %3) (qdt/create (clj->js bounds)) (dissoc objects uuid/zero))] - {:index index :bounds bounds})) + {:index index :bounds bounds :parents-index parents-index :clip-index clip-index})) ;; FIXME: optimize (defn- update-index @@ -140,6 +140,12 @@ (qdt/remove-all index changed-ids) shapes)] + (assoc data :index index :parents-index parents-index :clip-index clip-index))) + +(defn update-index-single + [{index :index parents-index :parents-index clip-index :clip-index :as data} objects shape] + (let [index (qdt/remove-all index [(:id shape)]) + index (index-shape objects parents-index clip-index index shape)] (assoc data :index index))) (defn- query-index diff --git a/render-wasm/src/shapes/text.rs b/render-wasm/src/shapes/text.rs index 25d1f4e85a..b26c0420b2 100644 --- a/render-wasm/src/shapes/text.rs +++ b/render-wasm/src/shapes/text.rs @@ -6,13 +6,16 @@ use crate::{ use core::f32; use macros::ToJs; +use skia_safe::textlayout::{RectHeightStyle, RectWidthStyle}; use skia_safe::{ self as skia, paint::{self, Paint}, textlayout::ParagraphBuilder, textlayout::ParagraphStyle, textlayout::PositionWithAffinity, + Contains, }; + use std::collections::HashSet; use super::FontFamily; @@ -182,6 +185,24 @@ impl TextContentLayout { } } +/* + * Check if the current x,y (in paragraph relative coordinates) is inside + * the paragraph + */ +fn intersects(paragraph: &skia_safe::textlayout::Paragraph, x: f32, y: f32) -> bool { + if y < 0.0 || y > paragraph.height() { + return false; + } + + let pos = paragraph.get_glyph_position_at_coordinate((x, y)); + let idx = pos.position as usize; + + let rects = + paragraph.get_rects_for_range(0..idx + 1, RectHeightStyle::Tight, RectWidthStyle::Tight); + + rects.iter().any(|r| r.rect.contains(&Point::new(x, y))) +} + #[derive(Debug, PartialEq, Clone)] pub struct TextContent { pub paragraphs: Vec, @@ -304,6 +325,32 @@ impl TextContent { bounds } + pub fn content_rect(&self, selrect: &Rect, valign: VerticalAlign) -> Rect { + let x = selrect.x(); + let mut y = selrect.y(); + + let width = if self.grow_type() == GrowType::AutoWidth { + self.size.width + } else { + selrect.width() + }; + + let height = if self.size.width.round() != width.round() { + self.get_height(width) + } else { + self.size.height + }; + + let offset_y = match valign { + VerticalAlign::Center => (selrect.height() - height) / 2.0, + VerticalAlign::Bottom => selrect.height() - height, + _ => 0.0, + }; + y += offset_y; + + Rect::from_xywh(x, y, width, height) + } + pub fn transform(&mut self, transform: &Matrix) { let left = self.bounds.left(); let right = self.bounds.right(); @@ -645,6 +692,42 @@ impl TextContent { (fallback_width, fallback_height) } + + pub fn intersect_position(&self, shape: &Shape, x_pos: f32, y_pos: f32) -> bool { + let rect = self.content_rect(&shape.selrect, shape.vertical_align); + let mut matrix = Matrix::new_identity(); + let center = shape.center(); + let Some(inv_transform) = &shape.transform.invert() else { + return false; + }; + matrix.pre_translate(center); + matrix.pre_concat(inv_transform); + matrix.pre_translate(-center); + + let result = matrix.map_point((x_pos, y_pos)); + + // Change coords to content space + let x_pos = result.x - rect.x(); + let y_pos = result.y - rect.y(); + + let width = self.width(); + let mut paragraph_builders = self.paragraph_builder_group_from_text(None); + let paragraphs = + self.build_paragraphs_from_paragraph_builders(&mut paragraph_builders, width); + + paragraphs + .iter() + .flatten() + .scan( + (0 as f32, None::), + |(height, _), p| { + let prev_height = *height; + *height += p.height(); + Some((prev_height, p)) + }, + ) + .any(|(height, p)| intersects(p, x_pos, y_pos - height)) + } } impl Default for TextContent { diff --git a/render-wasm/src/wasm/text.rs b/render-wasm/src/wasm/text.rs index 315f92d03a..9d6053af18 100644 --- a/render-wasm/src/wasm/text.rs +++ b/render-wasm/src/wasm/text.rs @@ -7,7 +7,9 @@ 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_mut, with_state_mut, with_state_mut_current_shape, STATE}; +use crate::{ + with_current_shape_mut, with_state, with_state_mut, with_state_mut_current_shape, STATE, +}; const RAW_SPAN_DATA_SIZE: usize = std::mem::size_of::(); const RAW_PARAGRAPH_DATA_SIZE: usize = std::mem::size_of::(); @@ -312,14 +314,23 @@ pub extern "C" fn set_shape_grow_type(grow_type: u8) { #[no_mangle] pub extern "C" fn get_text_dimensions() -> *mut u8 { let mut ptr = std::ptr::null_mut(); + with_current_shape_mut!(state, |shape: &mut Shape| { if let Type::Text(content) = &mut shape.shape_type { let text_content_size = content.update_layout(shape.selrect); - let mut bytes = vec![0; 12]; + // Sacar de aqui x, y, width, height + let rect = content.content_rect(&shape.selrect, shape.vertical_align); + + let mut bytes = vec![0; 20]; bytes[0..4].clone_from_slice(&text_content_size.width.to_le_bytes()); bytes[4..8].clone_from_slice(&text_content_size.height.to_le_bytes()); bytes[8..12].clone_from_slice(&text_content_size.max_width.to_le_bytes()); + + // veamos + bytes[12..16].clone_from_slice(&rect.x().to_le_bytes()); + bytes[16..20].clone_from_slice(&rect.y().to_le_bytes()); + ptr = mem::write_bytes(bytes) } }); @@ -329,6 +340,27 @@ pub extern "C" fn get_text_dimensions() -> *mut u8 { ptr } +#[no_mangle] +pub extern "C" fn intersect_position( + a: u32, + b: u32, + c: u32, + d: u32, + x_pos: f32, + y_pos: f32, +) -> bool { + with_state!(state, { + let id = uuid_from_u32_quartet(a, b, c, d); + let Some(shape) = state.shapes.get(&id) else { + return false; + }; + if let Type::Text(content) = &shape.shape_type { + return content.intersect_position(shape, x_pos, y_pos); + } + }); + false +} + fn update_text_layout(shape: &mut Shape) { if let Type::Text(text_content) = &mut shape.shape_type { text_content.update_layout(shape.selrect);