🐛 Fix text selrect size (#10566)

* 🐛 Fix text selrect size

* 🐛 Fix blinks on complex files

---------

Co-authored-by: Belén Albeza <belen@hey.com>
This commit is contained in:
Elena Torró 2026-07-08 13:56:58 +02:00 committed by GitHub
parent 88186eaec1
commit d656d342fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 88 additions and 30 deletions

View File

@ -65,6 +65,7 @@
[app.main.data.workspace.undo :as dwu]
[app.main.data.workspace.variants :as dwva]
[app.main.data.workspace.viewport :as dwv]
[app.main.data.workspace.wasm-text :as dwwt]
[app.main.data.workspace.zoom :as dwz]
[app.main.errors]
[app.main.features :as features]
@ -441,6 +442,14 @@
;; Keep comment thread positions in sync on undo/redo
(rx/of (dwcm/watch-comment-thread-position-changes stoper-s))
;; Resize auto-grow text shapes whose selrect does not match
;; the WASM text layout once their fonts finish loading.
(->> stream
(rx/filter (ptk/type? :app.render-wasm.api/stale-text-selrects))
(rx/map deref)
(rx/map (fn [{:keys [ids]}]
(dwwt/resize-wasm-text-all ids))))
(let [local-commits-s
(->> stream
(rx/filter dch/commit?)

View File

@ -42,12 +42,14 @@
(wasm.api/set-shape-text-images id content)
(let [dimension (when (not= :fixed grow-type)
(wasm.api/get-text-dimensions))]
{:width (if (#{:fixed :auto-height} grow-type)
(:width selrect)
(:width dimension))
:height (if (= :fixed grow-type)
(:height selrect)
(:height dimension))}))))
;; nil dimension = shape not present in WASM state; skip the resize.
(when (or (= :fixed grow-type) (some? dimension))
{:width (if (#{:fixed :auto-height} grow-type)
(:width selrect)
(:width dimension))
:height (if (= :fixed grow-type)
(:height selrect)
(:height dimension))})))))
(defn resize-wasm-text-modifiers
([shape]

View File

@ -573,15 +573,19 @@
(wasm.api/push-ruler-theme-colors!)
(wasm.api/request-render "rulers-colors-theme")))))
;; Ruler overlay updates below only change the UI surface, not the shapes.
;; They use `render-from-cache!` (cached tiles + UI, atomic) instead of a full
;; `request-render`, which would kick off a progressive tile-by-tile shape
;; re-render that flashes on zoomed-in views (see penpot ruler-selection flash).
(mf/with-effect [@canvas-init? frame-visible?]
(when @canvas-init?
(wasm.api/set-rulers-frame-visible! frame-visible?)
(wasm.api/request-render "rulers-frame")))
(wasm.api/render-from-cache!)))
(mf/with-effect [@canvas-init? show-rulers?]
(when @canvas-init?
(wasm.api/set-rulers-visible! show-rulers?)
(wasm.api/request-render "rulers-visible")))
(wasm.api/render-from-cache!)))
(mf/with-effect [@canvas-init? show-rulers? offset-x offset-y]
(when (and @canvas-init? show-rulers?)
@ -592,7 +596,7 @@
(some-> ruler-selection :width) (some-> ruler-selection :height)]
(when (and @canvas-init? show-rulers?)
(wasm.api/set-rulers-selection! ruler-selection)
(wasm.api/request-render "rulers-selection")))
(wasm.api/render-from-cache!)))
;; Paint background + rulers instantly, before shapes finish loading. Runs
;; after the ruler push effects so the WASM ruler state is already set.

View File

