mirror of
https://github.com/penpot/penpot.git
synced 2026-09-08 13:09:22 +00:00
🐛 Fix text selection render overlay and double/triple click flash (#11512)
This commit is contained in:
parent
86aebd3429
commit
0533be100d
@ -159,6 +159,10 @@
|
||||
(or (.-isComposing native)
|
||||
(= 229 (.-keyCode event)))))
|
||||
|
||||
(defn- double-click?
|
||||
[^js native-event]
|
||||
(= (.-detail native-event) 2))
|
||||
|
||||
(defn- triple-click?
|
||||
[^js native-event]
|
||||
(>= (.-detail native-event) 3))
|
||||
@ -194,6 +198,8 @@
|
||||
;; WASM `is_pointer_selection_active` guard), not on every hover move.
|
||||
dragging-ref (mf/use-ref false)
|
||||
|
||||
deferred-press-ref (mf/use-ref nil)
|
||||
|
||||
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)
|
||||
@ -459,23 +465,29 @@
|
||||
(mf/use-fn
|
||||
(fn [^js event]
|
||||
(let [native-event (dom/event->native-event event)
|
||||
off-pt (dom/get-offset-position native-event)]
|
||||
off-pt (dom/get-offset-position native-event)]
|
||||
;; Repositioning the caret abandons the pending caret style (also
|
||||
;; covers click and double-click, which fire pointer-down first).
|
||||
(text-editor/clear-pending-caret-styles!)
|
||||
(mf/set-ref-val! dragging-ref true)
|
||||
(if (.-shiftKey event)
|
||||
(wasm.api/text-editor-pointer-down-extend off-pt)
|
||||
(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!))))
|
||||
(do
|
||||
(mf/set-ref-val! dragging-ref true)
|
||||
(wasm.api/text-editor-pointer-down-extend off-pt)
|
||||
;; Repaint the caret over the cached tiles instead of a full
|
||||
;; render, which flashes at high zoom.
|
||||
(wasm.api/render-text-editor-overlay!))
|
||||
(mf/set-ref-val! deferred-press-ref off-pt)))))
|
||||
|
||||
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)]
|
||||
off-pt (dom/get-offset-position native-event)]
|
||||
(when-let [pressed-pt (and (pos? (.-buttons native-event))
|
||||
(mf/ref-val deferred-press-ref))]
|
||||
(mf/set-ref-val! deferred-press-ref nil)
|
||||
(mf/set-ref-val! dragging-ref true)
|
||||
(wasm.api/text-editor-pointer-down pressed-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.
|
||||
@ -486,20 +498,37 @@
|
||||
(mf/use-fn
|
||||
(fn [^js event]
|
||||
(let [native-event (dom/event->native-event event)
|
||||
off-pt (dom/get-offset-position native-event)]
|
||||
off-pt (dom/get-offset-position native-event)
|
||||
dragging? (mf/ref-val dragging-ref)]
|
||||
(mf/set-ref-val! dragging-ref false)
|
||||
(mf/set-ref-val! deferred-press-ref nil)
|
||||
(wasm.api/text-editor-pointer-up off-pt)
|
||||
(wasm.api/render-text-editor-overlay!))))
|
||||
;; Without a drag there is no pointer selection to close; the
|
||||
;; caret is placed by `on-click`.
|
||||
(when dragging?
|
||||
(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)]
|
||||
(if (triple-click? native-event)
|
||||
(wasm.api/text-editor-select-paragraph off-pt)
|
||||
(wasm.api/text-editor-set-cursor-from-offset off-pt))
|
||||
(wasm.api/render-text-editor-overlay!))))
|
||||
off-pt (dom/get-offset-position native-event)]
|
||||
(cond
|
||||
(triple-click? native-event)
|
||||
(do
|
||||
(wasm.api/text-editor-select-paragraph off-pt)
|
||||
(wasm.api/render-text-editor-overlay!))
|
||||
|
||||
;; `dblclick` selects the word right after. Shift+click still goes
|
||||
;; through: WASM consumes its skip-click flag there.
|
||||
(and (double-click? native-event)
|
||||
(not (.-shiftKey event)))
|
||||
nil
|
||||
|
||||
:else
|
||||
(do
|
||||
(wasm.api/text-editor-set-cursor-from-offset off-pt)
|
||||
(wasm.api/render-text-editor-overlay!))))))
|
||||
|
||||
on-double-click
|
||||
(mf/use-fn
|
||||
|
||||
@ -397,11 +397,6 @@
|
||||
(or (= wasm/internal-frame-type FRAME_TYPE_PARTIAL)
|
||||
(= wasm/internal-frame-type FRAME_TYPE_VIEWPORT_READY)))
|
||||
|
||||
(defn- frame-presented-target?
|
||||
"True when this frame recomposited Target (full or early viewport present)."
|
||||
[]
|
||||
(not= wasm/internal-frame-type FRAME_TYPE_PARTIAL))
|
||||
|
||||
(def ^:const RENDER-FLAG-SYNC-TILES 4) ;; Rebuild tile index without ending fast mode (pan/zoom pause).
|
||||
|
||||
(defn- internal-render
|
||||
@ -507,18 +502,8 @@
|
||||
(try
|
||||
(when (is-text-editor-wasm-enabled @st/state)
|
||||
(text-editor/text-editor-update-blink timestamp)
|
||||
;; Only repaint the overlay when this frame recomposited Target (a full
|
||||
;; frame or early viewport present). A partial frame is flushed but not
|
||||
;; presented - Target still shows the last presented frame with the
|
||||
;; overlay already on it - so repainting the translucent selection over
|
||||
;; it stacks another layer every progressive frame: it darkens, then
|
||||
;; snaps back when the final frame presents from the clean Backbuffer
|
||||
;; (the blink at the end of a zoom over a selection, gh-10709).
|
||||
(when (frame-presented-target?)
|
||||
(text-editor/text-editor-render-overlay))
|
||||
;; 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.
|
||||
;; The editor overlay is painted by the WASM frame composition; only
|
||||
;; content/layout changes need a full shape re-render here.
|
||||
(when (drain-text-editor-events!)
|
||||
(request-render-preserving-target "text-editor-content")))
|
||||
(catch :default e
|
||||
@ -1421,28 +1406,6 @@
|
||||
(let [local (get @st/state :workspace-local)]
|
||||
(or (:panning local) (:zooming local))))
|
||||
|
||||
(defn- render-text-editor-overlay-if-active!
|
||||
"Redraw the editor caret/selection straight onto the current Target frame when
|
||||
an editor is active (no-op otherwise). Used after the direct `_render_from_cache`
|
||||
/ `internal-render` calls of a view interaction, which bypass the rAF `render`
|
||||
loop that normally repaints the overlay. Without it the selection blinks out
|
||||
for the duration of a pan/zoom gesture over a text shape (gh-10709)."
|
||||
[]
|
||||
(when (is-text-editor-wasm-enabled @st/state)
|
||||
(text-editor/text-editor-render-overlay)))
|
||||
|
||||
(defn- render-text-editor-overlay-after-frame!
|
||||
"Repaint the overlay after a direct `internal-render`, but only when that
|
||||
render recomposited Target (a full frame or early viewport present). A partial
|
||||
frame is only flushed - Target keeps the last presented frame with the overlay
|
||||
already on it - so repainting the translucent selection then stacks another
|
||||
layer and it visibly darkens across the progressive frames before snapping
|
||||
back on the final present (the blink at the end of a zoom over a selection,
|
||||
gh-10709). The final full frame's own repaint keeps the overlay in place."
|
||||
[]
|
||||
(when (frame-presented-target?)
|
||||
(render-text-editor-overlay-if-active!)))
|
||||
|
||||
(defn finalize-view-interaction!
|
||||
"Ends an in-progress pan/zoom view interaction and triggers a full-quality
|
||||
render. No-ops when no view interaction is active.
|
||||
@ -1461,12 +1424,7 @@
|
||||
;; 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 (js/performance.now) RENDER-FLAG-SYNC-TILES)
|
||||
;; The direct render above bypasses the rAF `render` loop, so repaint the
|
||||
;; editor overlay explicitly. Only when this was a full frame: a progressive
|
||||
;; render keeps painting through the rAF loop and its partial frames must not
|
||||
;; be over-stamped (see `render-text-editor-overlay-after-frame!`).
|
||||
(render-text-editor-overlay-after-frame!)))
|
||||
(internal-render (js/performance.now) RENDER-FLAG-SYNC-TILES)))
|
||||
|
||||
(def render-finish
|
||||
(letfn [(do-render []
|
||||
@ -1475,9 +1433,7 @@
|
||||
(when (initialized?)
|
||||
(if (view-gesture-active?)
|
||||
;; Pan/zoom pause: render without ending the interaction.
|
||||
(do
|
||||
(internal-render (js/performance.now) RENDER-FLAG-SYNC-TILES)
|
||||
(render-text-editor-overlay-after-frame!))
|
||||
(internal-render (js/performance.now) RENDER-FLAG-SYNC-TILES)
|
||||
(finalize-view-interaction!))))]
|
||||
(fns/debounce do-render DEBOUNCE_DELAY_MS)))
|
||||
|
||||
@ -1495,15 +1451,6 @@
|
||||
|
||||
(perf/begin-measure "render-from-cache")
|
||||
(h/call wasm/internal-module "_render_from_cache" 0)
|
||||
;; Keep the text-editor caret/selection glued to the shapes while the view
|
||||
;; changes. `_render_from_cache` re-composites shapes + UI at the new viewbox
|
||||
;; but omits the editor overlay, so without this the selection would vanish for
|
||||
;; the whole pan/zoom gesture and only flash back when the debounced full
|
||||
;; render lands — the blink seen when zooming in/out over a selection at high
|
||||
;; zoom (gh-10709). `_text_editor_render_overlay` draws straight onto the
|
||||
;; freshly composited Target (no Backbuffer re-compose) and no-ops when no
|
||||
;; editor is active.
|
||||
(render-text-editor-overlay-if-active!)
|
||||
(render-finish)
|
||||
(perf/end-measure "render-from-cache"))))
|
||||
|
||||
|
||||
@ -245,11 +245,6 @@
|
||||
(when (wasm/ready?)
|
||||
(h/call wasm/internal-module "_text_editor_update_blink" timestamp-ms)))
|
||||
|
||||
(defn text-editor-render-overlay
|
||||
[]
|
||||
(when (wasm/ready?)
|
||||
(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
|
||||
|
||||
@ -108,7 +108,7 @@ flowchart TB
|
||||
FFI_Cursor["_text_editor_set_cursor_from_point<br/>_text_editor_move_cursor<br/>_text_editor_select_all"]
|
||||
FFI_Edit["_text_editor_insert_text<br/>_text_editor_delete_backward<br/>_text_editor_insert_paragraph"]
|
||||
FFI_Query["_text_editor_export_content<br/>_text_editor_get_selection<br/>_text_editor_poll_event"]
|
||||
FFI_Render["_text_editor_render_overlay<br/>_text_editor_update_blink"]
|
||||
FFI_Render["_text_editor_render_caret<br/>_text_editor_update_blink"]
|
||||
end
|
||||
|
||||
subgraph Rust["Rust Layer"]
|
||||
|
||||
@ -27,6 +27,7 @@ use options::RenderOptions;
|
||||
pub use surfaces::{SurfaceId, Surfaces};
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::globals::get_text_editor_state;
|
||||
use crate::math;
|
||||
use crate::shapes::{
|
||||
all_with_ancestors, modifier_changes_text_layout, radius_to_sigma, Blur, BlurType, Corners,
|
||||
@ -985,11 +986,34 @@ impl RenderState {
|
||||
debug::render(self);
|
||||
}
|
||||
if !self.preview_mode {
|
||||
self.render_text_editor_overlay(tree);
|
||||
ui::render(self, tree);
|
||||
}
|
||||
debug::render_wasm_label(self);
|
||||
}
|
||||
|
||||
/// Drawn on Target before the UI surface is composited, so rulers and guides
|
||||
/// stay above the selection band
|
||||
fn render_text_editor_overlay(&mut self, tree: ShapesPoolRef) {
|
||||
let editor_state = get_text_editor_state();
|
||||
let Some(shape_id) = editor_state.active_shape_id else {
|
||||
return;
|
||||
};
|
||||
let Some(shape) = tree.get(&shape_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let viewbox = self.viewbox;
|
||||
let options = self.options;
|
||||
text_editor::render_overlay(
|
||||
self.surfaces.canvas(SurfaceId::Target),
|
||||
&viewbox,
|
||||
&options,
|
||||
editor_state,
|
||||
shape,
|
||||
);
|
||||
}
|
||||
|
||||
/// Renders only the canvas background and UI surface (rulers/frame), without
|
||||
/// rebuilding or drawing any shape tiles. Used to show the viewport frame
|
||||
/// immediately before shape tiles are built (e.g., right after a DPR change).
|
||||
|
||||
@ -3,8 +3,6 @@ use macros::{wasm_error, ToJs};
|
||||
use crate::globals::{get_render_state, get_text_editor_state};
|
||||
use crate::math::{Matrix, Point};
|
||||
use crate::mem;
|
||||
use crate::render::text_editor as text_editor_render;
|
||||
use crate::render::SurfaceId;
|
||||
use crate::shapes::{TextAlign, TextContent, TextPositionWithAffinity, Type, VerticalAlign};
|
||||
use crate::state::{State, TextEditorEvent, TextEditorState};
|
||||
use crate::utils::uuid_from_u32_quartet;
|
||||
@ -851,7 +849,7 @@ fn update_text_layout_if_needed(state: &mut State, shape_id: Uuid) {
|
||||
/// 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.
|
||||
/// render); the compose step draws the editor overlay itself.
|
||||
///
|
||||
/// This exists because the caret blink must erase the previous caret, which
|
||||
/// means restoring the pixels underneath it. Doing that via `render_from_cache`
|
||||
@ -867,49 +865,7 @@ pub extern "C" fn text_editor_render_caret() {
|
||||
};
|
||||
|
||||
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, {
|
||||
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;
|
||||
};
|
||||
|
||||
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();
|
||||
get_render_state().present_frame(&state.shapes);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user