mirror of
https://github.com/penpot/penpot.git
synced 2026-09-10 04:38:51 -04:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2aa00f0cb | ||
|
|
6622a4e9d4 | ||
|
|
69581755b4 | ||
|
|
113156592a | ||
|
|
875171ee86 | ||
|
|
d0465e3601 |
No files matched your search
@@ -257,6 +257,10 @@
|
||||
|
||||
;; True when we are opening a new file or switching to a new page
|
||||
page-transition? (mf/deref wasm.api/page-transition?)
|
||||
;; True during pan/zoom (set by `set-view-box`). Used to suppress heavy
|
||||
;; overlays that would otherwise re-render + recompute geometry on every
|
||||
;; viewbox change of the gesture.
|
||||
view-interaction? (mf/deref wasm.api/view-interaction-active?)
|
||||
context-loss-overlay? (mf/deref wasm.api/context-loss-overlay?)
|
||||
transition-reveal-rulers? (mf/deref wasm.api/transition-reveal-rulers?)
|
||||
|
||||
@@ -289,7 +293,7 @@
|
||||
show-cursor-tooltip? tooltip
|
||||
show-draw-area? drawing-obj
|
||||
show-gradient-handlers? (= (count selected) 1)
|
||||
show-grids? (and (contains? layout :display-guides) (not page-transition?))
|
||||
show-grids? (and (contains? layout :display-guides) (not page-transition?) (not view-interaction?))
|
||||
|
||||
show-frame-outline? (and (= transform :move) (not panning) (not page-transition?))
|
||||
show-outlines? (and (nil? transform)
|
||||
@@ -325,7 +329,8 @@
|
||||
show-measures? (and (not transform)
|
||||
(not path-editing?)
|
||||
(or show-distances? mode-inspect? read-only?)
|
||||
(not page-transition?))
|
||||
(not page-transition?)
|
||||
(not view-interaction?))
|
||||
show-artboard-names? (and (contains? layout :display-artboard-names) (not page-transition?))
|
||||
hide-ui? (contains? layout :hide-ui)
|
||||
|
||||
@@ -360,6 +365,7 @@
|
||||
|
||||
show-padding?
|
||||
(and (nil? transform)
|
||||
(not view-interaction?)
|
||||
single-select?
|
||||
(= (:type first-shape) :frame)
|
||||
(= (:layout first-shape) :flex)
|
||||
@@ -367,6 +373,7 @@
|
||||
|
||||
show-margin?
|
||||
(and (nil? transform)
|
||||
(not view-interaction?)
|
||||
single-select?
|
||||
(= (:layout selected-frame) :flex)
|
||||
(zero? (:rotation first-shape)))
|
||||
|
||||
@@ -86,10 +86,6 @@
|
||||
(defonce transition-image-url* (atom nil))
|
||||
(defonce transition-epoch* (atom 0))
|
||||
(defonce transition-tiles-handler* (atom nil))
|
||||
(defonce snapshot-tiles-handler* (atom nil))
|
||||
|
||||
(def ^:private snapshot-capture-debounce-ms 250)
|
||||
|
||||
|
||||
(defn initialized?
|
||||
"True when the WASM render context is ready to receive design-state
|
||||
@@ -173,31 +169,6 @@
|
||||
(f))
|
||||
#js {:once true}))
|
||||
|
||||
(defonce ^:private schedule-canvas-snapshot-capture!
|
||||
(fns/debounce
|
||||
(fn []
|
||||
(when (and (initialized?)
|
||||
(some? wasm/canvas))
|
||||
(-> (webgl/capture-canvas-snapshot-url)
|
||||
(p/catch (fn [_] nil)))))
|
||||
snapshot-capture-debounce-ms))
|
||||
|
||||
(defn- start-canvas-snapshot-listener!
|
||||
[]
|
||||
(when-let [prev @snapshot-tiles-handler*]
|
||||
(.removeEventListener ^js ug/document "penpot:wasm:tiles-complete" prev))
|
||||
(let [handler (fn [_] (schedule-canvas-snapshot-capture!))]
|
||||
(reset! snapshot-tiles-handler* handler)
|
||||
(.addEventListener ^js ug/document "penpot:wasm:tiles-complete" handler)))
|
||||
|
||||
(defn- stop-canvas-snapshot-listener!
|
||||
[]
|
||||
(when-let [prev @snapshot-tiles-handler*]
|
||||
(.removeEventListener ^js ug/document "penpot:wasm:tiles-complete" prev))
|
||||
(reset! snapshot-tiles-handler* nil)
|
||||
(when-let [cancel (unchecked-get schedule-canvas-snapshot-capture! "cancel")]
|
||||
(cancel)))
|
||||
|
||||
(defn text-editor-wasm?
|
||||
[]
|
||||
(or (contains? cf/flags :feature-text-editor-wasm)
|
||||
@@ -224,7 +195,11 @@
|
||||
|
||||
(def ^:const DEBOUNCE_DELAY_MS 100)
|
||||
|
||||
(defonce ^:private view-interaction-active? (atom false))
|
||||
;; Public so the viewport can gate heavy overlays (flex controls, frame grid,
|
||||
;; measurements) during pan/zoom — they otherwise re-render + recompute geometry
|
||||
;; on every viewbox change. Set by `set-view-box` (pan AND zoom), cleared on
|
||||
;; interaction end.
|
||||
(defonce view-interaction-active? (atom false))
|
||||
|
||||
;; Time budget (ms) per chunk of shape processing before yielding to browser
|
||||
(def ^:private ^:const CHUNK_TIME_BUDGET_MS 8)
|
||||
@@ -270,7 +245,6 @@
|
||||
(def noop-fn
|
||||
(constantly nil))
|
||||
|
||||
;;
|
||||
(def shape-wrapper-factory nil)
|
||||
|
||||
(let [^js ch (js/MessageChannel.)]
|
||||
@@ -1270,17 +1244,34 @@
|
||||
(internal-render)))]
|
||||
(fns/debounce do-render DEBOUNCE_DELAY_MS)))
|
||||
|
||||
(defonce ^:private pending-cache-render? (atom false))
|
||||
|
||||
(defn- flush-cache-render!
|
||||
[]
|
||||
(reset! pending-cache-render? false)
|
||||
(when (initialized?)
|
||||
(perf/begin-measure "render-from-cache")
|
||||
(h/call wasm/internal-module "_render_from_cache" 0)
|
||||
(render-finish)
|
||||
(perf/end-measure "render-from-cache")))
|
||||
|
||||
(defn set-view-box
|
||||
[zoom vbox]
|
||||
(perf/begin-measure "set-view-box")
|
||||
(view-interaction-start!)
|
||||
;; `_set_view` is cheap and must run per event so the renderer always has the
|
||||
;; latest viewbox.
|
||||
(h/call wasm/internal-module "_set_view" zoom (- (:x vbox)) (- (:y vbox)))
|
||||
(perf/end-measure "set-view-box")
|
||||
|
||||
(perf/begin-measure "render-from-cache")
|
||||
(h/call wasm/internal-module "_render_from_cache" 0)
|
||||
(render-finish)
|
||||
(perf/end-measure "render-from-cache"))
|
||||
;; Coalesce the cache-preview render to one per animation frame. Wheel/trackpad
|
||||
;; momentum (and any batched viewport updates) fire many `set-view-box` calls
|
||||
;; per frame; calling `_render_from_cache` synchronously on each piled up to
|
||||
;; ~17 renders × tens-to-hundreds of ms in a single RAF (1.5s handlers that
|
||||
;; froze input). One coalesced render per frame uses the latest viewbox.
|
||||
(when-not @pending-cache-render?
|
||||
(reset! pending-cache-render? true)
|
||||
(timers/raf (fn [_] (flush-cache-render!)))))
|
||||
|
||||
(defn sync-workspace-local-viewport!
|
||||
"Pushes `[:workspace-local :zoom]` and `:vbox` into WASM."
|
||||
@@ -1981,7 +1972,6 @@
|
||||
(set! wasm/canvas canvas)
|
||||
(.addEventListener canvas "webglcontextlost" on-webgl-context-lost)
|
||||
(.addEventListener canvas "webglcontextrestored" on-webgl-context-restored)
|
||||
(start-canvas-snapshot-listener!)
|
||||
(reset! wasm/context-lost? false)
|
||||
(set! wasm/context-initialized? true)))
|
||||
|
||||
@@ -2008,7 +1998,6 @@
|
||||
(when wasm/canvas
|
||||
(.removeEventListener wasm/canvas "webglcontextlost" on-webgl-context-lost)
|
||||
(.removeEventListener wasm/canvas "webglcontextrestored" on-webgl-context-restored))
|
||||
(stop-canvas-snapshot-listener!)
|
||||
|
||||
(when (wasm/module-ready?)
|
||||
(free-gpu-resources)
|
||||
@@ -2083,8 +2072,10 @@
|
||||
(wasm-gesture/reset-after-wasm-reload!))))
|
||||
;; Ensure render surfaces are blank before replay to avoid overpainting.
|
||||
(p/then (fn [_] (h/call wasm/internal-module "_reset_canvas")))
|
||||
(p/then (fn [_] (replay-font-resources! fonts)))
|
||||
(p/then (fn [_] (replay-image-resources! image-resources)))
|
||||
(p/then (fn [_]
|
||||
(replay-font-resources! fonts)))
|
||||
(p/then (fn [_]
|
||||
(replay-image-resources! image-resources)))
|
||||
(p/then
|
||||
(fn []
|
||||
(initialize-viewport base-objects zoom vbox
|
||||
|
||||
@@ -134,11 +134,24 @@ void main() {
|
||||
(.bindTexture ^js gl (.-TEXTURE_2D ^js gl) nil)
|
||||
(.deleteTexture ^js gl texture))))
|
||||
|
||||
(defn capture-canvas-snapshot-url
|
||||
"Captures the current viewport canvas as a PNG `blob:` URL and stores it in
|
||||
`wasm/canvas-snapshot-url`.
|
||||
;; Codec for the transition snapshot. The snapshot is only ever shown as a
|
||||
;; heavily-blurred overlay (see TRANSITION_BLUR_RADIUS), so a lossy, alpha-capable
|
||||
;; codec is fine and encodes much faster than lossless PNG -- on a full-viewport
|
||||
;; canvas, PNG `toBlob` of millions of pixels costs ~1s+ on the main thread.
|
||||
;; WebP keeps the alpha channel (unlike JPEG) and encodes in a fraction of that.
|
||||
(def ^:private SNAPSHOT_MIME "image/webp")
|
||||
(def ^:private SNAPSHOT_QUALITY 0.6)
|
||||
|
||||
Returns a promise resolving to the URL string (or nil)."
|
||||
;; The snapshot is only shown heavily blurred, so it is downscaled to this max
|
||||
;; side before encoding to keep the WebP encode cheap on big/high-DPI canvases.
|
||||
(def ^:private SNAPSHOT_MAX_DIM 1024)
|
||||
|
||||
(defonce ^:private snapshot-scratch-canvas
|
||||
(delay (js/document.createElement "canvas")))
|
||||
|
||||
(defn capture-canvas-snapshot-url
|
||||
"Captures the current viewport canvas as a downscaled WebP `blob:` URL and
|
||||
stores it in `wasm/canvas-snapshot-url`. Returns a promise of the URL or nil."
|
||||
[]
|
||||
(if-let [^js canvas wasm/canvas]
|
||||
(p/create
|
||||
@@ -148,14 +161,26 @@ void main() {
|
||||
(when (and (string? prev) (.startsWith ^js prev "blob:"))
|
||||
(js/URL.revokeObjectURL prev)))
|
||||
(set! wasm/canvas-snapshot-url nil)
|
||||
(.toBlob canvas
|
||||
(fn [^js blob]
|
||||
(if blob
|
||||
(let [url (js/URL.createObjectURL blob)]
|
||||
(set! wasm/canvas-snapshot-url url)
|
||||
(resolve url))
|
||||
(resolve nil)))
|
||||
"image/png")))
|
||||
(let [cw (.-width canvas)
|
||||
ch (.-height canvas)
|
||||
;; Cap the longest side to SNAPSHOT_MAX_DIM (never upscale).
|
||||
scale (min 1.0 (/ SNAPSHOT_MAX_DIM (max 1 cw ch)))
|
||||
tw (max 1 (js/Math.round (* cw scale)))
|
||||
th (max 1 (js/Math.round (* ch scale)))
|
||||
^js sc @snapshot-scratch-canvas
|
||||
^js ctx (.getContext sc "2d")]
|
||||
(set! (.-width sc) tw)
|
||||
(set! (.-height sc) th)
|
||||
(.drawImage ctx canvas 0 0 tw th)
|
||||
(.toBlob sc
|
||||
(fn [^js blob]
|
||||
(if blob
|
||||
(let [url (js/URL.createObjectURL blob)]
|
||||
(set! wasm/canvas-snapshot-url url)
|
||||
(resolve url))
|
||||
(resolve nil)))
|
||||
SNAPSHOT_MIME
|
||||
SNAPSHOT_QUALITY))))
|
||||
(p/resolved nil)))
|
||||
|
||||
(defn draw-thumbnail-to-canvas
|
||||
|
||||
@@ -3,8 +3,8 @@ use crate::{
|
||||
error::Result,
|
||||
math::Rect,
|
||||
shapes::{
|
||||
calculate_text_layout_data, set_paint_fill, ParagraphBuilderGroup, Stroke, StrokeKind,
|
||||
TextContent,
|
||||
calculate_text_layout_data, set_paint_fill, ParagraphBuilderGroup, ParagraphLayout,
|
||||
Stroke, StrokeKind, TextContent,
|
||||
},
|
||||
utils::{get_fallback_fonts, get_font_collection},
|
||||
};
|
||||
@@ -250,6 +250,8 @@ fn render_text_on_canvas(
|
||||
fill_inset: Option<f32>,
|
||||
layer_opacity: Option<f32>,
|
||||
) {
|
||||
let use_cache = shadow.is_none() && blur.is_none();
|
||||
|
||||
if let Some(blur_filter) = blur {
|
||||
let mut blur_paint = Paint::default();
|
||||
blur_paint.set_image_filter(blur_filter.clone());
|
||||
@@ -260,7 +262,7 @@ fn render_text_on_canvas(
|
||||
if let Some(shadow_paint) = shadow {
|
||||
let layer_rec = SaveLayerRec::default().paint(shadow_paint);
|
||||
canvas.save_layer(&layer_rec);
|
||||
draw_text(canvas, shape, paragraph_builders, layer_opacity);
|
||||
draw_text(canvas, shape, paragraph_builders, layer_opacity, use_cache);
|
||||
canvas.restore();
|
||||
} else if let Some(eps) = fill_inset.filter(|&e| e > 0.0) {
|
||||
if let Some(erode) = skia_safe::image_filters::erode((eps, eps), None, None) {
|
||||
@@ -268,13 +270,13 @@ fn render_text_on_canvas(
|
||||
layer_paint.set_image_filter(erode);
|
||||
let layer_rec = SaveLayerRec::default().paint(&layer_paint);
|
||||
canvas.save_layer(&layer_rec);
|
||||
draw_text(canvas, shape, paragraph_builders, layer_opacity);
|
||||
draw_text(canvas, shape, paragraph_builders, layer_opacity, use_cache);
|
||||
canvas.restore();
|
||||
} else {
|
||||
draw_text(canvas, shape, paragraph_builders, layer_opacity);
|
||||
draw_text(canvas, shape, paragraph_builders, layer_opacity, use_cache);
|
||||
}
|
||||
} else {
|
||||
draw_text(canvas, shape, paragraph_builders, layer_opacity);
|
||||
draw_text(canvas, shape, paragraph_builders, layer_opacity, use_cache);
|
||||
}
|
||||
|
||||
if blur.is_some() {
|
||||
@@ -284,17 +286,8 @@ fn render_text_on_canvas(
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
/// Lays out and paints paragraph builders without any layer management.
|
||||
fn paint_text(
|
||||
canvas: &Canvas,
|
||||
shape: &Shape,
|
||||
paragraph_builder_groups: &mut [Vec<ParagraphBuilder>],
|
||||
) {
|
||||
let text_content = shape.get_text_content();
|
||||
let layout_info =
|
||||
calculate_text_layout_data(shape, text_content, paragraph_builder_groups, true);
|
||||
|
||||
for para in &layout_info.paragraphs {
|
||||
fn paint_paragraph_layouts(canvas: &Canvas, paragraphs: &[ParagraphLayout]) {
|
||||
for para in paragraphs {
|
||||
para.paragraph.paint(canvas, (para.x, para.y));
|
||||
for deco in ¶.decorations {
|
||||
draw_text_decorations(
|
||||
@@ -309,11 +302,53 @@ fn paint_text(
|
||||
}
|
||||
}
|
||||
|
||||
/// `use_cache` is set only by the plain fill path; stroke/mask/shadow pass
|
||||
/// `false` since they build differently painted builders.
|
||||
fn paint_text(
|
||||
canvas: &Canvas,
|
||||
shape: &Shape,
|
||||
paragraph_builder_groups: &mut [Vec<ParagraphBuilder>],
|
||||
use_cache: bool,
|
||||
) {
|
||||
let text_content = shape.get_text_content();
|
||||
|
||||
if use_cache {
|
||||
let selrect = shape.selrect();
|
||||
let key = (
|
||||
text_content.content_version(),
|
||||
selrect.x().to_bits(),
|
||||
selrect.y().to_bits(),
|
||||
selrect.width().to_bits(),
|
||||
selrect.height().to_bits(),
|
||||
shape.vertical_align() as u8,
|
||||
text_content.grow_type() as u8,
|
||||
);
|
||||
|
||||
if let Some(paragraphs) = text_content.layout.cached_render_paragraphs(key) {
|
||||
paint_paragraph_layouts(canvas, ¶graphs);
|
||||
return;
|
||||
}
|
||||
|
||||
let layout_info =
|
||||
calculate_text_layout_data(shape, text_content, paragraph_builder_groups, true);
|
||||
paint_paragraph_layouts(canvas, &layout_info.paragraphs);
|
||||
text_content
|
||||
.layout
|
||||
.store_render_paragraphs(key, layout_info.paragraphs);
|
||||
return;
|
||||
}
|
||||
|
||||
let layout_info =
|
||||
calculate_text_layout_data(shape, text_content, paragraph_builder_groups, true);
|
||||
paint_paragraph_layouts(canvas, &layout_info.paragraphs);
|
||||
}
|
||||
|
||||
fn draw_text(
|
||||
canvas: &Canvas,
|
||||
shape: &Shape,
|
||||
paragraph_builder_groups: &mut [Vec<ParagraphBuilder>],
|
||||
layer_opacity: Option<f32>,
|
||||
use_cache: bool,
|
||||
) {
|
||||
if let Some(opacity) = layer_opacity {
|
||||
let mut opacity_paint = Paint::default();
|
||||
@@ -324,7 +359,7 @@ fn draw_text(
|
||||
canvas.save_layer(&SaveLayerRec::default());
|
||||
}
|
||||
|
||||
paint_text(canvas, shape, paragraph_builder_groups);
|
||||
paint_text(canvas, shape, paragraph_builder_groups, use_cache);
|
||||
}
|
||||
|
||||
/// Renders an inner stroke using mask + SrcIn + DstOver layer structure.
|
||||
@@ -371,7 +406,7 @@ fn render_inner_stroke_on_canvas(
|
||||
canvas.save_layer(&SaveLayerRec::default());
|
||||
|
||||
// Draw opaque mask (full alpha text shape)
|
||||
paint_text(canvas, shape, mask_builders);
|
||||
paint_text(canvas, shape, mask_builders, false);
|
||||
|
||||
// SrcIn layer — only keeps stroke pixels where mask has alpha
|
||||
let mut src_in_paint = Paint::default();
|
||||
@@ -379,7 +414,7 @@ fn render_inner_stroke_on_canvas(
|
||||
canvas.save_layer(&SaveLayerRec::default().paint(&src_in_paint));
|
||||
|
||||
// Draw stroke
|
||||
paint_text(canvas, shape, stroke_builders);
|
||||
paint_text(canvas, shape, stroke_builders, false);
|
||||
|
||||
canvas.restore(); // SrcIn layer
|
||||
canvas.restore(); // mask group layer
|
||||
@@ -389,7 +424,7 @@ fn render_inner_stroke_on_canvas(
|
||||
dst_over_paint.set_blend_mode(skia::BlendMode::DstOver);
|
||||
canvas.save_layer(&SaveLayerRec::default().paint(&dst_over_paint));
|
||||
|
||||
paint_text(canvas, shape, fill_builders);
|
||||
paint_text(canvas, shape, fill_builders, false);
|
||||
|
||||
canvas.restore(); // DstOver layer
|
||||
canvas.restore(); // outer layer
|
||||
|
||||
@@ -18,6 +18,16 @@ use crate::shapes::{
|
||||
use crate::state::{ShapesPoolRef, State};
|
||||
use crate::uuid::Uuid;
|
||||
|
||||
/// Memoizes auto-grow text measurements within a single `propagate_modifiers`
|
||||
/// pass. The same text shape is re-visited many times because ancestor reflows
|
||||
/// clear the reflow guard and re-propagate the (often unchanged) layout
|
||||
/// transform; each visit would otherwise run a full Skia paragraph layout on a
|
||||
/// throwaway clone. The measured `(width, height)` is deterministic for a fixed
|
||||
/// content version and input selrect, and content does not change within one
|
||||
/// call, so it is safe to cache keyed by shape id + the selrect dimensions
|
||||
/// (as raw bits, for an exact match). The cache is per-call and discarded after.
|
||||
type TextMeasureCache = HashMap<(Uuid, u32, u32), (f32, f32)>;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn propagate_children(
|
||||
shape: &Shape,
|
||||
@@ -183,6 +193,7 @@ fn propagate_transform(
|
||||
reflown: &mut HashSet<Uuid>,
|
||||
reflowed_shapes: &mut HashSet<Uuid>,
|
||||
pending_reflows: &mut HashSet<Uuid>,
|
||||
text_measure_cache: &mut TextMeasureCache,
|
||||
) -> Result<()> {
|
||||
let Some(shape) = state.shapes.get(&entry.id) else {
|
||||
return Ok(());
|
||||
@@ -213,9 +224,20 @@ fn propagate_transform(
|
||||
GrowType::AutoHeight => {
|
||||
let height_before = text_content.size.height;
|
||||
let new_height = if width_changed {
|
||||
let mut clone = text_content.clone();
|
||||
clone.update_layout(resized_selrect);
|
||||
clone.size.height
|
||||
let key = (
|
||||
shape.id,
|
||||
resized_selrect.width().to_bits(),
|
||||
resized_selrect.height().to_bits(),
|
||||
);
|
||||
if let Some(&(_, h)) = text_measure_cache.get(&key) {
|
||||
h
|
||||
} else {
|
||||
let mut clone = text_content.clone();
|
||||
clone.update_layout(resized_selrect);
|
||||
let measured = (clone.width(), clone.size.height);
|
||||
text_measure_cache.insert(key, measured);
|
||||
measured.1
|
||||
}
|
||||
} else {
|
||||
height_before
|
||||
};
|
||||
@@ -243,9 +265,20 @@ fn propagate_transform(
|
||||
let width_before = text_content.width();
|
||||
let height_before = text_content.size.height;
|
||||
let (new_width, new_height) = if height_changed {
|
||||
let mut clone = text_content.clone();
|
||||
clone.update_layout(resized_selrect);
|
||||
(clone.width(), clone.size.height)
|
||||
let key = (
|
||||
shape.id,
|
||||
resized_selrect.width().to_bits(),
|
||||
resized_selrect.height().to_bits(),
|
||||
);
|
||||
if let Some(&measured) = text_measure_cache.get(&key) {
|
||||
measured
|
||||
} else {
|
||||
let mut clone = text_content.clone();
|
||||
clone.update_layout(resized_selrect);
|
||||
let measured = (clone.width(), clone.size.height);
|
||||
text_measure_cache.insert(key, measured);
|
||||
measured
|
||||
}
|
||||
} else {
|
||||
(width_before, height_before)
|
||||
};
|
||||
@@ -438,6 +471,10 @@ pub fn propagate_modifiers(
|
||||
// duplicate Reflow entries when many children of the same parent
|
||||
// are transformed in the same pass.
|
||||
let mut pending_reflows = HashSet::<Uuid>::new();
|
||||
// Memoizes auto-grow text measurements so the repeated ancestor re-reflows
|
||||
// (driven by `reflown` being cleared per text resize) don't re-run Skia
|
||||
// paragraph layout for the same text + selrect over and over in one call.
|
||||
let mut text_measure_cache = TextMeasureCache::new();
|
||||
|
||||
// We first propagate the transforms to the children and then after
|
||||
// recalculate the layouts. The layout can create further transforms that
|
||||
@@ -457,6 +494,7 @@ pub fn propagate_modifiers(
|
||||
&mut reflown,
|
||||
&mut reflowed_shapes,
|
||||
&mut pending_reflows,
|
||||
&mut text_measure_cache,
|
||||
)?,
|
||||
Modifier::Reflow(id, force_reflow) => {
|
||||
pending_reflows.remove(&id);
|
||||
|
||||
@@ -18,7 +18,7 @@ use skia_safe::{
|
||||
Contains,
|
||||
};
|
||||
|
||||
use std::cell::Cell;
|
||||
use std::cell::{Cell, Ref, RefCell};
|
||||
use std::collections::HashSet;
|
||||
|
||||
use super::FontFamily;
|
||||
@@ -190,11 +190,23 @@ struct CachedExtrect {
|
||||
bottom: f32,
|
||||
}
|
||||
|
||||
/// (content_version, x, y, width, height, valign, grow_type) as raw bits.
|
||||
pub type RenderTextCacheKey = (u64, u32, u32, u32, u32, u8, u8);
|
||||
|
||||
/// Cached laid-out paragraphs for the plain fill paint path, reused while the
|
||||
/// key is unchanged so re-renders skip Skia paragraph layout.
|
||||
#[derive(Debug)]
|
||||
struct RenderTextCache {
|
||||
key: RenderTextCacheKey,
|
||||
paragraphs: Vec<ParagraphLayout>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TextContentLayout {
|
||||
pub paragraph_builders: Vec<ParagraphBuilderGroup>,
|
||||
pub paragraphs: Vec<Vec<skia::textlayout::Paragraph>>,
|
||||
cached_extrect: Cell<Option<CachedExtrect>>,
|
||||
render_cache: RefCell<Option<RenderTextCache>>,
|
||||
}
|
||||
|
||||
impl Default for TextContentLayout {
|
||||
@@ -209,6 +221,7 @@ impl Clone for TextContentLayout {
|
||||
paragraph_builders: vec![],
|
||||
paragraphs: vec![],
|
||||
cached_extrect: Cell::new(None),
|
||||
render_cache: RefCell::new(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -225,6 +238,7 @@ impl TextContentLayout {
|
||||
paragraph_builders: vec![],
|
||||
paragraphs: vec![],
|
||||
cached_extrect: Cell::new(None),
|
||||
render_cache: RefCell::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,11 +250,35 @@ impl TextContentLayout {
|
||||
self.paragraph_builders = paragraph_builders;
|
||||
self.paragraphs = paragraphs;
|
||||
self.cached_extrect.set(None);
|
||||
self.render_cache.replace(None);
|
||||
}
|
||||
|
||||
pub fn needs_update(&self) -> bool {
|
||||
self.paragraph_builders.is_empty() || self.paragraphs.is_empty()
|
||||
}
|
||||
|
||||
pub fn cached_render_paragraphs(
|
||||
&self,
|
||||
key: RenderTextCacheKey,
|
||||
) -> Option<Ref<'_, Vec<ParagraphLayout>>> {
|
||||
let cache = self.render_cache.borrow();
|
||||
if cache.as_ref().map(|c| c.key) == Some(key) {
|
||||
Some(Ref::map(cache, |c| {
|
||||
&c.as_ref().expect("checked above").paragraphs
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn store_render_paragraphs(
|
||||
&self,
|
||||
key: RenderTextCacheKey,
|
||||
paragraphs: Vec<ParagraphLayout>,
|
||||
) {
|
||||
self.render_cache
|
||||
.replace(Some(RenderTextCache { key, paragraphs }));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -873,6 +911,10 @@ impl TextContent {
|
||||
paragraph_height
|
||||
}
|
||||
|
||||
pub fn content_version(&self) -> u64 {
|
||||
self.content_version
|
||||
}
|
||||
|
||||
pub fn needs_update_layout(&self) -> bool {
|
||||
self.layout.needs_update()
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user