mirror of
https://github.com/penpot/penpot.git
synced 2026-01-12 16:28:51 -05:00
Compare commits
9 Commits
develop
...
superalex-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0a7dbc1da | ||
|
|
a515c4d886 | ||
|
|
75d9004a8c | ||
|
|
af70d968c0 | ||
|
|
919b961348 | ||
|
|
9ea5e21870 | ||
|
|
10e145da35 | ||
|
|
8a2d03a28a | ||
|
|
c27a16e31a |
@@ -23,7 +23,7 @@
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
<script type="module">
|
||||
import initWasmModule from '/js/render_wasm.js';
|
||||
import initWasmModule from '/js/render-wasm.js';
|
||||
import {
|
||||
init, addShapeSolidFill, assignCanvas, hexToU32ARGB, getRandomInt, getRandomColor,
|
||||
getRandomFloat, useShape, setShapeChildren, setupInteraction, addShapeSolidStrokeFill,
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
<script type="module">
|
||||
import initWasmModule from '/js/render_wasm.js';
|
||||
import initWasmModule from '/js/render-wasm.js';
|
||||
import {
|
||||
init, addShapeSolidFill, assignCanvas, hexToU32ARGB, getRandomInt, getRandomColor,
|
||||
getRandomFloat, useShape, setShapeChildren, setupInteraction, set_parent
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
<script type="module">
|
||||
import initWasmModule from '/js/render_wasm.js';
|
||||
import initWasmModule from '/js/render-wasm.js';
|
||||
import {
|
||||
init, addShapeSolidFill, assignCanvas, hexToU32ARGB, getRandomInt, getRandomColor,
|
||||
getRandomFloat, useShape, setShapeChildren, setupInteraction, set_parent, draw_star,
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
<script type="module">
|
||||
import initWasmModule from '/js/render_wasm.js';
|
||||
import initWasmModule from '/js/render-wasm.js';
|
||||
import {
|
||||
init, addShapeSolidFill, assignCanvas, hexToU32ARGB, getRandomInt, getRandomColor,
|
||||
getRandomFloat, useShape, setShapeChildren, setupInteraction, set_parent, allocBytes,
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
<script type="module">
|
||||
import initWasmModule from '/js/render_wasm.js';
|
||||
import initWasmModule from '/js/render-wasm.js';
|
||||
import {
|
||||
init, addShapeSolidFill, assignCanvas, hexToU32ARGB, getRandomInt, getRandomColor,
|
||||
getRandomFloat, useShape, setShapeChildren, setupInteraction, addShapeSolidStrokeFill
|
||||
@@ -71,10 +71,10 @@
|
||||
|
||||
performance.mark('render:begin');
|
||||
Module._set_view(1, 0, 0);
|
||||
Module._render(Date.now());
|
||||
Module._render_sync();
|
||||
performance.mark('render:end');
|
||||
const { duration } = performance.measure('render', 'render:begin', 'render:end');
|
||||
// alert(`render time: ${duration.toFixed(2)}ms`);
|
||||
alert(`render time: ${duration.toFixed(2)}ms`);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
<script type="module">
|
||||
import initWasmModule from '/js/render_wasm.js';
|
||||
import initWasmModule from '/js/render-wasm.js';
|
||||
import {
|
||||
init, assignCanvas, setupInteraction, useShape, setShapeChildren, addTextShape, hexToU32ARGB,getRandomInt, getRandomColor, getRandomFloat, addShapeSolidFill, addShapeSolidStrokeFill
|
||||
} from './js/lib.js';
|
||||
@@ -102,4 +102,4 @@
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -10,6 +10,7 @@ $z-index-200: 200;
|
||||
$z-index-300: 300;
|
||||
$z-index-400: 400;
|
||||
$z-index-500: 500;
|
||||
$z-index-600: 600;
|
||||
|
||||
:global(:root) {
|
||||
--z-index-auto: #{$z-index-auto}; // Index for elements such as workspace, rulers ...
|
||||
@@ -18,4 +19,5 @@ $z-index-500: 500;
|
||||
--z-index-set: #{$z-index-300}; // Index for configuration elements like modals, color picker, grid edition elements
|
||||
--z-index-dropdown: #{$z-index-400}; // Index for dropdown like elements, selects, menus, dropdowns
|
||||
--z-index-notifications: #{$z-index-500}; // Index for notification
|
||||
--z-index-loaders: #{$z-index-600}; // Index for loaders
|
||||
}
|
||||
|
||||
@@ -217,6 +217,10 @@
|
||||
|
||||
design-tokens? (features/use-feature "design-tokens/v1")
|
||||
|
||||
wasm-renderer-enabled? (features/use-feature "render-wasm/v1")
|
||||
|
||||
first-frame-rendered? (mf/use-state false)
|
||||
|
||||
background-color (:background-color wglobal)]
|
||||
|
||||
(mf/with-effect []
|
||||
@@ -241,6 +245,17 @@
|
||||
(when (and file-loaded? (not page-id))
|
||||
(st/emit! (dcm/go-to-workspace :file-id file-id ::rt/replace true))))
|
||||
|
||||
(mf/with-effect [file-id page-id]
|
||||
(reset! first-frame-rendered? false))
|
||||
|
||||
(mf/with-effect []
|
||||
(let [handle-wasm-render
|
||||
(fn [_]
|
||||
(reset! first-frame-rendered? true))
|
||||
listener-key (events/listen globals/document "penpot:wasm:render" handle-wasm-render)]
|
||||
(fn []
|
||||
(events/unlistenByKey listener-key))))
|
||||
|
||||
[:> (mf/provider ctx/current-project-id) {:value project-id}
|
||||
[:> (mf/provider ctx/current-file-id) {:value file-id}
|
||||
[:> (mf/provider ctx/current-page-id) {:value page-id}
|
||||
@@ -249,15 +264,18 @@
|
||||
[:> modal-container*]
|
||||
[:section {:class (stl/css :workspace)
|
||||
:style {:background-color background-color
|
||||
:touch-action "none"}}
|
||||
:touch-action "none"
|
||||
:position "relative"}}
|
||||
[:> context-menu*]
|
||||
(if (and file-loaded? page-id)
|
||||
(when (and file-loaded? page-id)
|
||||
[:> workspace-inner*
|
||||
{:page-id page-id
|
||||
:file-id file-id
|
||||
:file file
|
||||
:wglobal wglobal
|
||||
:layout layout}]
|
||||
:layout layout}])
|
||||
(when (or (not (and file-loaded? page-id))
|
||||
(and wasm-renderer-enabled? (not @first-frame-rendered?)))
|
||||
[:> workspace-loader*])]]]]]]))
|
||||
|
||||
(mf/defc workspace-page*
|
||||
|
||||
@@ -20,7 +20,13 @@
|
||||
}
|
||||
|
||||
.workspace-loader {
|
||||
grid-area: viewport;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: var(--z-index-loaders);
|
||||
background-color: var(--color-background-primary);
|
||||
}
|
||||
|
||||
.workspace-content {
|
||||
|
||||
@@ -10,7 +10,6 @@ mod shadows;
|
||||
mod strokes;
|
||||
mod surfaces;
|
||||
pub mod text;
|
||||
|
||||
mod ui;
|
||||
|
||||
use skia_safe::{self as skia, Matrix, RRect, Rect};
|
||||
@@ -53,11 +52,113 @@ pub struct NodeRenderState {
|
||||
mask: bool,
|
||||
}
|
||||
|
||||
/// Get simplified children of a container, flattening nested flattened containers
|
||||
fn get_simplified_children<'a>(tree: ShapesPoolRef<'a>, shape: &'a Shape) -> Vec<Uuid> {
|
||||
let mut result = Vec::new();
|
||||
|
||||
for child_id in shape.children_ids_iter(false) {
|
||||
if let Some(child) = tree.get(child_id) {
|
||||
if child.can_flatten() {
|
||||
// Child is flattened: recursively get its simplified children
|
||||
result.extend(get_simplified_children(tree, child));
|
||||
} else {
|
||||
// Child is not flattened: add it directly
|
||||
result.push(*child_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
impl NodeRenderState {
|
||||
pub fn is_root(&self) -> bool {
|
||||
self.id.is_nil()
|
||||
}
|
||||
}
|
||||
|
||||
// Render signature that represents all properties affecting how a path is rendered
|
||||
// This allows efficient comparison of paths to determine if they can be combined
|
||||
#[derive(Debug, Clone)]
|
||||
struct PathRenderSignature<'a> {
|
||||
clip_bounds: &'a Option<ClipStack>,
|
||||
fills: &'a [Fill],
|
||||
strokes: &'a [Stroke],
|
||||
opacity: u32, // Converted to fixed-point for comparison (multiply by 1000)
|
||||
blend_mode: crate::shapes::BlendMode,
|
||||
transform: &'a Matrix,
|
||||
}
|
||||
|
||||
impl<'a> PathRenderSignature<'a> {
|
||||
fn from_shape(shape: &'a Shape, clip_bounds: &'a Option<ClipStack>) -> Option<Self> {
|
||||
use crate::shapes::Type;
|
||||
|
||||
// Only paths can have this signature
|
||||
if !matches!(shape.shape_type, Type::Path(_) | Type::Bool(_)) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Cannot combine paths with shadows or blur
|
||||
if !shape.shadows.is_empty() || shape.blur.is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Self {
|
||||
clip_bounds,
|
||||
fills: &shape.fills,
|
||||
strokes: &shape.strokes,
|
||||
opacity: (shape.opacity * 1000.0) as u32, // Convert to fixed-point
|
||||
blend_mode: shape.blend_mode(),
|
||||
transform: &shape.transform,
|
||||
})
|
||||
}
|
||||
|
||||
// Manual comparison since Matrix doesn't implement Eq
|
||||
fn equals(&self, other: &Self) -> bool {
|
||||
// Compare clip_bounds
|
||||
if self.clip_bounds != other.clip_bounds {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compare fills (using PartialEq)
|
||||
if self.fills != other.fills {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compare strokes (using PartialEq)
|
||||
if self.strokes != other.strokes {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compare opacity
|
||||
if self.opacity != other.opacity {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compare blend_mode
|
||||
if self.blend_mode != other.blend_mode {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compare transform (Matrix comparison - check if they're approximately equal)
|
||||
// For now, use exact equality, but we could add epsilon comparison if needed
|
||||
self.transform == other.transform
|
||||
}
|
||||
}
|
||||
|
||||
// SIMPLE OPTIMIZATION: Helper to check if two shapes can be rendered together
|
||||
// Uses render signatures for efficient comparison
|
||||
fn can_render_paths_together(shape1: &Shape, shape2: &Shape, clip_bounds1: &Option<ClipStack>, clip_bounds2: &Option<ClipStack>) -> bool {
|
||||
let sig1 = PathRenderSignature::from_shape(shape1, clip_bounds1);
|
||||
let sig2 = PathRenderSignature::from_shape(shape2, clip_bounds2);
|
||||
|
||||
match (sig1, sig2) {
|
||||
(Some(sig1), Some(sig2)) => sig1.equals(&sig2),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeRenderState {
|
||||
/// Calculates the clip bounds for child elements of a given shape.
|
||||
///
|
||||
/// This function determines the clipping region that should be applied to child elements
|
||||
@@ -276,6 +377,10 @@ pub(crate) struct RenderState {
|
||||
/// where we must render shapes without inheriting ancestor layer blurs. Toggle it through
|
||||
/// `with_nested_blurs_suppressed` to ensure it's always restored.
|
||||
pub ignore_nested_blurs: bool,
|
||||
/// Cached root_ids and root_ids_map to avoid recalculating them every frame
|
||||
/// These are invalidated when the tree structure changes
|
||||
cached_root_ids: Option<Vec<Uuid>>,
|
||||
cached_root_ids_map: Option<std::collections::HashMap<Uuid, usize>>,
|
||||
}
|
||||
|
||||
pub fn get_cache_size(viewbox: Viewbox, scale: f32) -> skia::ISize {
|
||||
@@ -348,6 +453,8 @@ impl RenderState {
|
||||
focus_mode: FocusMode::new(),
|
||||
touched_ids: HashSet::default(),
|
||||
ignore_nested_blurs: false,
|
||||
cached_root_ids: None,
|
||||
cached_root_ids_map: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,12 +505,7 @@ impl RenderState {
|
||||
}
|
||||
|
||||
fn frame_clip_layer_blur(shape: &Shape) -> Option<Blur> {
|
||||
match shape.shape_type {
|
||||
Type::Frame(_) if shape.clip() => shape.blur.filter(|blur| {
|
||||
!blur.hidden && blur.blur_type == BlurType::LayerBlur && blur.value > 0.
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
shape.frame_clip_layer_blur()
|
||||
}
|
||||
|
||||
/// Runs `f` with `ignore_nested_blurs` temporarily forced to `true`.
|
||||
@@ -521,38 +623,65 @@ impl RenderState {
|
||||
|
||||
let paint = skia::Paint::default();
|
||||
|
||||
self.surfaces
|
||||
.draw_into(SurfaceId::TextDropShadows, SurfaceId::Current, Some(&paint));
|
||||
// Only draw surfaces that have content (dirty flag optimization)
|
||||
if self.surfaces.is_dirty(SurfaceId::TextDropShadows) {
|
||||
self.surfaces
|
||||
.draw_into(SurfaceId::TextDropShadows, SurfaceId::Current, Some(&paint));
|
||||
}
|
||||
|
||||
self.surfaces
|
||||
.draw_into(SurfaceId::Fills, SurfaceId::Current, Some(&paint));
|
||||
if self.surfaces.is_dirty(SurfaceId::Fills) {
|
||||
self.surfaces
|
||||
.draw_into(SurfaceId::Fills, SurfaceId::Current, Some(&paint));
|
||||
}
|
||||
|
||||
let mut render_overlay_below_strokes = false;
|
||||
if let Some(shape) = shape {
|
||||
render_overlay_below_strokes = shape.has_fills();
|
||||
}
|
||||
|
||||
if render_overlay_below_strokes {
|
||||
if render_overlay_below_strokes && self.surfaces.is_dirty(SurfaceId::InnerShadows) {
|
||||
self.surfaces
|
||||
.draw_into(SurfaceId::InnerShadows, SurfaceId::Current, Some(&paint));
|
||||
}
|
||||
|
||||
self.surfaces
|
||||
.draw_into(SurfaceId::Strokes, SurfaceId::Current, Some(&paint));
|
||||
if self.surfaces.is_dirty(SurfaceId::Strokes) {
|
||||
self.surfaces
|
||||
.draw_into(SurfaceId::Strokes, SurfaceId::Current, Some(&paint));
|
||||
}
|
||||
|
||||
if !render_overlay_below_strokes {
|
||||
if !render_overlay_below_strokes && self.surfaces.is_dirty(SurfaceId::InnerShadows) {
|
||||
self.surfaces
|
||||
.draw_into(SurfaceId::InnerShadows, SurfaceId::Current, Some(&paint));
|
||||
}
|
||||
|
||||
// Only clear surfaces that were actually used (have dirty flag set)
|
||||
let surface_ids = SurfaceId::Strokes as u32
|
||||
| SurfaceId::Fills as u32
|
||||
| SurfaceId::InnerShadows as u32
|
||||
| SurfaceId::TextDropShadows as u32;
|
||||
|
||||
self.surfaces.apply_mut(surface_ids, |s| {
|
||||
s.canvas().clear(skia::Color::TRANSPARENT);
|
||||
});
|
||||
// Build mask of dirty surfaces that need clearing
|
||||
let mut dirty_surfaces_to_clear = 0u32;
|
||||
if self.surfaces.is_dirty(SurfaceId::Strokes) {
|
||||
dirty_surfaces_to_clear |= SurfaceId::Strokes as u32;
|
||||
}
|
||||
if self.surfaces.is_dirty(SurfaceId::Fills) {
|
||||
dirty_surfaces_to_clear |= SurfaceId::Fills as u32;
|
||||
}
|
||||
if self.surfaces.is_dirty(SurfaceId::InnerShadows) {
|
||||
dirty_surfaces_to_clear |= SurfaceId::InnerShadows as u32;
|
||||
}
|
||||
if self.surfaces.is_dirty(SurfaceId::TextDropShadows) {
|
||||
dirty_surfaces_to_clear |= SurfaceId::TextDropShadows as u32;
|
||||
}
|
||||
|
||||
if dirty_surfaces_to_clear != 0 {
|
||||
self.surfaces.apply_mut(dirty_surfaces_to_clear, |s| {
|
||||
s.canvas().clear(skia::Color::TRANSPARENT);
|
||||
});
|
||||
// Clear dirty flags for surfaces we just cleared
|
||||
self.surfaces.clear_dirty(dirty_surfaces_to_clear);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_focus_mode(&mut self) {
|
||||
@@ -605,9 +734,17 @@ impl RenderState {
|
||||
| strokes_surface_id as u32
|
||||
| innershadows_surface_id as u32
|
||||
| text_drop_shadows_surface_id as u32;
|
||||
self.surfaces.apply_mut(surface_ids, |s| {
|
||||
s.canvas().save();
|
||||
});
|
||||
|
||||
// Only save canvas state if we have clipping or transforms
|
||||
// For simple shapes without clipping, skip expensive save/restore
|
||||
let needs_save =
|
||||
clip_bounds.is_some() || offset.is_some() || !shape.transform.is_identity();
|
||||
|
||||
if needs_save {
|
||||
self.surfaces.apply_mut(surface_ids, |s| {
|
||||
s.canvas().save();
|
||||
});
|
||||
}
|
||||
|
||||
let antialias = shape.should_use_antialias(self.get_scale());
|
||||
|
||||
@@ -686,7 +823,7 @@ impl RenderState {
|
||||
self.surfaces.canvas(fills_surface_id).concat(&matrix);
|
||||
|
||||
if let Some(svg) = shape.svg.as_ref() {
|
||||
svg.render(self.surfaces.canvas(fills_surface_id))
|
||||
svg.render(self.surfaces.canvas(fills_surface_id));
|
||||
} else {
|
||||
let font_manager = skia::FontMgr::from(self.fonts().font_provider().clone());
|
||||
let dom_result = skia::svg::Dom::from_str(&sr.content, font_manager);
|
||||
@@ -908,9 +1045,13 @@ impl RenderState {
|
||||
if apply_to_current_surface {
|
||||
self.apply_drawing_to_render_canvas(Some(&shape));
|
||||
}
|
||||
self.surfaces.apply_mut(surface_ids, |s| {
|
||||
s.canvas().restore();
|
||||
});
|
||||
|
||||
// Only restore if we saved (optimization for simple shapes)
|
||||
if needs_save {
|
||||
self.surfaces.apply_mut(surface_ids, |s| {
|
||||
s.canvas().restore();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_render_context(&mut self, tile: tiles::Tile) {
|
||||
@@ -1031,6 +1172,7 @@ impl RenderState {
|
||||
// reorder by distance to the center.
|
||||
self.current_tile = None;
|
||||
self.render_in_progress = true;
|
||||
|
||||
self.apply_drawing_to_render_canvas(None);
|
||||
|
||||
if sync_render {
|
||||
@@ -1117,35 +1259,41 @@ impl RenderState {
|
||||
self.nested_fills.push(Vec::new());
|
||||
}
|
||||
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_blend_mode(element.blend_mode().into());
|
||||
paint.set_alpha_f(element.opacity());
|
||||
// Only create save_layer if actually needed
|
||||
// For simple shapes with default opacity and blend mode, skip expensive save_layer
|
||||
let needs_layer = element.needs_layer() || mask;
|
||||
|
||||
if let Some(frame_blur) = Self::frame_clip_layer_blur(element) {
|
||||
let scale = self.get_scale();
|
||||
let sigma = frame_blur.value * scale;
|
||||
if let Some(filter) = skia::image_filters::blur((sigma, sigma), None, None, None) {
|
||||
paint.set_image_filter(filter);
|
||||
if needs_layer {
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_blend_mode(element.blend_mode().into());
|
||||
paint.set_alpha_f(element.opacity());
|
||||
|
||||
if let Some(frame_blur) = Self::frame_clip_layer_blur(element) {
|
||||
let scale = self.get_scale();
|
||||
let sigma = frame_blur.value * scale;
|
||||
if let Some(filter) = skia::image_filters::blur((sigma, sigma), None, None, None) {
|
||||
paint.set_image_filter(filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When we're rendering the mask shape we need to set a special blend mode
|
||||
// called 'destination-in' that keeps the drawn content within the mask.
|
||||
// @see https://skia.org/docs/user/api/skblendmode_overview/
|
||||
if mask {
|
||||
let mut mask_paint = skia::Paint::default();
|
||||
mask_paint.set_blend_mode(skia::BlendMode::DstIn);
|
||||
let mask_rec = skia::canvas::SaveLayerRec::default().paint(&mask_paint);
|
||||
// When we're rendering the mask shape we need to set a special blend mode
|
||||
// called 'destination-in' that keeps the drawn content within the mask.
|
||||
// @see https://skia.org/docs/user/api/skblendmode_overview/
|
||||
if mask {
|
||||
let mut mask_paint = skia::Paint::default();
|
||||
mask_paint.set_blend_mode(skia::BlendMode::DstIn);
|
||||
let mask_rec = skia::canvas::SaveLayerRec::default().paint(&mask_paint);
|
||||
self.surfaces
|
||||
.canvas(SurfaceId::Current)
|
||||
.save_layer(&mask_rec);
|
||||
}
|
||||
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
|
||||
self.surfaces
|
||||
.canvas(SurfaceId::Current)
|
||||
.save_layer(&mask_rec);
|
||||
.save_layer(&layer_rec);
|
||||
}
|
||||
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
|
||||
self.surfaces
|
||||
.canvas(SurfaceId::Current)
|
||||
.save_layer(&layer_rec);
|
||||
|
||||
self.focus_mode.enter(&element.id);
|
||||
}
|
||||
|
||||
@@ -1217,7 +1365,15 @@ impl RenderState {
|
||||
);
|
||||
}
|
||||
|
||||
self.surfaces.canvas(SurfaceId::Current).restore();
|
||||
// Only restore if we created a layer (optimization for simple shapes)
|
||||
let needs_layer = element.needs_layer()
|
||||
|| (matches!(element.shape_type, Type::Group(_))
|
||||
&& matches!(element.shape_type, Type::Group(g) if g.masked));
|
||||
|
||||
if needs_layer {
|
||||
self.surfaces.canvas(SurfaceId::Current).restore();
|
||||
}
|
||||
|
||||
self.focus_mode.exit(&element.id);
|
||||
}
|
||||
|
||||
@@ -1457,18 +1613,40 @@ impl RenderState {
|
||||
}
|
||||
|
||||
if visited_children {
|
||||
self.render_shape_exit(element, visited_mask);
|
||||
// Skip render_shape_exit for flattened containers
|
||||
if !element.can_flatten() {
|
||||
self.render_shape_exit(element, visited_mask);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if !node_render_state.is_root() {
|
||||
let transformed_element: Cow<Shape> = Cow::Borrowed(element);
|
||||
let scale = self.get_scale();
|
||||
let extrect = transformed_element.extrect(tree, scale);
|
||||
|
||||
let is_visible = extrect.intersects(self.render_area)
|
||||
&& !transformed_element.hidden
|
||||
&& !transformed_element.visually_insignificant(scale, tree);
|
||||
// Aggressive early exit: check hidden and selrect first (fastest checks)
|
||||
if transformed_element.hidden {
|
||||
continue;
|
||||
}
|
||||
|
||||
let selrect = transformed_element.selrect();
|
||||
if !selrect.intersects(self.render_area) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// For simple shapes without effects, selrect check is sufficient
|
||||
// Only calculate expensive extrect for shapes with effects that might extend bounds
|
||||
let scale = self.get_scale();
|
||||
let has_effects = transformed_element.has_effects_that_extend_bounds();
|
||||
|
||||
let is_visible = if !has_effects {
|
||||
// Simple shape: selrect check is sufficient, skip expensive extrect
|
||||
!transformed_element.visually_insignificant(scale, tree)
|
||||
} else {
|
||||
// Shape with effects: need extrect for accurate bounds
|
||||
let extrect = transformed_element.extrect(tree, scale);
|
||||
extrect.intersects(self.render_area)
|
||||
&& !transformed_element.visually_insignificant(scale, tree)
|
||||
};
|
||||
|
||||
if self.options.is_debug_visible() {
|
||||
let shape_extrect_bounds =
|
||||
@@ -1481,7 +1659,12 @@ impl RenderState {
|
||||
}
|
||||
}
|
||||
|
||||
self.render_shape_enter(element, mask);
|
||||
// Skip render_shape_enter/exit for flattened containers
|
||||
// If a container was flattened, it doesn't affect children visually, so we skip
|
||||
// the expensive enter/exit operations and process children directly
|
||||
if !element.can_flatten() {
|
||||
self.render_shape_enter(element, mask);
|
||||
}
|
||||
|
||||
if !node_render_state.is_root() && self.focus_mode.is_active() {
|
||||
let scale: f32 = self.get_scale();
|
||||
@@ -1515,6 +1698,8 @@ impl RenderState {
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let element_extrect = element.extrect(tree, scale);
|
||||
|
||||
for shadow in element.drop_shadows_visible() {
|
||||
let paint = skia::Paint::default();
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
|
||||
@@ -1526,7 +1711,7 @@ impl RenderState {
|
||||
// First pass: Render shadow in black to establish alpha mask
|
||||
self.render_drop_black_shadow(
|
||||
element,
|
||||
&element.extrect(tree, scale),
|
||||
&element_extrect,
|
||||
shadow,
|
||||
clip_bounds.clone(),
|
||||
scale,
|
||||
@@ -1546,9 +1731,10 @@ impl RenderState {
|
||||
.get_nested_shadow_clip_bounds(element, shadow);
|
||||
|
||||
if !matches!(shadow_shape.shape_type, Type::Text(_)) {
|
||||
let shadow_extrect = shadow_shape.extrect(tree, scale);
|
||||
self.render_drop_black_shadow(
|
||||
shadow_shape,
|
||||
&shadow_shape.extrect(tree, scale),
|
||||
&shadow_extrect,
|
||||
shadow,
|
||||
clip_bounds,
|
||||
scale,
|
||||
@@ -1663,8 +1849,82 @@ impl RenderState {
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
.clear(skia::Color::TRANSPARENT);
|
||||
|
||||
// SIMPLE OPTIMIZATION: If this is a path, check if consecutive siblings
|
||||
// are also paths with the same properties and combine them all
|
||||
// We check BEFORE rendering to avoid rendering the first path twice
|
||||
let shape_to_render = if matches!(element.shape_type, Type::Path(_) | Type::Bool(_)) {
|
||||
// Collect all consecutive paths with same properties
|
||||
let mut path_ids = vec![element.id];
|
||||
let mut path_selrects = vec![element.selrect()];
|
||||
|
||||
// Keep checking and collecting consecutive compatible paths
|
||||
while let Some(next_node) = self.pending_nodes.last() {
|
||||
if next_node.visited_children || next_node.is_root() {
|
||||
break;
|
||||
}
|
||||
|
||||
if let Some(next_element) = tree.get(&next_node.id) {
|
||||
// Check if next element is also a path with same properties
|
||||
if can_render_paths_together(element, next_element, &clip_bounds, &next_node.clip_bounds) {
|
||||
// Remove it from pending_nodes and add to our collection
|
||||
let _next_node = self.pending_nodes.pop().unwrap();
|
||||
path_ids.push(next_element.id);
|
||||
path_selrects.push(next_element.selrect());
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we collected more than one path, combine them all
|
||||
if path_ids.len() > 1 {
|
||||
// Combine paths by joining their segments (simpler than boolean union)
|
||||
// This avoids the complexity and potential errors of boolean operations
|
||||
use crate::shapes::{ToPath, Path as ShapePath};
|
||||
|
||||
let mut combined_path = element.to_path(tree);
|
||||
for path_id in path_ids.iter().skip(1) {
|
||||
if let Some(path_shape) = tree.get(path_id) {
|
||||
let other_path = path_shape.to_path(tree);
|
||||
// Simple join: concatenate segments (like join_paths does)
|
||||
let mut segments = combined_path.segments().clone();
|
||||
segments.extend(other_path.segments().iter().cloned());
|
||||
combined_path = ShapePath::new(segments);
|
||||
}
|
||||
}
|
||||
|
||||
// Create a temporary shape with the combined path
|
||||
// Use properties from the first shape (they're the same anyway)
|
||||
let mut combined_shape = element.clone();
|
||||
combined_shape.shape_type = crate::shapes::Type::Path(combined_path);
|
||||
|
||||
// Update selrect to encompass all paths
|
||||
let mut left = path_selrects[0].left();
|
||||
let mut top = path_selrects[0].top();
|
||||
let mut right = path_selrects[0].right();
|
||||
let mut bottom = path_selrects[0].bottom();
|
||||
for selrect in path_selrects.iter().skip(1) {
|
||||
left = left.min(selrect.left());
|
||||
top = top.min(selrect.top());
|
||||
right = right.max(selrect.right());
|
||||
bottom = bottom.max(selrect.bottom());
|
||||
}
|
||||
combined_shape.set_selrect(left, top, right, bottom);
|
||||
|
||||
Some(combined_shape)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Render the shape (either combined or original)
|
||||
let final_shape = shape_to_render.as_ref().unwrap_or(element);
|
||||
self.render_shape(
|
||||
element,
|
||||
final_shape,
|
||||
clip_bounds.clone(),
|
||||
SurfaceId::Fills,
|
||||
SurfaceId::Strokes,
|
||||
@@ -1674,6 +1934,11 @@ impl RenderState {
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
// If we combined paths, skip normal processing of the remaining nodes
|
||||
if shape_to_render.is_some() {
|
||||
continue;
|
||||
}
|
||||
|
||||
self.surfaces
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
@@ -1682,14 +1947,18 @@ impl RenderState {
|
||||
self.apply_drawing_to_render_canvas(Some(element));
|
||||
}
|
||||
|
||||
match element.shape_type {
|
||||
Type::Frame(_) if Self::frame_clip_layer_blur(element).is_some() => {
|
||||
self.nested_blurs.push(None);
|
||||
// Skip nested state updates for flattened containers
|
||||
// Flattened containers don't affect children, so we don't need to track their state
|
||||
if !element.can_flatten() {
|
||||
match element.shape_type {
|
||||
Type::Frame(_) if Self::frame_clip_layer_blur(element).is_some() => {
|
||||
self.nested_blurs.push(None);
|
||||
}
|
||||
Type::Frame(_) | Type::Group(_) => {
|
||||
self.nested_blurs.push(element.blur);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Type::Frame(_) | Type::Group(_) => {
|
||||
self.nested_blurs.push(element.blur);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Set the node as visited_children before processing children
|
||||
@@ -1704,24 +1973,35 @@ impl RenderState {
|
||||
if element.is_recursive() {
|
||||
let children_clip_bounds =
|
||||
node_render_state.get_children_clip_bounds(element, None);
|
||||
let mut children_ids: Vec<_> = element.children_ids_iter(false).collect();
|
||||
|
||||
let children_ids: Vec<_> = if element.can_flatten() {
|
||||
// Container was flattened: get simplified children (which skip this level)
|
||||
get_simplified_children(tree, element)
|
||||
} else {
|
||||
// Container not flattened: use original children
|
||||
element.children_ids_iter(false).copied().collect()
|
||||
};
|
||||
|
||||
// Z-index ordering on Layouts
|
||||
if element.has_layout() {
|
||||
let children_ids = if element.has_layout() {
|
||||
let mut ids = children_ids;
|
||||
if element.is_flex() && !element.is_flex_reverse() {
|
||||
children_ids.reverse();
|
||||
ids.reverse();
|
||||
}
|
||||
|
||||
children_ids.sort_by(|id1, id2| {
|
||||
ids.sort_by(|id1, id2| {
|
||||
let z1 = tree.get(id1).map(|s| s.z_index()).unwrap_or(0);
|
||||
let z2 = tree.get(id2).map(|s| s.z_index()).unwrap_or(0);
|
||||
z2.cmp(&z1)
|
||||
});
|
||||
}
|
||||
ids
|
||||
} else {
|
||||
children_ids
|
||||
};
|
||||
|
||||
for child_id in children_ids.iter() {
|
||||
self.pending_nodes.push(NodeRenderState {
|
||||
id: **child_id,
|
||||
id: *child_id,
|
||||
visited_children: false,
|
||||
clip_bounds: children_clip_bounds.clone(),
|
||||
visited_mask: false,
|
||||
@@ -1803,14 +2083,37 @@ impl RenderState {
|
||||
.canvas(SurfaceId::Current)
|
||||
.clear(self.background_color);
|
||||
|
||||
let root_ids = {
|
||||
if let Some(shape_id) = base_object {
|
||||
// Get or compute root_ids and root_ids_map (cached to avoid recalculation every frame)
|
||||
let (root_ids, root_ids_map) = {
|
||||
let root_ids = if let Some(shape_id) = base_object {
|
||||
vec![*shape_id]
|
||||
} else {
|
||||
let Some(root) = tree.get(&Uuid::nil()) else {
|
||||
return Err(String::from("Root shape not found"));
|
||||
};
|
||||
root.children_ids(false)
|
||||
};
|
||||
|
||||
// Check if cache is valid (same root_ids)
|
||||
let cache_valid = self.cached_root_ids.as_ref()
|
||||
.map(|cached| cached.len() == root_ids.len() && cached.iter().zip(root_ids.iter()).all(|(a, b)| a == b))
|
||||
.unwrap_or(false);
|
||||
|
||||
if cache_valid {
|
||||
// Use cached map
|
||||
(root_ids, self.cached_root_ids_map.as_ref().unwrap().clone())
|
||||
} else {
|
||||
// Recompute and cache
|
||||
let root_ids_map: std::collections::HashMap<Uuid, usize> = root_ids
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, id)| (*id, i))
|
||||
.collect();
|
||||
|
||||
self.cached_root_ids = Some(root_ids.clone());
|
||||
self.cached_root_ids_map = Some(root_ids_map.clone());
|
||||
|
||||
(root_ids, root_ids_map)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1821,12 +2124,6 @@ impl RenderState {
|
||||
|
||||
if !self.surfaces.has_cached_tile_surface(next_tile) {
|
||||
if let Some(ids) = self.tiles.get_shapes_at(next_tile) {
|
||||
let root_ids_map: std::collections::HashMap<Uuid, usize> = root_ids
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, id)| (*id, i))
|
||||
.collect();
|
||||
|
||||
// We only need first level shapes
|
||||
let mut valid_ids: Vec<Uuid> = ids
|
||||
.iter()
|
||||
|
||||
@@ -55,6 +55,8 @@ pub struct Surfaces {
|
||||
tiles: TileTextureCache,
|
||||
sampling_options: skia::SamplingOptions,
|
||||
margins: skia::ISize,
|
||||
// Tracks which surfaces have content (dirty flag bitmask)
|
||||
dirty_surfaces: u32,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
@@ -105,6 +107,7 @@ impl Surfaces {
|
||||
tiles,
|
||||
sampling_options,
|
||||
margins,
|
||||
dirty_surfaces: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,9 +151,40 @@ impl Surfaces {
|
||||
}
|
||||
|
||||
pub fn canvas(&mut self, id: SurfaceId) -> &skia::Canvas {
|
||||
// Automatically mark render surfaces as dirty when accessed
|
||||
// This tracks which surfaces have content for optimization
|
||||
match id {
|
||||
SurfaceId::Fills
|
||||
| SurfaceId::Strokes
|
||||
| SurfaceId::InnerShadows
|
||||
| SurfaceId::TextDropShadows => {
|
||||
self.mark_dirty(id);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
self.get_mut(id).canvas()
|
||||
}
|
||||
|
||||
/// Marks a surface as having content (dirty)
|
||||
pub fn mark_dirty(&mut self, id: SurfaceId) {
|
||||
self.dirty_surfaces |= id as u32;
|
||||
}
|
||||
|
||||
/// Checks if a surface has content
|
||||
pub fn is_dirty(&self, id: SurfaceId) -> bool {
|
||||
(self.dirty_surfaces & id as u32) != 0
|
||||
}
|
||||
|
||||
/// Clears the dirty flag for a surface or set of surfaces
|
||||
pub fn clear_dirty(&mut self, ids: u32) {
|
||||
self.dirty_surfaces &= !ids;
|
||||
}
|
||||
|
||||
/// Clears all dirty flags
|
||||
pub fn clear_all_dirty(&mut self) {
|
||||
self.dirty_surfaces = 0;
|
||||
}
|
||||
|
||||
pub fn flush_and_submit(&mut self, gpu_state: &mut GpuState, id: SurfaceId) {
|
||||
let surface = self.get_mut(id);
|
||||
gpu_state.context.flush_and_submit_surface(surface, None);
|
||||
@@ -212,18 +246,33 @@ impl Surfaces {
|
||||
|
||||
pub fn update_render_context(&mut self, render_area: skia::Rect, scale: f32) {
|
||||
let translation = self.get_render_context_translation(render_area, scale);
|
||||
self.apply_mut(
|
||||
SurfaceId::Fills as u32
|
||||
| SurfaceId::Strokes as u32
|
||||
| SurfaceId::InnerShadows as u32
|
||||
| SurfaceId::TextDropShadows as u32,
|
||||
|s| {
|
||||
let canvas = s.canvas();
|
||||
canvas.reset_matrix();
|
||||
canvas.scale((scale, scale));
|
||||
canvas.translate(translation);
|
||||
},
|
||||
);
|
||||
|
||||
// When context changes (zoom/pan/tile), clear all render surfaces first
|
||||
// to remove any residual content from previous tiles, then mark as dirty
|
||||
// so they get redrawn with new transformations
|
||||
let surface_ids = SurfaceId::Fills as u32
|
||||
| SurfaceId::Strokes as u32
|
||||
| SurfaceId::InnerShadows as u32
|
||||
| SurfaceId::TextDropShadows as u32;
|
||||
|
||||
// Clear surfaces before updating transformations to remove residual content
|
||||
self.apply_mut(surface_ids, |s| {
|
||||
s.canvas().clear(skia::Color::TRANSPARENT);
|
||||
});
|
||||
|
||||
// Mark all render surfaces as dirty so they get redrawn
|
||||
self.mark_dirty(SurfaceId::Fills);
|
||||
self.mark_dirty(SurfaceId::Strokes);
|
||||
self.mark_dirty(SurfaceId::InnerShadows);
|
||||
self.mark_dirty(SurfaceId::TextDropShadows);
|
||||
|
||||
// Update transformations
|
||||
self.apply_mut(surface_ids, |s| {
|
||||
let canvas = s.canvas();
|
||||
canvas.reset_matrix();
|
||||
canvas.scale((scale, scale));
|
||||
canvas.translate(translation);
|
||||
});
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -268,15 +317,18 @@ impl Surfaces {
|
||||
} else {
|
||||
self.canvas(id).draw_rect(shape.selrect, paint);
|
||||
}
|
||||
self.mark_dirty(id);
|
||||
}
|
||||
|
||||
pub fn draw_circle_to(&mut self, id: SurfaceId, shape: &Shape, paint: &Paint) {
|
||||
self.canvas(id).draw_oval(shape.selrect, paint);
|
||||
self.mark_dirty(id);
|
||||
}
|
||||
|
||||
pub fn draw_path_to(&mut self, id: SurfaceId, shape: &Shape, paint: &Paint) {
|
||||
if let Some(path) = shape.get_skia_path() {
|
||||
self.canvas(id).draw_path(&path, paint);
|
||||
self.mark_dirty(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,6 +356,9 @@ impl Surfaces {
|
||||
self.canvas(SurfaceId::UI)
|
||||
.clear(skia::Color::TRANSPARENT)
|
||||
.reset_matrix();
|
||||
|
||||
// Clear all dirty flags after reset
|
||||
self.clear_all_dirty();
|
||||
}
|
||||
|
||||
pub fn cache_current_tile_texture(
|
||||
|
||||
@@ -920,10 +920,27 @@ impl Shape {
|
||||
}
|
||||
|
||||
Type::Group(_) | Type::Frame(_) if !self.clip_content => {
|
||||
// Use selrect as a fast approximation first, then calculate
|
||||
// extrect only if needed. This avoids expensive recursive extrect calculations
|
||||
// for children that don't significantly expand the bounds.
|
||||
for child_id in self.children_ids_iter(false) {
|
||||
if let Some(child_shape) = shapes_pool.get(child_id) {
|
||||
let child_extrect = child_shape.calculate_extrect(shapes_pool, scale);
|
||||
rect.join(child_extrect);
|
||||
// Fast path: check if child has effects that might expand bounds
|
||||
// If no effects, selrect is likely sufficient
|
||||
let has_effects = !child_shape.shadows.is_empty()
|
||||
|| child_shape.blur.is_some()
|
||||
|| !child_shape.strokes.is_empty()
|
||||
|| matches!(child_shape.shape_type, Type::Group(_) | Type::Frame(_));
|
||||
|
||||
if has_effects {
|
||||
// Calculate full extrect for shapes with effects
|
||||
let child_extrect = child_shape.calculate_extrect(shapes_pool, scale);
|
||||
rect.join(child_extrect);
|
||||
} else {
|
||||
// No effects, selrect is sufficient (much faster)
|
||||
let child_selrect = child_shape.selrect();
|
||||
rect.join(child_selrect);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1419,6 +1436,92 @@ impl Shape {
|
||||
!self.fills.is_empty()
|
||||
}
|
||||
|
||||
/// Determines if this frame or group can be flattened (doesn't affect children visually)
|
||||
/// A container can be flattened if it has no visual effects that affect its children
|
||||
/// and doesn't render its own content (no fills/strokes)
|
||||
pub fn can_flatten(&self) -> bool {
|
||||
// Only frames and groups can be flattened
|
||||
if !matches!(self.shape_type, Type::Frame(_) | Type::Group(_)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cannot flatten if it has visual effects that affect children:
|
||||
|
||||
if self.clip_content {
|
||||
return false;
|
||||
}
|
||||
|
||||
if !self.transform.is_identity() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.opacity != 1.0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.blend_mode() != BlendMode::default() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.blur.is_some() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if !self.shadows.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Type::Group(group) = &self.shape_type {
|
||||
if group.masked {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// If the container itself has fills/strokes, it renders something visible
|
||||
// We cannot flatten containers that render their own background/border
|
||||
// because they need to be rendered even if they don't affect children
|
||||
if self.has_fills() || self.has_visible_strokes() {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Checks if this shape needs a layer for rendering due to visual effects
|
||||
/// (opacity < 1.0, non-default blend mode, or frame clip layer blur)
|
||||
pub fn needs_layer(&self) -> bool {
|
||||
self.opacity() < 1.0
|
||||
|| self.blend_mode().0 != skia::BlendMode::SrcOver
|
||||
|| self.has_frame_clip_layer_blur()
|
||||
}
|
||||
|
||||
/// Checks if this frame has clip layer blur (affects children)
|
||||
/// A frame has clip layer blur if it clips content and has layer blur
|
||||
pub fn has_frame_clip_layer_blur(&self) -> bool {
|
||||
self.frame_clip_layer_blur().is_some()
|
||||
}
|
||||
|
||||
/// Returns the frame clip layer blur if this frame has one
|
||||
/// A frame has clip layer blur if it clips content and has layer blur
|
||||
pub fn frame_clip_layer_blur(&self) -> Option<Blur> {
|
||||
use crate::shapes::BlurType;
|
||||
match self.shape_type {
|
||||
Type::Frame(_) if self.clip_content => self.blur.filter(|blur| {
|
||||
!blur.hidden && blur.blur_type == BlurType::LayerBlur && blur.value > 0.0
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if this shape has visual effects that might extend its bounds beyond selrect
|
||||
/// Shapes with these effects require expensive extrect calculation for accurate visibility checks
|
||||
pub fn has_effects_that_extend_bounds(&self) -> bool {
|
||||
!self.shadows.is_empty()
|
||||
|| self.blur.is_some()
|
||||
|| !self.strokes.is_empty()
|
||||
|| matches!(self.shape_type, Type::Group(_) | Type::Frame(_))
|
||||
}
|
||||
|
||||
pub fn count_visible_inner_strokes(&self) -> usize {
|
||||
self.visible_strokes()
|
||||
.filter(|s| s.kind == StrokeKind::Inner)
|
||||
|
||||
Reference in New Issue
Block a user