From d656d342fec6766f8868aa57f98016c531b75593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Torr=C3=B3?= Date: Wed, 8 Jul 2026 13:56:58 +0200 Subject: [PATCH] :bug: Fix text selrect size (#10566) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :bug: Fix text selrect size * :bug: Fix blinks on complex files --------- Co-authored-by: Belén Albeza --- frontend/src/app/main/data/workspace.cljs | 9 ++ .../app/main/data/workspace/wasm_text.cljs | 14 +-- .../app/main/ui/workspace/viewport_wasm.cljs | 10 ++- frontend/src/app/render_wasm/api.cljs | 85 ++++++++++++++----- 4 files changed, 88 insertions(+), 30 deletions(-) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 26bed66c22..25eb0e50d3 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -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?) diff --git a/frontend/src/app/main/data/workspace/wasm_text.cljs b/frontend/src/app/main/data/workspace/wasm_text.cljs index 6468f487aa..d893caedf6 100644 --- a/frontend/src/app/main/data/workspace/wasm_text.cljs +++ b/frontend/src/app/main/data/workspace/wasm_text.cljs @@ -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] diff --git a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs index ca67ec8f11..6529ccf7d1 100644 --- a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs +++ b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs @@ -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. diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index b00406d0f5..3a5357d5d1 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -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]