@ -58,6 +58,7 @@
[app.util.timers :as timers]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[potok.v2.core :as ptk]
[promesa.core :as p]
[rumext.v2 :as mf]))
@ -459,6 +460,20 @@
(when (and wasm/context-initialized? (not @wasm/context-lost?))
(h/call wasm/internal-module "_render_ui_only")))
(defn render-from-cache!
"Blit the shapes from the cached tile atlas and redraw the UI overlay
(rulers, selection band) fresh on top, in a single atomic frame. The
*shapes* are the cached part (already-rasterized tiles, not rebuilt); the UI
is re-rendered every call, which is what lets it reflect a new selection.
Use for UI-only updates that don't change shapes (e.g. the ruler selection
band): unlike `request-render`, it never kicks off a progressive,
tile-by-tile shape re-render, so it does not flash on zoomed-in views where
the scene spans multiple tiles."
[]
(when (and wasm/context-initialized? (not @wasm/context-lost?))
(h/call wasm/internal-module "_render_from_cache" 0)))
;; CSS-pixel blur radius for the page-transition snapshot (DPR-scaled in WASM).
(def ^:private TRANSITION_BLUR_RADIUS 4.0)
@ -1282,17 +1297,19 @@
([]
(if-not (initialized?)
{:x 0 :y 0 :width 0 :height 0 :max-width 0}
(let [offset (-> (h/call wasm/internal-module "_get_text_dimensions")
(mem/->offset-32))
heapf32 (mem/get-heap-f32)
width (aget heapf32 (+ offset 0))
height (aget heapf32 (+ offset 1))
max-width (aget heapf32 (+ offset 2))
(let [ptr (h/call wasm/internal-module "_get_text_dimensions")]
;; NULL pointer when there is no current shape or it is not a text.
(when-not (zero? ptr)
(let [offset (mem/->offset-32 ptr)
heapf32 (mem/get-heap-f32)
width (aget heapf32 (+ offset 0))
height (aget heapf32 (+ offset 1))
max-width (aget heapf32 (+ offset 2))
x (aget heapf32 (+ offset 3))
y (aget heapf32 (+ offset 4))]
(mem/free)
{:x x :y y :width width :height height :max-width max-width}))))
x (aget heapf32 (+ offset 3))
y (aget heapf32 (+ offset 4))]
(mem/free)
{:x x :y y :width width :height height :max-width max-width}))))))
(defn intersect-position-in-shape
[id position]
@ -1452,19 +1469,45 @@
[text-ids]
(run! f/force-update-text-layout text-ids))
(defn- text-selrect-stale?
"Check if the WASM-measured dimensions of an auto-grow text shape differ
from its stored selrect (same 0.1px tolerance as the classic renderer)."
[{:keys [id selrect grow-type]}]
(when-let [{:keys [width height]} (get-text-dimensions id)]
(case grow-type
:auto-width (or (not (mth/close? width (:width selrect) 0.1))
(not (mth/close? height (:height selrect) 0.1)))
:auto-height (not (mth/close? height (:height selrect) 0.1))
false)))
(defn- sync-stale-text-selrects!
"Emit the ids of auto-grow text shapes whose selrect no longer matches the
measured layout, so the workspace resizes them (data-event instead of a
direct call to avoid a circular dependency; see the watcher in
`app.main.data.workspace/initialize-workspace`)."
[shapes]
(let [stale-ids (into []
(comp (filter cfh/text-shape?)
(filter (comp #{:auto-width :auto-height} :grow-type))
(filter text-selrect-stale?)
(map :id))
shapes)]
(when (seq stale-ids)
(st/emit! (ptk/data-event ::stale-text-selrects {:ids stale-ids})))))
(defn- relayout-after-fonts!
"Relayout text shapes once their pending fonts have resolved. Shapes in
`font-pending-ids` had a font fetched, so they get a forced relayout to pick
up the real glyph metrics; the remaining text shapes get a normal layout."
"Relayout text shapes once their pending fonts have resolved. Font fetches
are deduped per URL and storing a font does not invalidate cached layouts,
so every text shape (not only the fetch triggers in `font-pending-ids`)
needs a forced relayout; then re-sync selrects that drifted."
[shapes font-pending-ids]
(let [force-ids (set font-pending-ids)
text-ids (into [] (comp (filter cfh/text-shape?) (map :id)) shapes)
forced (filterv force-ids text-ids)
rest-ids (filterv (complement force-ids) text-ids)]
(when (seq forced)
(force-update-text-layouts forced))
(when (seq rest-ids)
(update-text-layouts rest-ids))))
(let [text-ids (into [] (comp (filter cfh/text-shape?) (map :id)) shapes)]
(when (seq text-ids)
(if (seq font-pending-ids)
(do
(force-update-text-layouts text-ids)
(sync-stale-text-selrects! shapes))
(update-text-layouts text-ids)))))
(defn process-pending
[shapes thumbnails full font-pending-ids on-complete]