mirror of
https://github.com/penpot/penpot.git
synced 2026-07-25 07:28:22 +00:00
🐛 Fix text editor v3 blinks (#10654)
This commit is contained in:
parent
c467d98c9e
commit
4f46700c94
@ -113,6 +113,11 @@
|
||||
;; `input`. See on-before-input / on-input.
|
||||
pending-replace-ref (mf/use-ref 0)
|
||||
|
||||
;; Tracks an in-flight pointer drag-selection so `on-pointer-move` only
|
||||
;; repaints the selection overlay while a drag is active (mirrors the
|
||||
;; WASM `is_pointer_selection_active` guard), not on every hover move.
|
||||
dragging-ref (mf/use-ref false)
|
||||
|
||||
fallback-fonts (wasm.api/fonts-from-text-content (:content shape) false)
|
||||
fallback-families (map (fn [font]
|
||||
(font-family-from-font-id (:font-id font))) fallback-fonts)
|
||||
@ -157,7 +162,7 @@
|
||||
(when (some? data)
|
||||
(text-editor/text-editor-composition-update data)
|
||||
(sync-wasm-text-editor-content!)
|
||||
(wasm.api/request-render "text-composition"))
|
||||
(wasm.api/request-render-preserving-target "text-composition"))
|
||||
(reset-input-node (mf/ref-val contenteditable-ref)))))
|
||||
|
||||
on-composition-end
|
||||
@ -166,7 +171,7 @@
|
||||
(let [data (or (.-data event) "")]
|
||||
(text-editor/text-editor-composition-end data)
|
||||
(sync-wasm-text-editor-content!)
|
||||
(wasm.api/request-render "text-composition"))
|
||||
(wasm.api/request-render-preserving-target "text-composition"))
|
||||
(reset-input-node (mf/ref-val contenteditable-ref))))
|
||||
|
||||
on-paste
|
||||
@ -178,7 +183,7 @@
|
||||
(when (and text (seq text))
|
||||
(text-editor/text-editor-insert-text text)
|
||||
(sync-wasm-text-editor-content!)
|
||||
(wasm.api/request-render "text-paste"))
|
||||
(wasm.api/request-render-preserving-target "text-paste"))
|
||||
(reset-input-node (mf/ref-val contenteditable-ref)))))
|
||||
|
||||
on-copy
|
||||
@ -201,7 +206,7 @@
|
||||
(when (and text (seq text))
|
||||
(text-editor/text-editor-delete-backward)
|
||||
(sync-wasm-text-editor-content!)
|
||||
(wasm.api/request-render "text-cut"))))
|
||||
(wasm.api/request-render-preserving-target "text-cut"))))
|
||||
(reset-input-node (mf/ref-val contenteditable-ref)))))
|
||||
|
||||
on-key-down
|
||||
@ -225,7 +230,7 @@
|
||||
(do
|
||||
(dom/prevent-default event)
|
||||
(text-editor/text-editor-select-all)
|
||||
(wasm.api/request-render "text-select-all"))
|
||||
(wasm.api/render-text-editor-overlay!))
|
||||
|
||||
;; Enter
|
||||
(= key "Enter")
|
||||
@ -233,7 +238,7 @@
|
||||
(dom/prevent-default event)
|
||||
(text-editor/text-editor-insert-paragraph)
|
||||
(sync-wasm-text-editor-content!)
|
||||
(wasm.api/request-render "text-paragraph"))
|
||||
(wasm.api/request-render-preserving-target "text-paragraph"))
|
||||
|
||||
;; Backspace
|
||||
(= key "Backspace")
|
||||
@ -241,7 +246,7 @@
|
||||
(dom/prevent-default event)
|
||||
(text-editor/text-editor-delete-backward ctrl?)
|
||||
(sync-wasm-text-editor-content!)
|
||||
(wasm.api/request-render "text-delete-backward"))
|
||||
(wasm.api/request-render-preserving-target "text-delete-backward"))
|
||||
|
||||
;; Delete
|
||||
(= key "Delete")
|
||||
@ -249,51 +254,51 @@
|
||||
(dom/prevent-default event)
|
||||
(text-editor/text-editor-delete-forward ctrl?)
|
||||
(sync-wasm-text-editor-content!)
|
||||
(wasm.api/request-render "text-delete-forward"))
|
||||
(wasm.api/request-render-preserving-target "text-delete-forward"))
|
||||
|
||||
;; Insert
|
||||
(= key "Insert")
|
||||
(do
|
||||
(dom/prevent-default event)
|
||||
(text-editor/text-editor-toggle-overtype-mode)
|
||||
(wasm.api/request-render "text-overtype-mode"))
|
||||
(wasm.api/render-text-editor-overlay!))
|
||||
|
||||
;; Arrow keys
|
||||
(= key "ArrowLeft")
|
||||
(do
|
||||
(dom/prevent-default event)
|
||||
(text-editor/text-editor-move-cursor 0 ctrl? shift?)
|
||||
(wasm.api/request-render "text-cursor-move"))
|
||||
(wasm.api/render-text-editor-overlay!))
|
||||
|
||||
(= key "ArrowRight")
|
||||
(do
|
||||
(dom/prevent-default event)
|
||||
(text-editor/text-editor-move-cursor 1 ctrl? shift?)
|
||||
(wasm.api/request-render "text-cursor-move"))
|
||||
(wasm.api/render-text-editor-overlay!))
|
||||
|
||||
(= key "ArrowUp")
|
||||
(do
|
||||
(dom/prevent-default event)
|
||||
(text-editor/text-editor-move-cursor 2 ctrl? shift?)
|
||||
(wasm.api/request-render "text-cursor-move"))
|
||||
(wasm.api/render-text-editor-overlay!))
|
||||
|
||||
(= key "ArrowDown")
|
||||
(do
|
||||
(dom/prevent-default event)
|
||||
(text-editor/text-editor-move-cursor 3 ctrl? shift?)
|
||||
(wasm.api/request-render "text-cursor-move"))
|
||||
(wasm.api/render-text-editor-overlay!))
|
||||
|
||||
(= key "Home")
|
||||
(do
|
||||
(dom/prevent-default event)
|
||||
(text-editor/text-editor-move-cursor 4 ctrl? shift?)
|
||||
(wasm.api/request-render "text-cursor-move"))
|
||||
(wasm.api/render-text-editor-overlay!))
|
||||
|
||||
(= key "End")
|
||||
(do
|
||||
(dom/prevent-default event)
|
||||
(text-editor/text-editor-move-cursor 5 ctrl? shift?)
|
||||
(wasm.api/request-render "text-cursor-move"))
|
||||
(wasm.api/render-text-editor-overlay!))
|
||||
|
||||
;; Let contenteditable handle text input via on-input
|
||||
:else nil)))))
|
||||
@ -341,7 +346,7 @@
|
||||
(text-editor/text-editor-delete-backward)))
|
||||
(text-editor/text-editor-insert-text data)
|
||||
(sync-wasm-text-editor-content!)
|
||||
(wasm.api/request-render "text-input"))
|
||||
(wasm.api/request-render-preserving-target "text-input"))
|
||||
(mf/set-ref-val! pending-replace-ref 0)
|
||||
;; IMPORTANT: do NOT clear the surface here (see keep-input-alive):
|
||||
;; the browser must retain the just-typed character so the macOS
|
||||
@ -353,35 +358,47 @@
|
||||
(fn [^js event]
|
||||
(let [native-event (dom/event->native-event event)
|
||||
off-pt (dom/get-offset-position native-event)]
|
||||
(wasm.api/text-editor-pointer-down off-pt))))
|
||||
(mf/set-ref-val! dragging-ref true)
|
||||
(wasm.api/text-editor-pointer-down off-pt)
|
||||
;; Repaint the caret over the cached tiles instead of a full render,
|
||||
;; which flashes at high zoom (see `render-text-editor-overlay!`).
|
||||
(wasm.api/render-text-editor-overlay!))))
|
||||
|
||||
on-pointer-move
|
||||
(mf/use-fn
|
||||
(fn [^js event]
|
||||
(let [native-event (dom/event->native-event event)
|
||||
off-pt (dom/get-offset-position native-event)]
|
||||
(wasm.api/text-editor-pointer-move off-pt))))
|
||||
(wasm.api/text-editor-pointer-move off-pt)
|
||||
;; Only while dragging: `text-editor-pointer-move` is a no-op
|
||||
;; otherwise, so avoid repainting on plain hover.
|
||||
(when (mf/ref-val dragging-ref)
|
||||
(wasm.api/render-text-editor-overlay!)))))
|
||||
|
||||
on-pointer-up
|
||||
(mf/use-fn
|
||||
(fn [^js event]
|
||||
(let [native-event (dom/event->native-event event)
|
||||
off-pt (dom/get-offset-position native-event)]
|
||||
(wasm.api/text-editor-pointer-up off-pt))))
|
||||
(mf/set-ref-val! dragging-ref false)
|
||||
(wasm.api/text-editor-pointer-up off-pt)
|
||||
(wasm.api/render-text-editor-overlay!))))
|
||||
|
||||
on-click
|
||||
(mf/use-fn
|
||||
(fn [^js event]
|
||||
(let [native-event (dom/event->native-event event)
|
||||
off-pt (dom/get-offset-position native-event)]
|
||||
(wasm.api/text-editor-set-cursor-from-offset off-pt))))
|
||||
(wasm.api/text-editor-set-cursor-from-offset off-pt)
|
||||
(wasm.api/render-text-editor-overlay!))))
|
||||
|
||||
on-double-click
|
||||
(mf/use-fn
|
||||
(fn [^js event]
|
||||
(let [native-event (dom/event->native-event event)
|
||||
off-pt (dom/get-offset-position native-event)]
|
||||
(wasm.api/text-editor-select-word-boundary off-pt))))
|
||||
(wasm.api/text-editor-select-word-boundary off-pt)
|
||||
(wasm.api/render-text-editor-overlay!))))
|
||||
|
||||
on-focus
|
||||
(mf/use-fn
|
||||
@ -420,21 +437,30 @@
|
||||
;; Focus and select all text on mount (this will trigger on-focus)
|
||||
(.focus node)
|
||||
(text-editor/text-editor-select-all)
|
||||
(wasm.api/request-render "text-editor-select-all-on-mount"))
|
||||
(wasm.api/request-render-preserving-target "text-editor-select-all-on-mount"))
|
||||
;; On unmount, finalize the editor content and then dispose the WASM editor.
|
||||
;; We finalize on unmount instead of relying on the browser blur event, because
|
||||
;; it was not being reliable (timing issues, Firefox issues…)
|
||||
(fn []
|
||||
(on-blur)
|
||||
(text-editor/text-editor-dispose)
|
||||
(wasm.api/request-render "text-editor-dispose"))))
|
||||
(wasm.api/request-render-preserving-target "text-editor-dispose"))))
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps)
|
||||
(fn []
|
||||
(let [timeout-id (atom nil)
|
||||
schedule-blink (fn schedule-blink []
|
||||
(when (text-editor/text-editor-has-focus?)
|
||||
(wasm.api/request-render "cursor-blink"))
|
||||
;; The caret only blinks for a collapsed cursor. With an active
|
||||
;; selection there is nothing to animate, so skip the repaint:
|
||||
;; re-compositing every interval would otherwise redraw the
|
||||
;; selection over and over (a visible flicker at high zoom).
|
||||
(when (and (text-editor/text-editor-has-focus?)
|
||||
(not (text-editor/text-editor-has-selection?)))
|
||||
;; Redraw only the caret (cached frame + overlay) instead of a
|
||||
;; full `request-render`, which flashes on zoomed-in views by
|
||||
;; kicking off a progressive tile-by-tile shape re-render.
|
||||
(wasm.api/render-text-editor-overlay!))
|
||||
(reset! timeout-id (js/setTimeout schedule-blink caret-blink-interval-ms)))]
|
||||
(schedule-blink)
|
||||
(fn []
|
||||
|
||||
@ -586,19 +586,22 @@
|
||||
(wasm.api/text-editor-apply-theme)
|
||||
(wasm.api/request-render "text-editor-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).
|
||||
;; Ruler overlay updates below only change the UI surface, not the shapes,
|
||||
;; and they fire on a stable viewbox (toggles / selection changes, not pan).
|
||||
;; They re-present via `render-from-backbuffer!` — reusing the crisp last
|
||||
;; frame + fresh UI — instead of a full `request-render` (which would kick
|
||||
;; off a progressive tile-by-tile re-render that flashes) or a cached-atlas
|
||||
;; blit (whose scale-capped atlas flashes crisp->blurry on zoomed-in views,
|
||||
;; e.g. when the text editor opens at high zoom).
|
||||
(mf/with-effect [@canvas-init? frame-visible?]
|
||||
(when @canvas-init?
|
||||
(wasm.api/set-rulers-frame-visible! frame-visible?)
|
||||
(wasm.api/render-from-cache!)))
|
||||
(wasm.api/render-from-backbuffer!)))
|
||||
|
||||
(mf/with-effect [@canvas-init? show-rulers?]
|
||||
(when @canvas-init?
|
||||
(wasm.api/set-rulers-visible! show-rulers?)
|
||||
(wasm.api/render-from-cache!)))
|
||||
(wasm.api/render-from-backbuffer!)))
|
||||
|
||||
(mf/with-effect [@canvas-init? show-rulers? offset-x offset-y]
|
||||
(when (and @canvas-init? show-rulers?)
|
||||
@ -609,7 +612,7 @@
|
||||
(some-> ruler-selection :width) (some-> ruler-selection :height)]
|
||||
(when (and @canvas-init? show-rulers?)
|
||||
(wasm.api/set-rulers-selection! ruler-selection)
|
||||
(wasm.api/render-from-cache!)))
|
||||
(wasm.api/render-from-backbuffer!)))
|
||||
|
||||
;; Paint background + rulers instantly, before shapes finish loading. Runs
|
||||
;; after the ruler push effects so the WASM ruler state is already set.
|
||||
|
||||
@ -351,6 +351,8 @@
|
||||
(declare request-render)
|
||||
(declare set-shape-vertical-align fonts-from-text-content)
|
||||
(declare reload-renderer!)
|
||||
(declare request-render-preserving-target)
|
||||
(declare render-pending?)
|
||||
|
||||
;; These are the type of frames we have in our
|
||||
;; render pipeline.
|
||||
@ -419,32 +421,52 @@
|
||||
(when (initialized?)
|
||||
(h/call wasm/internal-module "_free_gpu_resources")))
|
||||
|
||||
;; When set, the next render keeps the previously presented frame on screen
|
||||
;; while the new tiles are rasterized, instead of clearing the canvas first.
|
||||
;; See `request-render-preserving-target`.
|
||||
(defonce ^:private preserve-target-render? (atom false))
|
||||
|
||||
(defn- drain-text-editor-events!
|
||||
"Pop and handle every pending text-editor event.
|
||||
|
||||
StylesChanged syncs the caret's current styles to the toolbar. Returns true
|
||||
when some event needs a full shape re-render (content or layout changed)."
|
||||
[]
|
||||
(loop [needs-render? false]
|
||||
(let [ev (text-editor/text-editor-poll-event)]
|
||||
(if (or (nil? ev) (= ev TEXT_EDITOR_EVENT_NONE))
|
||||
needs-render?
|
||||
(do
|
||||
(when (= ev TEXT_EDITOR_EVENT_STYLES_CHANGED)
|
||||
(let [current-styles (text-editor/text-editor-get-current-styles)
|
||||
shape-id (text-editor/text-editor-get-active-shape-id)]
|
||||
(st/emit! (texts/v3-update-text-editor-styles shape-id current-styles))))
|
||||
(recur (or needs-render?
|
||||
(= ev TEXT_EDITOR_EVENT_CONTENT_CHANGED)
|
||||
(= ev TEXT_EDITOR_EVENT_NEEDS_LAYOUT))))))))
|
||||
|
||||
;; This should never be called from the outside.
|
||||
(defn- render
|
||||
[timestamp]
|
||||
(when (and wasm/context-initialized? (not @wasm/context-lost?))
|
||||
(internal-render timestamp)
|
||||
;; SYNC-TILES makes WASM keep the last presented frame while the new tiles
|
||||
;; are rasterized, rather than clearing to the background first. The flag is
|
||||
;; one-shot on both sides: WASM clears it when the render loop starts, so
|
||||
;; the progressive continuation frames behave normally.
|
||||
(if (compare-and-set! preserve-target-render? true false)
|
||||
(internal-render timestamp (bit-or wasm/internal-frame-type RENDER-FLAG-SYNC-TILES))
|
||||
(internal-render timestamp))
|
||||
|
||||
;; Update text editor blink (so cursor toggles) using the same timestamp
|
||||
(try
|
||||
(when (is-text-editor-wasm-enabled @st/state)
|
||||
(text-editor/text-editor-update-blink timestamp)
|
||||
(text-editor/text-editor-render-overlay)
|
||||
;; Poll for editor events; if any event occurs, trigger a re-render
|
||||
(let [ev (text-editor/text-editor-poll-event)]
|
||||
(when (and ev (not= ev TEXT_EDITOR_EVENT_NONE))
|
||||
;; When StylesChanged, get the current styles.
|
||||
(case ev
|
||||
;; StylesChanged Event
|
||||
TEXT_EDITOR_EVENT_STYLES_CHANGED
|
||||
(let [current-styles (text-editor/text-editor-get-current-styles)
|
||||
shape-id (text-editor/text-editor-get-active-shape-id)]
|
||||
(st/emit! (texts/v3-update-text-editor-styles shape-id current-styles)))
|
||||
|
||||
;; Default case
|
||||
nil)
|
||||
|
||||
(request-render "text-editor-event"))))
|
||||
;; Drain editor events. Only content/layout changes need a full shape
|
||||
;; re-render; selection/style changes are already reflected by the
|
||||
;; overlay redrawn just above.
|
||||
(when (drain-text-editor-events!)
|
||||
(request-render-preserving-target "text-editor-content")))
|
||||
(catch :default e
|
||||
(js/console.error "text-editor overlay/update failed:" e)))
|
||||
|
||||
@ -459,19 +481,51 @@
|
||||
(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.
|
||||
(defn render-from-backbuffer!
|
||||
"Re-present the last fully rendered frame with the UI overlay (rulers,
|
||||
selection band) redrawn on top, reusing the crisp Backbuffer instead of
|
||||
rebuilding it from the scale-capped document atlas.
|
||||
|
||||
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."
|
||||
For UI-only updates that must not kick off a progressive shape re-render. The
|
||||
alternative — blitting shapes from the cached tile atlas (WASM
|
||||
`_render_from_cache`, what pan/zoom uses in `set-view-box`) — is fine at
|
||||
normal zoom, but on a zoomed-in view (>1000%) it is a heavy upscale that
|
||||
flashes crisp->blurry->crisp. Reusing the Backbuffer is pixel-identical at
|
||||
any zoom. Only valid on a stable viewbox; pan/zoom must keep using the atlas
|
||||
blit, whose preview tracks the moving viewport."
|
||||
[]
|
||||
(when (and wasm/context-initialized? (not @wasm/context-lost?))
|
||||
(h/call wasm/internal-module "_render_from_cache" 0)))
|
||||
(h/call wasm/internal-module "_render_from_backbuffer")))
|
||||
|
||||
(defn render-text-editor-overlay!
|
||||
"Repaint the text-editor caret and selection over the last fully rendered
|
||||
frame, without a full (tile-rebuilding) render.
|
||||
|
||||
Caret/selection changes — the blink, clicks, drag-selection, arrow-key
|
||||
navigation — never alter the shapes. `text-editor-render-caret` re-composes
|
||||
the frame from the Backbuffer (which still holds the last complete render)
|
||||
and draws the caret/selection overlay on top, so the shapes are pixel
|
||||
identical to the last full render at any zoom level. A full `request-render`
|
||||
would instead blank the canvas and re-rasterize tiles progressively, which
|
||||
flashes on zoomed-in views; blitting from the cached tile atlas
|
||||
(`_render_from_cache`) would instead look softer when upscaled (the atlas is
|
||||
scale-capped), so the blink would alternate crisp/soft — a subtler flash.
|
||||
Pending editor events are still drained so the style toolbar stays in sync; a
|
||||
full render is only requested when content or layout actually changed."
|
||||
[]
|
||||
(when (and wasm/context-initialized?
|
||||
(not @wasm/context-lost?)
|
||||
;; Skip when a render is already pending: `text-editor-render-caret`
|
||||
;; composes from the Backbuffer, but a progressive render (multiple
|
||||
;; rAF frames at high zoom) is still rebuilding it. Compositing then
|
||||
;; would show a half-built frame — a sparse, timing-dependent flash.
|
||||
;; The in-flight render draws the overlay itself when it completes.
|
||||
(not (render-pending?)))
|
||||
(when (is-text-editor-wasm-enabled @st/state)
|
||||
(text-editor/text-editor-update-blink (js/performance.now))
|
||||
(text-editor/text-editor-render-caret)
|
||||
(when (drain-text-editor-events!)
|
||||
(request-render-preserving-target "text-editor-content")))))
|
||||
|
||||
;; CSS-pixel blur radius for the page-transition snapshot (DPR-scaled in WASM).
|
||||
(def ^:private TRANSITION_BLUR_RADIUS 4.0)
|
||||
@ -512,6 +566,12 @@
|
||||
(defonce shapes-loading? (atom false))
|
||||
(defonce deferred-render? (atom false))
|
||||
|
||||
(defn render-pending?
|
||||
"True while a render has been scheduled but not yet completed — including the
|
||||
frames of an in-progress progressive render."
|
||||
[]
|
||||
@pending-render)
|
||||
|
||||
(defn- register-deferred-render!
|
||||
[]
|
||||
(reset! deferred-render? true))
|
||||
@ -538,6 +598,23 @@
|
||||
(throw e)))))]
|
||||
(set! wasm/internal-frame-id frame-id))))))
|
||||
|
||||
(defn request-render-preserving-target
|
||||
"Like `request-render`, but keeps the previously presented frame on screen
|
||||
while the new tiles are rasterized instead of blanking the canvas first.
|
||||
|
||||
A plain `request-render` goes through WASM's `reset_canvas`, which clears to
|
||||
the background colour and then fills the viewport tile by tile. When that
|
||||
rasterization spans more than one frame — as it does on zoomed-in views,
|
||||
where glyphs are expensive to raster — the cleared canvas is visible as a
|
||||
flash. Preserving the target is what the renderer already does after a
|
||||
pan/zoom gesture for exactly this reason.
|
||||
|
||||
Use for shape edits on a stable viewbox (typing in the text editor), where
|
||||
the previous frame is a good stand-in until the new one is ready."
|
||||
[requester]
|
||||
(reset! preserve-target-render? true)
|
||||
(request-render requester))
|
||||
|
||||
(defn- begin-shapes-loading!
|
||||
[]
|
||||
(reset! shapes-loading? true)
|
||||
@ -1271,7 +1348,16 @@
|
||||
"Ends the view interaction and triggers a full-quality render."
|
||||
[]
|
||||
(view-interaction-end!)
|
||||
(internal-render 0 0))
|
||||
;; Preserve the last presented frame while the new one renders. A plain render
|
||||
;; goes through WASM `reset_canvas`, which clears to the background and
|
||||
;; re-rasterizes the viewport tile by tile — a visible flash on zoomed-in
|
||||
;; views. The content is unchanged across a view-interaction end (only the
|
||||
;; view moved), so there is nothing to clear; SYNC-TILES sets `preserve_target`
|
||||
;; and lets the new tiles replace the old frame in place. Zoom-end already did
|
||||
;; this implicitly (`zoom_changed`); this extends it to pan/resize-triggered
|
||||
;; ends (e.g. selecting a shape opens the options panel and resizes the
|
||||
;; viewport), which previously blanked.
|
||||
(internal-render 0 RENDER-FLAG-SYNC-TILES))
|
||||
|
||||
(def render-finish
|
||||
(letfn [(do-render []
|
||||
|
||||
@ -206,6 +206,14 @@
|
||||
(when wasm/context-initialized?
|
||||
(h/call wasm/internal-module "_text_editor_render_overlay")))
|
||||
|
||||
(defn text-editor-render-caret
|
||||
"Re-compose the frame from the Backbuffer (the last full render) and draw the
|
||||
caret/selection overlay on top, submitting one atomic frame. Pixel identical
|
||||
to the last full render at any zoom, so the blink does not flash."
|
||||
[]
|
||||
(when wasm/context-initialized?
|
||||
(h/call wasm/internal-module "_text_editor_render_caret")))
|
||||
|
||||
(defn text-editor-poll-event
|
||||
[]
|
||||
(when wasm/context-initialized?
|
||||
|
||||
@ -236,6 +236,23 @@ pub extern "C" fn render_from_cache(_: i32) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn render_from_backbuffer() -> Result<()> {
|
||||
with_state!(state, {
|
||||
// Re-present the last fully rendered frame from the Backbuffer with the
|
||||
// UI overlay (rulers) redrawn on top. Unlike `render_from_cache`, it does
|
||||
// NOT rebuild the Backbuffer from the document tile atlas — that atlas is
|
||||
// capped at scale <= 1.0, so on a zoomed-in view its blit is a blurry
|
||||
// upscale and swapping it in reads as a flash. Reusing the Backbuffer is
|
||||
// pixel-identical at any zoom. Only valid on a stable viewbox (the
|
||||
// Backbuffer must still match the current view); pan/zoom keeps using
|
||||
// `render_from_cache`.
|
||||
state.present_frame();
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_preview_mode(enabled: bool) -> Result<()> {
|
||||
|
||||
@ -942,6 +942,18 @@ impl RenderState {
|
||||
/// on top of Target, then present. Backbuffer is left clean so it can be reused
|
||||
/// as-is across interactive-transform frames without stale overlay pixels.
|
||||
pub fn present_frame(&mut self, tree: ShapesPoolRef) {
|
||||
self.compose_frame(tree);
|
||||
self.surfaces.flush_and_submit(SurfaceId::Target);
|
||||
}
|
||||
|
||||
/// Compose the frame on Target — the already-rendered Backbuffer plus the
|
||||
/// UI/debug overlays — *without* submitting it, so a caller can draw extra
|
||||
/// overlays into the same frame and present them atomically.
|
||||
///
|
||||
/// Unlike `render_from_cache`, this reuses the Backbuffer as-is instead of
|
||||
/// rebuilding it from the document atlas, so the result is pixel-identical
|
||||
/// to the last full render at any zoom level.
|
||||
pub fn compose_frame(&mut self, tree: ShapesPoolRef) {
|
||||
// Viewer masked passes render a partial scene onto a transparent backbuffer.
|
||||
// SrcOver would keep pass-1 pixels wherever the backbuffer stays transparent.
|
||||
if self.viewer_masked_pass() {
|
||||
@ -959,7 +971,6 @@ impl RenderState {
|
||||
ui::render(self, tree);
|
||||
}
|
||||
debug::render_wasm_label(self);
|
||||
self.surfaces.flush_and_submit(SurfaceId::Target);
|
||||
}
|
||||
|
||||
/// Renders only the canvas background and UI surface (rulers/frame), without
|
||||
|
||||
@ -26,10 +26,11 @@ pub fn render_overlay(
|
||||
canvas.translate((-viewbox.area.left, -viewbox.area.top));
|
||||
|
||||
if editor_state.selection.is_selection() {
|
||||
// With an active selection there is no blinking caret (the caret is one
|
||||
// end of the selection); drawing it would make it toggle on top of the
|
||||
// highlight while the selection is held.
|
||||
render_selection(canvas, editor_state, text_content, shape);
|
||||
}
|
||||
|
||||
if editor_state.cursor_visible {
|
||||
} else if editor_state.cursor_visible {
|
||||
render_cursor(canvas, zoom, options.dpr, editor_state, text_content, shape);
|
||||
}
|
||||
|
||||
|
||||
@ -68,6 +68,10 @@ impl State {
|
||||
get_render_state().render_from_cache(&self.shapes);
|
||||
}
|
||||
|
||||
pub fn present_frame(&mut self) {
|
||||
get_render_state().present_frame(&self.shapes);
|
||||
}
|
||||
|
||||
pub fn render_ui_only(&mut self) {
|
||||
get_render_state().render_ui_only(&self.shapes);
|
||||
}
|
||||
|
||||
@ -535,11 +535,17 @@ impl TextEditorState {
|
||||
|
||||
pub fn set_caret_from_position(&mut self, position: &TextPositionWithAffinity) {
|
||||
self.selection.set_caret(*position);
|
||||
// Restart the blink so the caret is solid right after it is placed,
|
||||
// instead of keeping whatever phase it had (which can toggle off at the
|
||||
// moment of the click and read as a flash). Mirrors the keyboard paths
|
||||
// (`move_cursor`, `select_all`) which already reset the blink.
|
||||
self.reset_blink();
|
||||
self.push_event(TextEditorEvent::SelectionChanged);
|
||||
}
|
||||
|
||||
pub fn extend_selection_from_position(&mut self, position: &TextPositionWithAffinity) {
|
||||
self.selection.extend_to(*position);
|
||||
self.reset_blink();
|
||||
self.push_event(TextEditorEvent::SelectionChanged);
|
||||
}
|
||||
|
||||
|
||||
@ -6,9 +6,10 @@ use crate::mem;
|
||||
use crate::render::text_editor as text_editor_render;
|
||||
use crate::render::SurfaceId;
|
||||
use crate::shapes::{Shape, TextAlign, TextContent, TextPositionWithAffinity, Type, VerticalAlign};
|
||||
use crate::state::{TextEditorEvent, TextSelection};
|
||||
use crate::state::{State, TextEditorEvent, TextSelection};
|
||||
use crate::utils::uuid_from_u32_quartet;
|
||||
use crate::utils::uuid_to_u32_quartet;
|
||||
use crate::uuid::Uuid;
|
||||
use crate::wasm::fills::RawFillData;
|
||||
use crate::wasm::text::{
|
||||
helpers as text_helpers, RawTextAlign, RawTextDecoration, RawTextDirection, RawTextTransform,
|
||||
@ -849,6 +850,63 @@ pub extern "C" fn text_editor_update_blink(timestamp_ms: f32) {
|
||||
get_text_editor_state().update_blink(timestamp_ms);
|
||||
}
|
||||
|
||||
/// Refresh a text shape's layout if the editor marked it dirty, so the
|
||||
/// caret/selection overlay is measured against up-to-date glyph geometry.
|
||||
fn update_text_layout_if_needed(state: &mut State, shape_id: Uuid) {
|
||||
let Some(shape) = state.shapes.get_mut(&shape_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let selrect = shape.selrect();
|
||||
|
||||
let Type::Text(text_content) = &mut shape.shape_type else {
|
||||
return;
|
||||
};
|
||||
|
||||
if text_content.needs_update_layout() {
|
||||
text_content.update_layout(selrect);
|
||||
}
|
||||
}
|
||||
|
||||
/// Repaint the caret/selection over the last fully rendered frame.
|
||||
///
|
||||
/// Re-composes Target from the Backbuffer (which still holds the last complete
|
||||
/// render) and draws the editor overlay on top, in a single submitted frame.
|
||||
///
|
||||
/// This exists because the caret blink must erase the previous caret, which
|
||||
/// means restoring the pixels underneath it. Doing that via `render_from_cache`
|
||||
/// rebuilds the frame from the document atlas, and that atlas is capped at
|
||||
/// scale <= 1.0 — on a zoomed-in view it gets blitted heavily upscaled, so the
|
||||
/// blink alternates between the crisp render and a softer approximation, which
|
||||
/// reads as a flash. Reusing the Backbuffer is pixel-identical at any zoom.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn text_editor_render_caret() {
|
||||
with_state!(state, {
|
||||
let Some(shape_id) = get_text_editor_state().active_shape_id else {
|
||||
return;
|
||||
};
|
||||
|
||||
update_text_layout_if_needed(state, shape_id);
|
||||
|
||||
let Some(shape) = state.shapes.get(&shape_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
get_render_state().compose_frame(&state.shapes);
|
||||
|
||||
let canvas = get_render_state().surfaces.canvas(SurfaceId::Target);
|
||||
let viewbox = get_render_state().viewbox;
|
||||
text_editor_render::render_overlay(
|
||||
canvas,
|
||||
&viewbox,
|
||||
&get_render_state().options,
|
||||
get_text_editor_state(),
|
||||
shape,
|
||||
);
|
||||
get_render_state().flush_and_submit();
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn text_editor_render_overlay() {
|
||||
with_state!(state, {
|
||||
@ -856,18 +914,7 @@ pub extern "C" fn text_editor_render_overlay() {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(shape) = state.shapes.get(&shape_id) {
|
||||
if let Type::Text(text_content) = &shape.shape_type {
|
||||
if text_content.needs_update_layout() {
|
||||
let selrect = shape.selrect();
|
||||
if let Some(shape) = state.shapes.get_mut(&shape_id) {
|
||||
if let Type::Text(text_content) = &mut shape.shape_type {
|
||||
text_content.update_layout(selrect);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
update_text_layout_if_needed(state, shape_id);
|
||||
|
||||
let Some(shape) = state.shapes.get(&shape_id) else {
|
||||
return;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user