From 424b689dcac88d61e633fa675dccbd96993f9266 Mon Sep 17 00:00:00 2001 From: Aitor Moreno Date: Wed, 15 Apr 2026 12:07:52 +0200 Subject: [PATCH] :bug: Fix mixed fills issues --- .../src/app/main/data/workspace/texts.cljs | 22 +++--- frontend/src/app/render_wasm/text_editor.cljs | 72 +++++++++++-------- render-wasm/src/state/text_editor.rs | 21 +++--- render-wasm/src/wasm/text_editor.rs | 65 +++++++++-------- 4 files changed, 95 insertions(+), 85 deletions(-) diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index dde3e88322..451b4fb8ae 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -503,13 +503,9 @@ ptk/WatchEvent (watch [_ state _] (when (or - (and (features/active-feature? state "text-editor-wasm/v1") - (nil? (get-in state [:workspace-wasm-editor-styles id]))) (and (features/active-feature? state "text-editor/v2") - (not (features/active-feature? state "text-editor-wasm/v1")) (nil? (:workspace-editor state))) (and (not (features/active-feature? state "text-editor/v2")) - (not (features/active-feature? state "text-editor-wasm/v1")) (nil? (get-in state [:workspace-editor-state id])))) (let [page-id (or (get options :page-id) (get state :current-page-id)) @@ -533,16 +529,20 @@ (-> shape (dissoc :fills) (d/update-when :content update-content)))] - (rx/of (dwsh/update-shapes shape-ids update-shape options))))) + + (rx/concat (rx/of (dwsh/update-shapes shape-ids update-shape options)) + (when (features/active-feature? state "text-editor-wasm/v1") + (let [styles ((comp update-node-fn migrate-node)) + result (wasm.api/apply-styles-to-selection styles)] + (when result + (rx/of (v2-update-text-shape-content + (:shape-id result) + (:content result) + :update-name? true))))))))) ptk/EffectEvent (effect [_ state _] - (cond - (features/active-feature? state "text-editor-wasm/v1") - (let [styles ((comp update-node-fn migrate-node))] - (wasm.api/apply-styles-to-selection styles)) - - (features/active-feature? state "text-editor/v2") + (when (features/active-feature? state "text-editor/v2") (when-let [instance (:workspace-editor state)] (let [styles (some-> (editor.v2/getCurrentStyle instance) (styles/get-styles-from-style-declaration :removed-mixed true) diff --git a/frontend/src/app/render_wasm/text_editor.cljs b/frontend/src/app/render_wasm/text_editor.cljs index cadbd72d31..1cfb5b834c 100644 --- a/frontend/src/app/render_wasm/text_editor.cljs +++ b/frontend/src/app/render_wasm/text_editor.cljs @@ -15,7 +15,7 @@ [app.render-wasm.mem :as mem] [app.render-wasm.wasm :as wasm])) -(def ^:const TEXT_EDITOR_STYLES_METADATA_SIZE (* 30 4)) +(def ^:const TEXT_EDITOR_STYLES_METADATA_SIZE (* 31 4)) (def ^:const TEXT_EDITOR_STYLES_FILL_SOLID 0) (def ^:const TEXT_EDITOR_STYLES_FILL_LINEAR_GRADIENT 1) (def ^:const TEXT_EDITOR_STYLES_FILL_RADIAL_GRADIENT 2) @@ -261,22 +261,23 @@ line-height-state (aget heap-u32 (+ u32-offset 9)) letter-spacing-state (aget heap-u32 (+ u32-offset 10)) num-fills (aget heap-u32 (+ u32-offset 11)) + multiple-fills (aget heap-u32 (+ u32-offset 12)) - text-align-value (aget heap-u32 (+ u32-offset 12)) - text-direction-value (aget heap-u32 (+ u32-offset 13)) - text-decoration-value (aget heap-u32 (+ u32-offset 14)) - text-transform-value (aget heap-u32 (+ u32-offset 15)) - font-family-id-a (aget heap-u32 (+ u32-offset 16)) - font-family-id-b (aget heap-u32 (+ u32-offset 17)) - font-family-id-c (aget heap-u32 (+ u32-offset 18)) - font-family-id-d (aget heap-u32 (+ u32-offset 19)) + text-align-value (aget heap-u32 (+ u32-offset 13)) + text-direction-value (aget heap-u32 (+ u32-offset 14)) + text-decoration-value (aget heap-u32 (+ u32-offset 15)) + text-transform-value (aget heap-u32 (+ u32-offset 16)) + font-family-id-a (aget heap-u32 (+ u32-offset 17)) + font-family-id-b (aget heap-u32 (+ u32-offset 18)) + font-family-id-c (aget heap-u32 (+ u32-offset 19)) + font-family-id-d (aget heap-u32 (+ u32-offset 20)) font-family-id-value (uuid/from-unsigned-parts font-family-id-a font-family-id-b font-family-id-c font-family-id-d) - font-family-style-value (aget heap-u32 (+ u32-offset 20)) - _font-family-weight-value (aget heap-u32 (+ u32-offset 21)) - font-size-value (aget heap-f32 (+ u32-offset 22)) - font-weight-value (aget heap-i32 (+ u32-offset 23)) - line-height-value (aget heap-f32 (+ u32-offset 28)) - letter-spacing-value (aget heap-f32 (+ u32-offset 29)) + font-family-style-value (aget heap-u32 (+ u32-offset 21)) + _font-family-weight-value (aget heap-u32 (+ u32-offset 22)) + font-size-value (aget heap-f32 (+ u32-offset 23)) + font-weight-value (aget heap-i32 (+ u32-offset 24)) + line-height-value (aget heap-f32 (+ u32-offset 29)) + letter-spacing-value (aget heap-f32 (+ u32-offset 30)) font-id (fonts/uuid->font-id font-family-id-value) font-style-value (text-editor-translate-font-style (text-editor-get-style-property font-family-state font-family-style-value)) font-variant-id-computed (text-editor-compute-font-variant-id font-id font-weight-value font-style-value) @@ -291,6 +292,11 @@ (filter some?) (into [])) + ;; The order of these two variables is important, do not + ;; reorder them. + selected-colors (if (= multiple-fills 1) fills nil) + fills (if (= multiple-fills 1) :multiple fills) + result {:vertical-align (text-editor-translate-vertical-align vertical-align) :text-align (text-editor-translate-text-align (text-editor-get-style-property text-align-state text-align-value)) :text-direction (text-editor-translate-text-direction (text-editor-get-style-property text-direction-state text-direction-value)) @@ -306,6 +312,7 @@ :font-variant-id (text-editor-get-style-property font-variant-id-state font-variant-id-computed) :typography-ref-file nil :typography-ref-id nil + :selected-colors selected-colors :fills fills}] (mem/free) @@ -471,6 +478,19 @@ ;; This is used as a intermediate cache between Clojure global state and WASM state. (def ^:private shape-text-contents (atom {})) +(defn cache-shape-text-content! + [shape-id content] + (when (some? content) + (swap! shape-text-contents assoc shape-id content))) + +(defn get-cached-content + [shape-id] + (get @shape-text-contents shape-id)) + +(defn update-cached-content! + [shape-id content] + (swap! shape-text-contents assoc shape-id content)) + (defn- merge-exported-texts-into-content "Merge exported span texts back into the existing content tree. @@ -522,26 +542,13 @@ new-texts (text-editor-export-content)] (when (and shape-id new-texts) (let [texts-clj (js->clj new-texts) - content (get @shape-text-contents shape-id)] + content (get-cached-content shape-id)] (when content (let [merged (merge-exported-texts-into-content content texts-clj)] (swap! shape-text-contents assoc shape-id merged) {:shape-id shape-id :content merged}))))))) -(defn cache-shape-text-content! - [shape-id content] - (when (some? content) - (swap! shape-text-contents assoc shape-id content))) - -(defn get-cached-content - [shape-id] - (get @shape-text-contents shape-id)) - -(defn update-cached-content! - [shape-id content] - (swap! shape-text-contents assoc shape-id content)) - (defn- normalize-selection "Given anchor/focus para+offset, return {:start-para :start-offset :end-para :end-offset} ordered so start <= end." @@ -558,6 +565,7 @@ Splits spans at boundaries as needed." [para sel-start sel-end attrs] (let [spans (:children para) + result (loop [spans spans pos 0 acc []] @@ -594,7 +602,7 @@ selection (text-editor-get-selection)] (when (and shape-id selection) - (let [content (get @shape-text-contents shape-id)] + (let [content (get-cached-content shape-id)] (when content (let [normalized-selection (normalize-selection selection) {:keys [start-para start-offset end-para end-offset]} normalized-selection @@ -630,11 +638,13 @@ (range (count paragraphs)) paragraphs)) + new-content (when new-paragraphs (assoc content :children [(assoc paragraph-set :children new-paragraphs)]))] + (when new-content - (swap! shape-text-contents assoc shape-id new-content) + (update-cached-content! shape-id new-content) (use-shape-fn shape-id) (set-shape-text-content-fn shape-id new-content) {:shape-id shape-id diff --git a/render-wasm/src/state/text_editor.rs b/render-wasm/src/state/text_editor.rs index 82e7daf1ad..062e56864c 100644 --- a/render-wasm/src/state/text_editor.rs +++ b/render-wasm/src/state/text_editor.rs @@ -117,6 +117,7 @@ pub struct TextEditorStyles { pub font_variant_id: Multiple, pub line_height: Multiple, pub letter_spacing: Multiple, + pub fills_are_multiple: bool, pub fills: Vec, } @@ -233,6 +234,7 @@ impl TextEditorStyles { font_variant_id: Multiple::empty(), line_height: Multiple::empty(), letter_spacing: Multiple::empty(), + fills_are_multiple: false, fills: Vec::new(), } } @@ -248,6 +250,7 @@ impl TextEditorStyles { self.font_variant_id.reset(); self.line_height.reset(); self.letter_spacing.reset(); + self.fills_are_multiple = false; self.fills.clear(); } } @@ -529,11 +532,7 @@ impl TextEditorState { let end_paragraph = end.paragraph.min(paragraphs.len() - 1); self.current_styles.reset(); - let mut has_selected_content = false; - let mut has_fills = false; - let mut fills_are_multiple = false; - for (para_idx, paragraph) in paragraphs .iter() .enumerate() @@ -606,14 +605,11 @@ impl TextEditorState { .letter_spacing .merge(Some(span.letter_spacing)); - if !fills_are_multiple { - if !has_fills { - self.current_styles.fills = span.fills.clone(); - has_fills = true; - } else if self.current_styles.fills != span.fills { - fills_are_multiple = true; - self.current_styles.fills.clear(); - } + if self.current_styles.fills.is_empty() { + self.current_styles.fills.append(&mut span.fills.clone()); + } else if self.current_styles.fills != span.fills { + self.current_styles.fills_are_multiple = true; + self.current_styles.fills.append(&mut span.fills.clone()); } } } @@ -630,6 +626,7 @@ impl TextEditorState { let current_offset = focus.offset; let current_text_span = find_text_span_at_offset(current_paragraph, current_offset); + self.current_styles.reset(); self.current_styles .text_align .set_single(Some(current_paragraph.text_align())); diff --git a/render-wasm/src/wasm/text_editor.rs b/render-wasm/src/wasm/text_editor.rs index 9e364e8fb7..dd145bab64 100644 --- a/render-wasm/src/wasm/text_editor.rs +++ b/render-wasm/src/wasm/text_editor.rs @@ -769,6 +769,7 @@ pub extern "C" fn text_editor_get_current_styles() -> *mut u8 { } let mut fill_bytes = Vec::new(); + let fill_multiple = styles.fills_are_multiple; let mut fill_count: u32 = 0; for fill in &styles.fills { if let Ok(raw_fill) = RawFillData::try_from(fill) { @@ -781,39 +782,41 @@ pub extern "C" fn text_editor_get_current_styles() -> *mut u8 { // Layout: 48-byte fixed header + fixed values + serialized fills. let mut bytes = Vec::with_capacity(132 + fill_bytes.len()); - bytes.extend_from_slice(&vertical_align.to_le_bytes()); - bytes.extend_from_slice(&(*styles.text_align.state() as u32).to_le_bytes()); - bytes.extend_from_slice(&(*styles.text_direction.state() as u32).to_le_bytes()); - bytes.extend_from_slice(&(*styles.text_decoration.state() as u32).to_le_bytes()); - bytes.extend_from_slice(&(*styles.text_transform.state() as u32).to_le_bytes()); - bytes.extend_from_slice(&(*styles.font_family.state() as u32).to_le_bytes()); - bytes.extend_from_slice(&(*styles.font_size.state() as u32).to_le_bytes()); - bytes.extend_from_slice(&(*styles.font_weight.state() as u32).to_le_bytes()); - bytes.extend_from_slice(&(*styles.font_variant_id.state() as u32).to_le_bytes()); - bytes.extend_from_slice(&(*styles.line_height.state() as u32).to_le_bytes()); - bytes.extend_from_slice(&(*styles.letter_spacing.state() as u32).to_le_bytes()); - bytes.extend_from_slice(&fill_count.to_le_bytes()); + // Header data // offset // index + bytes.extend_from_slice(&vertical_align.to_le_bytes()); // 0 // 0 + bytes.extend_from_slice(&(*styles.text_align.state() as u32).to_le_bytes()); // 4 // 1 + bytes.extend_from_slice(&(*styles.text_direction.state() as u32).to_le_bytes()); // 8 // 2 + bytes.extend_from_slice(&(*styles.text_decoration.state() as u32).to_le_bytes()); // 12 // 3 + bytes.extend_from_slice(&(*styles.text_transform.state() as u32).to_le_bytes()); // 16 // 4 + bytes.extend_from_slice(&(*styles.font_family.state() as u32).to_le_bytes()); // 20 // 5 + bytes.extend_from_slice(&(*styles.font_size.state() as u32).to_le_bytes()); // 24 // 6 + bytes.extend_from_slice(&(*styles.font_weight.state() as u32).to_le_bytes()); // 28 // 7 + bytes.extend_from_slice(&(*styles.font_variant_id.state() as u32).to_le_bytes()); // 32 // 8 + bytes.extend_from_slice(&(*styles.line_height.state() as u32).to_le_bytes()); // 36 // 9 + bytes.extend_from_slice(&(*styles.letter_spacing.state() as u32).to_le_bytes()); // 40 // 10 + bytes.extend_from_slice(&fill_count.to_le_bytes()); // 44 // 11 + bytes.extend_from_slice(&(fill_multiple as u32).to_le_bytes()); // 48 // 12 // Value section. - bytes.extend_from_slice(&text_align.to_le_bytes()); - bytes.extend_from_slice(&text_direction.to_le_bytes()); - bytes.extend_from_slice(&text_decoration.to_le_bytes()); - bytes.extend_from_slice(&text_transform.to_le_bytes()); - bytes.extend_from_slice(&font_family_id[0].to_le_bytes()); - bytes.extend_from_slice(&font_family_id[1].to_le_bytes()); - bytes.extend_from_slice(&font_family_id[2].to_le_bytes()); - bytes.extend_from_slice(&font_family_id[3].to_le_bytes()); - bytes.extend_from_slice(&font_family_style.to_le_bytes()); - bytes.extend_from_slice(&font_family_weight.to_le_bytes()); - bytes.extend_from_slice(&font_size.to_le_bytes()); - bytes.extend_from_slice(&font_weight.to_le_bytes()); - bytes.extend_from_slice(&font_variant_id[0].to_le_bytes()); - bytes.extend_from_slice(&font_variant_id[1].to_le_bytes()); - bytes.extend_from_slice(&font_variant_id[2].to_le_bytes()); - bytes.extend_from_slice(&font_variant_id[3].to_le_bytes()); - bytes.extend_from_slice(&line_height.to_le_bytes()); - bytes.extend_from_slice(&letter_spacing.to_le_bytes()); - bytes.extend_from_slice(&fill_bytes); + bytes.extend_from_slice(&text_align.to_le_bytes()); // 52 // 13 + bytes.extend_from_slice(&text_direction.to_le_bytes()); // 56 // 14 + bytes.extend_from_slice(&text_decoration.to_le_bytes()); // 60 // 15 + bytes.extend_from_slice(&text_transform.to_le_bytes()); // 64 // 16 + bytes.extend_from_slice(&font_family_id[0].to_le_bytes()); // 68 // 17 + bytes.extend_from_slice(&font_family_id[1].to_le_bytes()); // 72 // 18 + bytes.extend_from_slice(&font_family_id[2].to_le_bytes()); // 76 // 19 + bytes.extend_from_slice(&font_family_id[3].to_le_bytes()); // 80 // 20 + bytes.extend_from_slice(&font_family_style.to_le_bytes()); // 84 // 21 + bytes.extend_from_slice(&font_family_weight.to_le_bytes()); // 88 // 22 + bytes.extend_from_slice(&font_size.to_le_bytes()); // 92 // 23 + bytes.extend_from_slice(&font_weight.to_le_bytes()); // 96 // 24 + bytes.extend_from_slice(&font_variant_id[0].to_le_bytes()); // 100 // 25 + bytes.extend_from_slice(&font_variant_id[1].to_le_bytes()); // 104 // 26 + bytes.extend_from_slice(&font_variant_id[2].to_le_bytes()); // 108 // 27 + bytes.extend_from_slice(&font_variant_id[3].to_le_bytes()); // 112 // 28 + bytes.extend_from_slice(&line_height.to_le_bytes()); // 116 // 29 + bytes.extend_from_slice(&letter_spacing.to_le_bytes()); // 120 // 30 + bytes.extend_from_slice(&fill_bytes); // 124 mem::write_bytes(bytes) })