From 53c2acb3e6a4956b2f20d5c29fc9c438ef484ef8 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 3 Feb 2026 17:30:54 +0100 Subject: [PATCH] :bug: Fix several problems with layouts and texts --- common/src/app/common/types/text.cljc | 4 +- .../app/main/data/workspace/shape_layout.cljs | 2 +- .../src/app/main/data/workspace/texts.cljs | 6 +- .../app/main/data/workspace/wasm_text.cljs | 59 +++++++++++++++ .../app/main/ui/workspace/viewport/utils.cljs | 49 +++++++------ frontend/src/app/plugins/api.cljs | 16 +++-- frontend/src/app/render_wasm/api.cljs | 14 ++-- render-wasm/src/shapes.rs | 4 ++ render-wasm/src/shapes/modifiers.rs | 72 ++++++++++--------- .../src/shapes/modifiers/flex_layout.rs | 19 +++-- .../src/shapes/modifiers/grid_layout.rs | 5 +- render-wasm/src/shapes/transform.rs | 6 +- render-wasm/src/state/shapes_pool.rs | 20 ++++++ 13 files changed, 191 insertions(+), 85 deletions(-) diff --git a/common/src/app/common/types/text.cljc b/common/src/app/common/types/text.cljc index 9a56504f0e..7c140136ca 100644 --- a/common/src/app/common/types/text.cljc +++ b/common/src/app/common/types/text.cljc @@ -407,17 +407,19 @@ (defn change-text "Changes the content of the text shape to use the text as argument. Will use the styles of the first paragraph and text that is present in the shape (and override the rest)" - [content text] + [content text & {:as styles}] (let [root-styles (select-keys content root-attrs) paragraph-style (merge default-text-attrs + styles (select-keys (->> content (node-seq is-paragraph-node?) first) text-all-attrs)) text-style (merge default-text-attrs + styles (select-keys (->> content (node-seq is-text-node?) first) text-all-attrs)) paragraph-texts diff --git a/frontend/src/app/main/data/workspace/shape_layout.cljs b/frontend/src/app/main/data/workspace/shape_layout.cljs index e31b892a8f..163195f11f 100644 --- a/frontend/src/app/main/data/workspace/shape_layout.cljs +++ b/frontend/src/app/main/data/workspace/shape_layout.cljs @@ -104,7 +104,7 @@ (watch [_ state _] (let [page-id (or page-id (:current-page-id state)) objects (dsh/lookup-page-objects state page-id) - ids (->> ids (filter #(contains? objects %)))] + ids (->> ids (remove uuid/zero?) (filter #(contains? objects %)))] (if (d/not-empty? ids) (let [modif-tree (dwm/create-modif-tree ids (ctm/reflow-modifiers))] (if (features/active-feature? state "render-wasm/v1") diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index 54fcf70abc..76b888e238 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -776,11 +776,7 @@ (rx/of (v2-update-text-editor-styles id attrs))) (when (features/active-feature? state "render-wasm/v1") - ;; This delay is to give time for the font to be correctly rendered - ;; in wasm. - (cond->> (rx/of (dwwt/resize-wasm-text id)) - (contains? attrs :font-id) - (rx/delay 200))))))) + (rx/of (dwwt/resize-wasm-text-debounce id))))))) ptk/EffectEvent (effect [_ state _] diff --git a/frontend/src/app/main/data/workspace/wasm_text.cljs b/frontend/src/app/main/data/workspace/wasm_text.cljs index 2174ba7161..fd814b45f5 100644 --- a/frontend/src/app/main/data/workspace/wasm_text.cljs +++ b/frontend/src/app/main/data/workspace/wasm_text.cljs @@ -62,6 +62,65 @@ (rx/of (dwm/apply-wasm-modifiers (resize-wasm-text-modifiers shape))) (rx/empty)))))) +(defn resize-wasm-text-debounce-commit + [] + (ptk/reify ::resize-wasm-text + ptk/WatchEvent + (watch [_ state _] + (let [ids (get state ::resize-wasm-text-debounce-ids) + objects (dsh/lookup-page-objects state) + + modifiers + (reduce + (fn [modifiers id] + (let [shape (get objects id)] + (cond-> modifiers + (and (some? shape) + (cfh/text-shape? shape) + (not= :fixed (:grow-type shape))) + (merge (resize-wasm-text-modifiers shape))))) + {} + ids)] + (if (not (empty? modifiers)) + (rx/of (dwm/apply-wasm-modifiers modifiers)) + (rx/empty)))))) + +;; This event will debounce the resize events so, if there are many, they +;; are processed at the same time and not one-by-one. This will improve +;; performance because it's better to make only one layout calculation instead +;; of (potentialy) hundreds. +(defn resize-wasm-text-debounce + [id] + (let [cur-event (js/Symbol)] + (ptk/reify ::resize-wasm-text-debounce + ptk/UpdateEvent + (update [_ state] + (-> state + (update ::resize-wasm-text-debounce-ids (fnil conj []) id) + (cond-> (nil? (::resize-wasm-text-debounce-event state)) + (assoc ::resize-wasm-text-debounce-event cur-event)))) + + ptk/WatchEvent + (watch [_ state stream] + (if (= (::resize-wasm-text-debounce-event state) cur-event) + (let [stopper (->> stream (rx/filter (ptk/type? :app.main.data.workspace/finalize)))] + (rx/concat + (rx/merge + (->> stream + (rx/filter (ptk/type? ::resize-wasm-text-debounce)) + (rx/debounce 20) + (rx/take 1) + (rx/delay 200) + (rx/map #(resize-wasm-text-debounce-commit)) + (rx/take-until stopper)) + + (rx/of (resize-wasm-text-debounce id))) + + (rx/of #(dissoc % + ::resize-wasm-text-debounce-ids + ::resize-wasm-text-debounce-event)))) + (rx/empty)))))) + (defn resize-wasm-text-all "Resize all text shapes (auto-width/auto-height) from a collection of ids." [ids] diff --git a/frontend/src/app/main/ui/workspace/viewport/utils.cljs b/frontend/src/app/main/ui/workspace/viewport/utils.cljs index fac3c4ba00..a4ac92b6c2 100644 --- a/frontend/src/app/main/ui/workspace/viewport/utils.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/utils.cljs @@ -75,32 +75,31 @@ [{:keys [points] :as shape} zoom grid-edition?] (let [leftmost (->> points (reduce left?)) topmost (->> points (remove #{leftmost}) (reduce top?)) - rightmost (->> points (remove #{leftmost topmost}) (reduce right?)) + rightmost (->> points (remove #{leftmost topmost}) (reduce right?))] + (when (and (some? leftmost) (some? topmost) (some? rightmost)) + (let [left-top (gpt/to-vec leftmost topmost) + left-top-angle (gpt/angle left-top) - left-top (gpt/to-vec leftmost topmost) - left-top-angle (gpt/angle left-top) + top-right (gpt/to-vec topmost rightmost) + top-right-angle (gpt/angle top-right) - top-right (gpt/to-vec topmost rightmost) - top-right-angle (gpt/angle top-right) + ;; Choose the position that creates the less angle between left-side and top-side + [label-pos angle h-pos v-pos] + (if (< (mth/abs left-top-angle) (mth/abs top-right-angle)) + [leftmost left-top-angle left-top (gpt/perpendicular left-top)] + [topmost top-right-angle top-right (gpt/perpendicular top-right)]) - ;; Choose the position that creates the less angle between left-side and top-side - [label-pos angle h-pos v-pos] - (if (< (mth/abs left-top-angle) (mth/abs top-right-angle)) - [leftmost left-top-angle left-top (gpt/perpendicular left-top)] - [topmost top-right-angle top-right (gpt/perpendicular top-right)]) + delta-x (if grid-edition? 40 0) + delta-y (if grid-edition? 50 10) - delta-x (if grid-edition? 40 0) - delta-y (if grid-edition? 50 10) - - label-pos - (-> label-pos - (gpt/subtract (gpt/scale (gpt/unit v-pos) (/ delta-y zoom))) - (gpt/subtract (gpt/scale (gpt/unit h-pos) (/ delta-x zoom))))] - - (dm/fmt "rotate(% %,%) scale(%, %) translate(%, %)" - ;; rotate - angle (:x label-pos) (:y label-pos) - ;; scale - (/ 1 zoom) (/ 1 zoom) - ;; translate - (* zoom (:x label-pos)) (* zoom (:y label-pos))))) + label-pos + (-> label-pos + (gpt/subtract (gpt/scale (gpt/unit v-pos) (/ delta-y zoom))) + (gpt/subtract (gpt/scale (gpt/unit h-pos) (/ delta-x zoom))))] + (dm/fmt "rotate(% %,%) scale(%, %) translate(%, %)" + ;; rotate + angle (:x label-pos) (:y label-pos) + ;; scale + (/ 1 zoom) (/ 1 zoom) + ;; translate + (* zoom (:x label-pos)) (* zoom (:y label-pos))))))) diff --git a/frontend/src/app/plugins/api.cljs b/frontend/src/app/plugins/api.cljs index be97f52a78..8e977858c1 100644 --- a/frontend/src/app/plugins/api.cljs +++ b/frontend/src/app/plugins/api.cljs @@ -26,6 +26,7 @@ [app.main.data.workspace.groups :as dwg] [app.main.data.workspace.media :as dwm] [app.main.data.workspace.selection :as dws] + [app.main.data.workspace.wasm-text :as dwwt] [app.main.fonts :refer [fetch-font-css]] [app.main.router :as rt] [app.main.store :as st] @@ -338,9 +339,14 @@ :else (let [page (dsh/lookup-page @st/state) - shape (-> (cts/setup-shape {:type :text :x 0 :y 0 :grow-type :auto-width}) - (update :content txt/change-text text) - (assoc :position-data nil)) + shape (-> (cts/setup-shape {:type :text + :x 0 :y 0 + :width 1 :height 1 + :grow-type :auto-width}) + (update :content txt/change-text text + ;; Text should be given a color by default + {:fills [{:fill-color "#000000" :fill-opacity 1}]}) + (dissoc :position-data)) changes (-> (cb/empty-changes) @@ -348,7 +354,9 @@ (cb/with-objects (:objects page)) (cb/add-object shape))] - (st/emit! (ch/commit-changes changes)) + (st/emit! + (ch/commit-changes changes) + (dwwt/resize-wasm-text-debounce (:id shape))) (shape/shape-proxy plugin-id (:id shape))))) :createShapeFromSvg diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 5d1ddbd731..bbe548835c 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -190,11 +190,13 @@ (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)}))) + (let [dimensions (get-text-dimensions id) + page-id (:current-page-id @st/state)] + (mw/emit! + {:cmd :index/update-text-rect + :page-id page-id + :shape-id id + :dimensions dimensions})))) (defn- ensure-text-content @@ -1564,7 +1566,7 @@ :text-decoration (get element :text-decoration) :letter-spacing (get element :letter-spacing) :font-style (get element :font-style) - :fills (get element :fills) + :fills (d/nilv (get element :fills) [{:fill-color "#000000"}]) :text text}))))))] (mem/free) diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index adcff410d2..48c3bda1c7 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -1074,6 +1074,10 @@ impl Shape { self.children.first() } + pub fn children_count(&self) -> usize { + self.children_ids_iter(false).count() + } + pub fn children_ids(&self, include_hidden: bool) -> Vec { if include_hidden { return self.children.iter().rev().copied().collect(); diff --git a/render-wasm/src/shapes/modifiers.rs b/render-wasm/src/shapes/modifiers.rs index d0db679cf7..b9cc7201bc 100644 --- a/render-wasm/src/shapes/modifiers.rs +++ b/render-wasm/src/shapes/modifiers.rs @@ -264,7 +264,7 @@ fn propagate_transform( // If this is a layout and we're only moving don't need to reflow if shape.has_layout() && is_resize { - entries.push_back(Modifier::reflow(shape.id)); + entries.push_back(Modifier::reflow(shape.id, false)); } if let Some(parent) = shape.parent_id.and_then(|id| shapes.get(&id)) { @@ -272,7 +272,7 @@ fn propagate_transform( // if the current transformation is not a move propagation. // If it's a move propagation we don't need to reflow, the parent is already changed. if (parent.has_layout() || parent.is_group_like()) && (is_resize || !is_propagate) { - entries.push_back(Modifier::reflow(parent.id)); + entries.push_back(Modifier::reflow(parent.id, false)); } } } @@ -282,7 +282,7 @@ fn propagate_reflow( state: &State, entries: &mut VecDeque, bounds: &mut HashMap, - layout_reflows: &mut Vec, + layout_reflows: &mut HashSet, reflown: &mut HashSet, modifiers: &HashMap, ) { @@ -300,20 +300,7 @@ fn propagate_reflow( Type::Frame(Frame { layout: Some(_), .. }) => { - let mut skip_reflow = false; - if shape.is_layout_horizontal_fill() || shape.is_layout_vertical_fill() { - if let Some(parent_id) = shape.parent_id { - if parent_id != Uuid::nil() && !reflown.contains(&parent_id) { - // If this is a fill layout but the parent has not been reflown yet - // we wait for the next iteration for reflow - skip_reflow = true; - } - } - } - - if !skip_reflow { - layout_reflows.push(*id); - } + layout_reflows.insert(*id); } Type::Group(Group { masked: true }) => { let children_ids = shape.children_ids(true); @@ -340,7 +327,7 @@ fn propagate_reflow( if let Some(parent) = shape.parent_id.and_then(|id| shapes.get(&id)) { if parent.has_layout() || parent.is_group_like() { - entries.push_back(Modifier::reflow(parent.id)); + entries.push_back(Modifier::reflow(parent.id, false)); } } } @@ -382,19 +369,20 @@ pub fn propagate_modifiers( let mut entries: VecDeque<_> = modifiers .iter() .map(|entry| { - // If we receibe a identity matrix we force a reflow + // If we receive a identity matrix we force a reflow if math::identitish(&entry.transform) { - Modifier::Reflow(entry.id) + Modifier::Reflow(entry.id, false) } else { Modifier::Transform(*entry) } }) .collect(); + let shapes = &state.shapes; let mut modifiers = HashMap::::new(); let mut bounds = HashMap::::new(); let mut reflown = HashSet::::new(); - let mut layout_reflows = Vec::::new(); + let mut layout_reflows = HashSet::::new(); // We first propagate the transforms to the children and then after // recalculate the layouts. The layout can create further transforms that @@ -412,25 +400,43 @@ pub fn propagate_modifiers( &mut bounds, &mut modifiers, ), - Modifier::Reflow(id) => propagate_reflow( - &id, - state, - &mut entries, - &mut bounds, - &mut layout_reflows, - &mut reflown, - &modifiers, - ), + Modifier::Reflow(id, force_reflow) => { + if force_reflow { + reflown.remove(&id); + } + + propagate_reflow( + &id, + state, + &mut entries, + &mut bounds, + &mut layout_reflows, + &mut reflown, + &modifiers, + ) + }, } } - for id in layout_reflows.iter() { + let mut layout_reflows_vec: Vec = layout_reflows.into_iter().collect(); + + // We sort the reflows so they are process first the ones that are more + // deep in the tree structure. This way we can be sure that the children layouts + // are already reflowed. + layout_reflows_vec.sort_unstable_by(|id_a, id_b| { + let da = shapes.get_depth(id_a); + let db = shapes.get_depth(id_b); + db.cmp(&da) + }); + + let mut bounds_temp = bounds.clone(); + for id in layout_reflows_vec.iter() { if reflown.contains(id) { continue; } - reflow_shape(id, state, &mut reflown, &mut entries, &mut bounds); + reflow_shape(id, state, &mut reflown, &mut entries, &mut bounds_temp); } - layout_reflows = Vec::new(); + layout_reflows = HashSet::new(); } modifiers diff --git a/render-wasm/src/shapes/modifiers/flex_layout.rs b/render-wasm/src/shapes/modifiers/flex_layout.rs index 9742227833..6377379306 100644 --- a/render-wasm/src/shapes/modifiers/flex_layout.rs +++ b/render-wasm/src/shapes/modifiers/flex_layout.rs @@ -61,6 +61,7 @@ impl LayoutAxis { layout_data: &LayoutData, flex_data: &FlexData, ) -> Self { + let num_child = shape.children_count(); if flex_data.is_row() { Self { main_size: layout_bounds.width(), @@ -73,8 +74,8 @@ impl LayoutAxis { padding_across_end: layout_data.padding_bottom, gap_main: layout_data.column_gap, gap_across: layout_data.row_gap, - is_auto_main: shape.is_layout_horizontal_auto(), - is_auto_across: shape.is_layout_vertical_auto(), + is_auto_main: num_child > 0 && shape.is_layout_horizontal_auto(), + is_auto_across: num_child > 0 && shape.is_layout_vertical_auto(), } } else { Self { @@ -88,8 +89,8 @@ impl LayoutAxis { padding_across_end: layout_data.padding_right, gap_main: layout_data.row_gap, gap_across: layout_data.column_gap, - is_auto_main: shape.is_layout_vertical_auto(), - is_auto_across: shape.is_layout_horizontal_auto(), + is_auto_main: num_child > 0 && shape.is_layout_vertical_auto(), + is_auto_across: num_child > 0 && shape.is_layout_horizontal_auto(), } } } @@ -345,7 +346,10 @@ fn distribute_fill_across_space(layout_axis: &LayoutAxis, tracks: &mut [TrackDat let mut size = track.across_size - child.margin_across_start - child.margin_across_end; size = size.clamp(child.min_across_size, child.max_across_size); - size = f32::min(size, layout_axis.across_space()); + + if !layout_axis.is_auto_across { + size = f32::min(size, layout_axis.across_space()); + } child.across_size = size; } } @@ -620,9 +624,12 @@ pub fn reflow_flex_layout( let mut transform = Matrix::default(); + let mut force_reflow = false; if (new_width - child_bounds.width()).abs() > MIN_SIZE || (new_height - child_bounds.height()).abs() > MIN_SIZE { + // When the child is fill we need to force a reflow + force_reflow = true; transform.post_concat(&math::resize_matrix( layout_bounds, child_bounds, @@ -637,7 +644,7 @@ pub fn reflow_flex_layout( result.push_back(Modifier::transform_propagate(child.id, transform)); if child.has_layout() { - result.push_back(Modifier::reflow(child.id)); + result.push_back(Modifier::reflow(child.id, force_reflow)); } shape_anchor = next_anchor( diff --git a/render-wasm/src/shapes/modifiers/grid_layout.rs b/render-wasm/src/shapes/modifiers/grid_layout.rs index 3fe8e8f6bf..93e7ac571e 100644 --- a/render-wasm/src/shapes/modifiers/grid_layout.rs +++ b/render-wasm/src/shapes/modifiers/grid_layout.rs @@ -765,9 +765,12 @@ pub fn reflow_grid_layout( let mut transform = Matrix::default(); + let mut force_reflow = false; if (new_width - child_bounds.width()).abs() > MIN_SIZE || (new_height - child_bounds.height()).abs() > MIN_SIZE { + // When the child is a fill it needs to be reflown + force_reflow = true; transform.post_concat(&math::resize_matrix( &layout_bounds, &child_bounds, @@ -793,7 +796,7 @@ pub fn reflow_grid_layout( result.push_back(Modifier::transform_propagate(child.id, transform)); if child.has_layout() { - result.push_back(Modifier::reflow(child.id)); + result.push_back(Modifier::reflow(child.id, force_reflow)); } } diff --git a/render-wasm/src/shapes/transform.rs b/render-wasm/src/shapes/transform.rs index d6997599d8..61ed53e891 100644 --- a/render-wasm/src/shapes/transform.rs +++ b/render-wasm/src/shapes/transform.rs @@ -8,7 +8,7 @@ use skia::Matrix; #[derive(PartialEq, Debug, Clone)] pub enum Modifier { Transform(TransformEntry), - Reflow(Uuid), + Reflow(Uuid, bool), } impl Modifier { @@ -18,8 +18,8 @@ impl Modifier { pub fn parent(id: Uuid, transform: Matrix) -> Self { Modifier::Transform(TransformEntry::parent(id, transform)) } - pub fn reflow(id: Uuid) -> Self { - Modifier::Reflow(id) + pub fn reflow(id: Uuid, force_reflow: bool) -> Self { + Modifier::Reflow(id, force_reflow) } } diff --git a/render-wasm/src/state/shapes_pool.rs b/render-wasm/src/state/shapes_pool.rs index 6587a23de2..436d57f2ea 100644 --- a/render-wasm/src/state/shapes_pool.rs +++ b/render-wasm/src/state/shapes_pool.rs @@ -177,6 +177,26 @@ impl ShapesPoolImpl { } } + // Given an id, returns the depth in the tree-shaped structure + // of shapes. + pub fn get_depth(&self, id: &Uuid) -> usize { + if id == &Uuid::nil() { + return 0; + } + + let Some(idx) = self.uuid_to_idx.get(id) else { + return 0; + }; + + let shape = &self.shapes[*idx]; + + let Some(parent_id) = shape.parent_id else { + return 0; + }; + + self.get_depth(&parent_id) + 1 + } + #[allow(dead_code)] pub fn iter(&self) -> std::slice::Iter<'_, Shape> { self.shapes.iter()