Merge remote-tracking branch 'origin/staging' into develop

This commit is contained in:
Alejandro Alonso 2026-03-31 11:29:44 +02:00
commit 56b28b5440
12 changed files with 355 additions and 123 deletions

View File

@ -1191,9 +1191,9 @@
; Check if the shape has changed any
; attribute that participates in components synchronization.
(and (= (:type operation) :set)
(get ctk/sync-attrs (:attr operation))))
any-sync? (some need-sync? operations)]
(when any-sync?
(contains? ctk/sync-attrs (:attr operation))))]
(when (some need-sync? operations)
(parents-frames id (:objects page))))))
(defmethod frames-changed :mov-objects

View File

@ -48,11 +48,12 @@
(def log-shape-ids #{})
(def log-container-ids #{})
(def updatable-attrs (->> (seq (keys ctk/sync-attrs))
;; We don't update the flex-child attrs
(remove ctk/swap-keep-attrs)
;; We don't do automatic update of the `layout-grid-cells` property.
(remove #(= :layout-grid-cells %))))
(def updatable-attrs
(->> (keys ctk/sync-attrs)
;; We don't update the flex-child attrs
(remove ctk/swap-keep-attrs)
;; We don't do automatic update of the `layout-grid-cells` property.
(remove #(= :layout-grid-cells %))))
(defn enabled-shape?
[id container]
@ -1646,19 +1647,22 @@
(defn- generate-update-tokens
[changes container dest-shape origin-shape touched omit-touched? valid-attrs]
;; valid-attrs is a set of attrs to consider on the update. If it is nil, it will consider all the attrs
(let [attrs (->> (seq (keys ctk/sync-attrs))
;; We don't update the flex-child attrs
(remove #(= :layout-grid-cells %)))
(let [attrs
(->> (keys ctk/sync-attrs)
;; We don't update the flex-child attrs
(remove #(= :layout-grid-cells %)))
applied-tokens (reduce (fn [applied-tokens attr]
(let [attr-group (get ctk/sync-attrs attr)
token-attrs (cto/shape-attr->token-attrs attr)]
(if (and (or (not omit-touched?) (not (touched attr-group)))
(or (empty? valid-attrs) (contains? valid-attrs attr)))
(into applied-tokens token-attrs)
applied-tokens)))
#{}
attrs)]
applied-tokens
(reduce (fn [applied-tokens attr]
(let [sync-group (or (ctk/resolve-sync-group (:type origin-shape) attr)
(ctk/resolve-sync-group (:type dest-shape) attr))
token-attrs (cto/shape-attr->token-attrs attr)]
(if (and (or (not omit-touched?) (not (touched sync-group)))
(or (empty? valid-attrs) (contains? valid-attrs attr)))
(into applied-tokens token-attrs)
applied-tokens)))
#{}
attrs)]
(cond-> changes
(seq applied-tokens)
(update-tokens container dest-shape origin-shape applied-tokens))))
@ -1804,6 +1808,7 @@
uoperations '()]
(let [attr (first attrs)]
(if (nil? attr)
(cond-> changes
(seq roperations)
@ -1813,55 +1818,60 @@
:always
(generate-update-tokens container dest-shape origin-shape touched omit-touched? nil))
(let [attr-group (get ctk/sync-attrs attr)
(let [sync-group
(or (ctk/resolve-sync-group (:type origin-shape) attr)
(ctk/resolve-sync-group (:type dest-shape) attr))
;; position-data is a special case because can be affected by
;; :geometry-group and :content-group so, if the position-data
;; changes but the geometry is touched we need to reset the position-data
;; so it's calculated again
reset-pos-data? (and (cfh/text-shape? origin-shape)
(= attr :position-data)
(not= (:position-data origin-shape) (:position-data dest-shape))
(touched :geometry-group))
;; :geometry-group and :content-group so, if the
;; position-data changes but the geometry is touched
;; we need to reset the position-data so it's
;; calculated again
reset-pos-data?
(and (cfh/text-shape? origin-shape)
(= attr :position-data)
(not= (:position-data origin-shape) (:position-data dest-shape))
(touched :geometry-group))
;; On texts, when we want to omit the touched attrs, both text (the actual letters)
;; and attrs (bold, font, etc) are in the same attr :content.
;; If only one of them is touched, we want to adress this case and
;; only update the untouched one
text-content-change?
(and
omit-touched?
(cfh/text-shape? origin-shape)
(= :content attr)
(touched attr-group))
(and omit-touched?
(cfh/text-shape? origin-shape)
(= :content attr)
(touched sync-group))
skip-operations?
(or (= (get origin-shape attr) (get dest-shape attr))
(and (touched attr-group)
(and (touched sync-group)
omit-touched?
;; When it is a text-partial-change, we should generate operations
;; even when omit-touched? is true, but updating only the text or
;; the attributes, omiting the other part
(not text-content-change?)))
attr-val (when-not skip-operations?
(cond
;; If position data changes and the geometry group is touched
;; we need to put to nil so we can regenerate it
reset-pos-data?
nil
attr-val
(when-not skip-operations?
(cond
;; If position data changes and the geometry group is touched
;; we need to put to nil so we can regenerate it
reset-pos-data?
nil
text-content-change?
(text-change-value (:content dest-shape)
(:content origin-shape)
touched)
text-content-change?
(text-change-value (:content dest-shape)
(:content origin-shape)
touched)
:else
(get origin-shape attr)))
:else
(get origin-shape attr)))
;; If the final attr-value is the actual value, skip
skip-operations? (or skip-operations?
(= attr-val (get dest-shape attr)))
skip-operations?
(or skip-operations?
(= attr-val (get dest-shape attr)))
;; On a text-partial-change, we want to force a position-data reset
;; so it's calculated again
@ -2079,7 +2089,9 @@
roperations [{:type :set-touched :touched (:touched previous-shape)}]
uoperations (list {:type :set-touched :touched (:touched current-shape)})]
(if-let [attr (first attrs)]
(let [attr-group (get ctk/sync-attrs attr)
(let [sync-group
(ctk/resolve-sync-group (:type previous-shape) attr)
skip-operations?
(or
;; For auto text, avoid copying geometry-driven attrs on switch.
@ -2096,7 +2108,7 @@
(= (get previous-shape attr) (get origin-ref-shape attr))
;; If the attr is not touched, don't copy it
(not (touched attr-group))
(not (touched sync-group))
;; If both variants (origin and destiny) don't have the same value
;; for that attribute, don't copy it.
@ -2120,12 +2132,11 @@
;; If only one of them is touched, we want to adress this case and
;; only update the untouched one
text-change?
(and
(not skip-operations?)
(cfh/text-shape? current-shape)
(cfh/text-shape? previous-shape)
(= :content attr)
(touched attr-group))
(and (not skip-operations?)
(cfh/text-shape? current-shape)
(cfh/text-shape? previous-shape)
(= :content attr)
(touched sync-group))
path-change?
(and (= :path (:type current-shape))
@ -2218,9 +2229,12 @@
(->> attrs
(reduce
(fn [dest attr]
(let [attr-group (get ctk/sync-attrs attr)]
(let [sync-group
(or (ctk/resolve-sync-group (:type origin) attr)
(ctk/resolve-sync-group (:type dest) attr))]
(cond-> dest
(or (not (touched attr-group)) (not omit-touched?))
(or (not (touched sync-group))
(not omit-touched?))
(assoc attr (get origin attr)))))
dest))))

View File

@ -7,6 +7,7 @@
(ns app.common.types.component
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.schema :as sm]
[app.common.time :as-alias ct]
[app.common.types.page :as ctp]
@ -39,15 +40,17 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Attributes that may be synced in components, and the group they belong to.
;; When one attribute is modified in a shape inside a component, the corresponding
;; group is marked as :touched. Then, if the shape is synced with the remote shape
;; in the main component, none of the attributes of the same group is changed.
;; When one attribute is modified in a shape inside a component, the
;; corresponding group is marked as :touched. Then, if the shape is synced with
;; the remote shape in the main component, none of the attributes of the same
;; group is changed.
(def sync-attrs
{:name :name-group
:fills :fill-group
:hide-fill-on-export :fill-group
:content :content-group
:content {:path :geometry-group
:text :content-group}
:position-data :content-group
:hidden :visibility-group
:blocked :modifiable-group
@ -143,6 +146,18 @@
:layout-item-align-self
:interactions})
(defn resolve-sync-group
"Makes a by type resolution of the sync group. This is necessary
because we have several properties that has different group
depending on the shape type. Per example the attr `:content` is used
by path and text shapes and the sync groups are different for each
shape type."
[type attr]
(when-let [group (get sync-attrs attr)]
(if (map? group)
(get group type)
group)))
(defn component-attr?
"Check if some attribute is one that is involved in component syncrhonization.
Note that design tokens also are involved, although they go by an alternate
@ -150,7 +165,7 @@
Also when detaching a nested copy it also needs to trigger a synchronization,
even though :shape-ref is not a synced attribute per se"
[attr]
(or (get sync-attrs attr)
(or (contains? sync-attrs attr)
(= :shape-ref attr)
(= :applied-tokens attr)))
@ -356,15 +371,17 @@
(or (not (instance-head? shape))
(not (in-component-copy? parent))))))
(defn all-touched-groups
[]
(into #{} (vals sync-attrs)))
(def ^:private all-touched-groups
(reduce-kv (fn [acc _ v]
(if (map? v)
(into acc (vals v))
(conj acc v)))
#{}
sync-attrs))
(defn valid-touched-group?
[group]
(try
(or (contains? (all-touched-groups) group)
(and (swap-slot? group)
(some? (group->swap-slot group))))
(catch #?(:clj Throwable :cljs :default) _
false)))
(ex/ignoring
(or (contains? all-touched-groups group)
(and (swap-slot? group)
(some? (group->swap-slot group))))))

View File

@ -554,14 +554,14 @@
(let [old-applied-tokens (d/nilv (:applied-tokens shape) #{})
changed-token-attrs (filter #(not= (get old-applied-tokens %) (get new-applied-tokens %))
ctt/all-keys)
text-shape? (= (:type shape) :text)
shape-type (get shape :type)
text-shape? (= shape-type :text)
attrs-in-text-content? (some #(ctt/attrs-in-text-content %)
changed-token-attrs)
changed-groups (into #{}
(comp (map ctt/token-attr->shape-attr)
(map #(get ctk/sync-attrs %))
(filter some?))
(keep #(ctk/resolve-sync-group shape-type %)))
changed-token-attrs)
changed-groups (if (and text-shape?
@ -577,8 +577,9 @@
The returned shape will contain a metadata associated with it
indicating if shape is touched or not."
[shape attr val & {:keys [ignore-touched ignore-geometry]}]
(let [group (get ctk/sync-attrs attr)
shape-val (get shape attr)
(let [type (get shape :type)
group (ctk/resolve-sync-group type attr)
shape-val (get shape attr)
ignore?
(or ignore-touched
@ -612,16 +613,20 @@
(not equal?)
(not (and ignore-geometry is-geometry?)))
content-diff-type (when (and (= (:type shape) :text) (= attr :content))
(cttx/get-diff-type (:content shape) val))
content-diff-type
(when (and (= type :text) (= attr :content))
(cttx/get-diff-type (:content shape) val))
token-groups (if (= attr :applied-tokens)
(get-token-groups shape val)
#{})
token-groups
(if (= attr :applied-tokens)
(get-token-groups shape val)
#{})
groups
(cond-> token-groups
(and group (not equal?))
(set/union #{group} content-diff-type))]
groups (cond-> token-groups
(and group (not equal?))
(set/union #{group} content-diff-type))]
(cond-> shape
;; Depending on the origin of the attribute change, we need or not to
;; set the "touched" flag for the group the attribute belongs to.

View File

@ -27,7 +27,7 @@
;; --- Auxiliar Functions
(def valid-browsers
#{:chrome :firefox :safari :safari-16 :safari-17 :edge :other})
#{:chrome :firefox :safari :safari-16 :safari-17 :safari-18 :safari-26 :edge :other})
(def valid-platforms
#{:windows :linux :macos :other})
@ -40,13 +40,17 @@
check-edge? (fn [] (str/includes? user-agent "edg"))
check-safari? (fn [] (str/includes? user-agent "safari"))
check-safari-16? (fn [] (and (check-safari?) (str/includes? user-agent "version/16")))
check-safari-17? (fn [] (and (check-safari?) (str/includes? user-agent "version/17")))]
check-safari-17? (fn [] (and (check-safari?) (str/includes? user-agent "version/17")))
check-safari-18? (fn [] (and (check-safari?) (str/includes? user-agent "version/18")))
check-safari-26? (fn [] (and (check-safari?) (str/includes? user-agent "version/26")))]
(cond
^boolean (check-edge?) :edge
^boolean (check-chrome?) :chrome
^boolean (check-firefox?) :firefox
^boolean (check-safari-16?) :safari-16
^boolean (check-safari-17?) :safari-17
^boolean (check-safari-18?) :safari-18
^boolean (check-safari-26?) :safari-26
^boolean (check-safari?) :safari
:else :unknown)))
@ -212,7 +216,7 @@
(defn ^boolean check-browser? [candidate]
(dm/assert! (contains? valid-browsers candidate))
(if (= candidate :safari)
(contains? #{:safari :safari-16 :safari-17} browser)
(contains? #{:safari :safari-16 :safari-17 :safari-18 :safari-26} browser)
(= candidate browser)))
(defn ^boolean check-platform? [candidate]

View File

@ -63,7 +63,7 @@
(ptk/reify ::initialize-rasterizer
ptk/EffectEvent
(effect [_ state _]
(when (feat/active-feature? state "render-wasm/v1")
(when-not (feat/active-feature? state "render-wasm/v1")
(thr/init!)))))
(defn initialize

View File

@ -44,6 +44,7 @@
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.specialized-panel :as dwsp]
[app.main.data.workspace.thumbnails :as dwt]
[app.main.data.workspace.thumbnails-wasm :as dwt.wasm]
[app.main.data.workspace.transforms :as dwtr]
[app.main.data.workspace.undo :as dwu]
[app.main.data.workspace.wasm-text :as dwwt]
@ -945,7 +946,12 @@
component (ctkl/get-component data component-id)
page-id (:main-instance-page component)
root-id (:main-instance-id component)]
(dwt/update-thumbnail file-id page-id root-id tag "update-component-thumbnail-sync")))
(if (and (= tag "component")
(features/active-feature? state "render-wasm/v1"))
;; WASM: render immediately, UI only — server persist happens
;; on the debounced path (update-component-thumbnail)
(dwt.wasm/render-thumbnail file-id page-id root-id)
(dwt/update-thumbnail file-id page-id root-id tag "update-component-thumbnail-sync"))))
(defn update-component-sync
([shape-id file-id] (update-component-sync shape-id file-id nil))
@ -964,9 +970,12 @@
(dwu/start-undo-transaction undo-id)
(update-component shape-id undo-group)
;; These two calls are necessary for properly sync thumbnails
;; when a main component does not live in the same page
(update-component-thumbnail-sync state component-id file-id "frame")
;; These calls are necessary for properly sync thumbnails
;; when a main component does not live in the same page.
;; When WASM is active, skip the "frame" tag (SVG-based) since
;; component previews are rendered locally via WASM.
(when-not (features/active-feature? state "render-wasm/v1")
(update-component-thumbnail-sync state component-id file-id "frame"))
(update-component-thumbnail-sync state component-id file-id "component")
(sync-file current-file-id file-id :components component-id undo-group)
@ -991,13 +1000,21 @@
(dwu/commit-undo-transaction undo-id)))))))
(defn update-component-thumbnail
"Update the thumbnail of the component with the given id, in the
current file and in the imported libraries."
"Persist the thumbnail of the component to the server.
For WASM, the UI is already up-to-date from the immediate render in
update-component-thumbnail-sync, so this only persists.
For SVG, this does the full render + persist."
[component-id file-id]
(ptk/reify ::update-component-thumbnail
ptk/WatchEvent
(watch [_ state _]
(rx/of (update-component-thumbnail-sync state component-id file-id "component")))))
(if (features/active-feature? state "render-wasm/v1")
(let [data (dsh/lookup-file-data state file-id)
component (ctkl/get-component data component-id)
page-id (:main-instance-page component)
root-id (:main-instance-id component)]
(rx/of (dwt.wasm/persist-thumbnail file-id page-id root-id)))
(rx/of (update-component-thumbnail-sync state component-id file-id "component"))))))
(defn- find-shape-index
[objects id shape-id]
@ -1373,9 +1390,18 @@
(->> (rx/from changed-components)
(rx/map #(component-changed % (:id old-data) undo-group))))
;; even if save-undo? is false, we need to update the :modified-date of the component
;; (for example, for undos)
(->> (rx/from changed-components)
(rx/map touch-component)))
;; (for example, for undos). When WASM is active, also re-render the thumbnail
;; so undo/redo visually updates component previews.
(->> (mapcat (fn [component-id]
(if (features/active-feature? @st/state "render-wasm/v1")
(let [component (ctkl/get-component old-data component-id)]
[(touch-component component-id)
(dwt.wasm/render-thumbnail (:id old-data)
(:main-instance-page component)
(:main-instance-id component))])
[(touch-component component-id)]))
changed-components)
(rx/from)))
(rx/empty)))))
@ -1390,18 +1416,32 @@
(rx/debounce 5000)
(rx/tap #(log/trc :hint "buffer initialized")))]
(when (contains? cf/flags :component-thumbnails)
(->> (rx/merge
changes-s
(when (or (contains? cf/flags :component-thumbnails)
(features/active-feature? @st/state "render-wasm/v1"))
(let [wasm? (features/active-feature? @st/state "render-wasm/v1")]
(->> (rx/merge
changes-s
(->> changes-s
(rx/map deref)
(rx/buffer-until notifier-s)
(rx/mapcat #(into #{} %))
(rx/map (fn [[component-id file-id]]
(update-component-thumbnail component-id file-id)))))
;; WASM: render thumbnails immediately for instant UI feedback
(if wasm?
(->> changes-s
(rx/filter (ptk/type? ::component-changed))
(rx/map deref)
(rx/map (fn [[component-id file-id]]
(update-component-thumbnail-sync @st/state component-id file-id "component"))))
(rx/empty))
(rx/take-until stopper-s)))))))
;; Persist thumbnails to the server in batches after user
;; becomes inactive for 5 seconds.
(->> changes-s
(rx/filter (ptk/type? ::component-changed))
(rx/map deref)
(rx/buffer-until notifier-s)
(rx/mapcat #(into #{} %))
(rx/map (fn [[component-id file-id]]
(update-component-thumbnail component-id file-id)))))
(rx/take-until stopper-s))))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Backend interactions

View File

@ -118,7 +118,7 @@
(rx/ignore))))
(rx/empty)))))))
(defn- assoc-thumbnail
(defn assoc-thumbnail
[object-id uri]
(let [prev-uri* (volatile! nil)]
(ptk/reify ::assoc-thumbnail

View File

@ -0,0 +1,118 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.main.data.workspace.thumbnails-wasm
"WASM-based component thumbnail rendering.
Renders component previews using the existing workspace WASM context
via render-shape-pixels (SurfaceId::Export), avoiding a separate
WASM module in the worker.
Two-phase design:
- render-thumbnail: immediate WASM render → UI update (no server)
- persist-thumbnail: pushes current data-uri to the server (debounced)"
(:require
[app.common.data.macros :as dm]
[app.common.logging :as l]
[app.common.thumbnails :as thc]
[app.common.time :as ct]
[app.main.data.workspace.thumbnails :as dwt]
[app.main.repo :as rp]
[app.render-wasm.api :as wasm.api]
[app.util.webapi :as wapi]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[potok.v2.core :as ptk]))
(l/set-level! :warn)
(defn- png-bytes->data-uri
"Converts a Uint8Array of PNG bytes to a data:image/png;base64 URI."
[png-bytes]
(let [blob (wapi/create-blob png-bytes "image/png")
reader (js/FileReader.)]
(js/Promise.
(fn [resolve reject]
(set! (.-onload reader)
(fn [] (resolve (.-result reader))))
(set! (.-onerror reader)
(fn [e] (reject e)))
(.readAsDataURL reader blob)))))
(defn- render-component-pixels
"Renders a component frame using the workspace WASM context.
Returns an observable that emits a data-uri string.
Deferred by one animation frame so that process-shape-changes!
has time to sync all child shapes to WASM memory first."
[frame-id]
(rx/create
(fn [subs]
(js/requestAnimationFrame
(fn [_]
(try
(let [png-bytes (wasm.api/render-shape-pixels frame-id 1)]
(if (or (nil? png-bytes) (zero? (.-length png-bytes)))
(do (js/console.error "[thumbnails] render-shape-pixels returned empty for" (str frame-id))
(rx/end! subs))
(.then (png-bytes->data-uri png-bytes)
(fn [data-uri]
(rx/push! subs data-uri)
(rx/end! subs))
(fn [err]
(rx/error! subs err)))))
(catch :default err
(rx/error! subs err)))))
nil)))
(defn render-thumbnail
"Renders a component thumbnail via WASM and updates the UI immediately.
Does NOT persist to the server — persistence is handled separately
by `persist-thumbnail` on a debounced schedule."
[file-id page-id frame-id]
(let [object-id (thc/fmt-object-id file-id page-id frame-id "component")]
(ptk/reify ::render-thumbnail
cljs.core/IDeref
(-deref [_] object-id)
ptk/WatchEvent
(watch [_ _ stream]
(let [tp (ct/tpoint-ms)]
(->> (render-component-pixels frame-id)
(rx/map
(fn [data-uri]
(l/dbg :hint "component thumbnail rendered (wasm)"
:elapsed (dm/str (tp) "ms"))
(dwt/assoc-thumbnail object-id data-uri)))
(rx/catch (fn [err]
(js/console.error "[thumbnails] error rendering component thumbnail" err)
(rx/empty)))
(rx/take-until
(->> stream
(rx/filter (ptk/type? ::dwt/clear-thumbnail))
(rx/filter #(= (deref %) object-id))))))))))
(defn persist-thumbnail
"Persists the current component thumbnail data-uri to the server.
Expects that `render-thumbnail` has already been called so the
data-uri is present in app state. If not, this is a no-op."
[file-id page-id frame-id]
(let [object-id (thc/fmt-object-id file-id page-id frame-id "component")]
(ptk/reify ::persist-thumbnail
ptk/WatchEvent
(watch [_ state _]
(let [data-uri (dm/get-in state [:thumbnails object-id])]
(if (and (some? data-uri)
(str/starts-with? data-uri "data:"))
(let [blob (wapi/data-uri->blob data-uri)]
(->> (rp/cmd! :create-file-object-thumbnail
{:file-id file-id
:object-id object-id
:media blob
:tag "component"})
(rx/catch rx/empty)
(rx/ignore)))
(rx/empty)))))))

View File

@ -68,14 +68,16 @@
"false" false
nil))
(def wasm-url-override-ref
(l/derived wasm-url-override st/state))
(defn active-feature?
"Given a state and feature, check if feature is enabled."
[state feature]
(assert (contains? cfeat/supported-features feature)
"feature not supported")
(let [wasm-override (when (= feature "render-wasm/v1")
(wasm-url-override state))]
(let [wasm-override (when (= feature "render-wasm/v1") (wasm-url-override state))]
(cond
(some? wasm-override)
wasm-override
@ -110,8 +112,15 @@
(defn use-feature
"A react hook that checks if feature is currently enabled"
[feature]
(let [enabled-features (mf/deref features-ref)]
(contains? enabled-features feature)))
(let [enabled-features (mf/deref features-ref)
wasm-override (mf/deref wasm-url-override-ref)
wasm-override (when (= feature "render-wasm/v1") wasm-override)]
(cond
(some? wasm-override)
wasm-override
:else
(contains? enabled-features feature))))
(defn toggle-feature
"An event constructor for runtime feature toggle.

View File

@ -205,6 +205,19 @@
(dwt/dispose! instance)
(st/emit! (dwt/update-editor nil)))))
(defn vertical-align-editor-classes
"Returns `[align-top? align-center? align-bottom?]` for the text editor root
flex layout. When `render-wasm?` is true, the `foreignObject` is already
positioned using the same vertical offset as Skia (`content_rect`); applying
`justify-content` center/end here would double the offset and misalign the DOM
editor with the rendered text, caret, and selection."
[content render-wasm?]
(if render-wasm?
[true false false]
[(= (:vertical-align content "top") "top")
(= (:vertical-align content) "center")
(= (:vertical-align content) "bottom")]))
(defn get-color-from-content [content]
(let [fills (->> (tree-seq map? :children content)
(mapcat :fills)
@ -225,7 +238,7 @@
"Text editor (HTML)"
{::mf/wrap [mf/memo]
::mf/props :obj}
[{:keys [shape canvas-ref]}]
[{:keys [shape canvas-ref render-wasm?] :or {render-wasm? false}}]
(let [content (:content shape)
shape-id (dm/get-prop shape :id)
fill-color (get-color-from-content content)
@ -244,6 +257,9 @@
text-color (or fill-color (get-default-text-color {:frame frame
:background-color background-color}) color/black)
[align-top? align-center? align-bottom?]
(vertical-align-editor-classes content render-wasm?)
fonts
(-> (mf/use-memo (mf/deps content) #(get-fonts content))
(h/use-equal-memo))]
@ -291,9 +307,9 @@
:grow-type-fixed (= (:grow-type shape) :fixed)
:grow-type-auto-width (= (:grow-type shape) :auto-width)
:grow-type-auto-height (= (:grow-type shape) :auto-height)
:align-top (= (:vertical-align content "top") "top")
:align-center (= (:vertical-align content) "center")
:align-bottom (= (:vertical-align content) "bottom")))
:align-top align-top?
:align-center align-center?
:align-bottom align-bottom?))
:ref editor-ref
:data-testid "text-editor-content"
:data-x (dm/get-prop shape :x)
@ -348,7 +364,7 @@
;; NOTE: this teoretically breaks hooks rules, but in practice
;; it is imposible to really break it
maybe-zoom
(when (cf/check-browser? :safari-16)
(when (cf/check-browser? :safari)
(mf/deref refs/selected-zoom))
shape (cond-> shape
@ -404,17 +420,23 @@
;; Transform is necessary when there is a text overflow and the vertical
;; aligment is center or bottom.
(and (not render-wasm?)
(not (cf/check-browser? :safari)))
(not (cf/check-browser? :safari-16)))
(obj/merge!
#js {:transform (dm/fmt "translate(%px, %px)" (- (dm/get-prop shape :x) x) (- (dm/get-prop shape :y) y))})
(cf/check-browser? :safari-17)
(and (cf/check-browser? :safari) (not (cf/check-browser? :safari-16)))
(obj/merge!
#js {:height "100%"
:display "flex"
:flexDirection "column"
:justifyContent (shape->justify shape)})
(or (cf/check-browser? :safari-26) (cf/check-browser? :safari-18))
(obj/merge!
#js {:position "fixed"
:transform-origin "top left"
:transform (dm/fmt "scale(%)" maybe-zoom)})
(cf/check-browser? :safari-16)
(obj/merge!
#js {:position "fixed"
@ -435,4 +457,5 @@
[:div {:style style}
[:& text-editor-html {:shape shape
:canvas-ref canvas-ref
:render-wasm? render-wasm?
:key (dm/str shape-id)}]]]]))

View File

@ -24,6 +24,7 @@
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.undo :as dwu]
[app.main.data.workspace.variants :as dwv]
[app.main.features :as features]
[app.main.refs :as refs]
[app.main.render :refer [component-svg component-svg-thumbnail]]
[app.main.store :as st]
@ -298,7 +299,8 @@
(inc retry))))]
(if (and (some? thumbnail-uri)
(contains? cf/flags :component-thumbnails))
(or (contains? cf/flags :component-thumbnails)
(features/active-feature? @st/state "render-wasm/v1")))
[:& component-svg-thumbnail
{:thumbnail-uri thumbnail-uri
:class class