🐛 Fix mixed fills issues

This commit is contained in:
Aitor Moreno 2026-04-15 12:07:52 +02:00 committed by Belén Albeza
parent 77b4d07d1f
commit 424b689dca
4 changed files with 95 additions and 85 deletions

View File

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

View File

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

View File

@ -117,6 +117,7 @@ pub struct TextEditorStyles {
pub font_variant_id: Multiple<Uuid>,
pub line_height: Multiple<f32>,
pub letter_spacing: Multiple<f32>,
pub fills_are_multiple: bool,
pub fills: Vec<Fill>,
}
@ -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()));

View File

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