This commit is contained in:
Aitor Moreno 2026-06-10 15:46:16 +02:00
parent 3ffc76a552
commit 9c2d963ff5
15 changed files with 252 additions and 222 deletions

View File

@ -922,7 +922,7 @@ pub extern "C" fn get_shape_extrect(a: u32, b: u32, c: u32, d: u32) -> Result<*m
let Some(shape) = state.shapes.get(&id) else {
return Err(Error::CriticalError("Shape not found".to_string()));
};
let extrect = get_render_state().get_cached_extrect(shape, &state.shapes, 1.0);
let extrect = shape.extrect(&state.shapes, 1.0);
let mut buf = Vec::with_capacity(16);
buf.extend_from_slice(&extrect.x().to_le_bytes());
buf.extend_from_slice(&extrect.y().to_le_bytes());

View File

@ -333,11 +333,7 @@ fn beziers_to_segments(beziers: &[(BezierSource, Bezier)]) -> Vec<Segment> {
let mut last_end = (first_bezier.end.x as f32, first_bezier.end.y as f32);
let mut cur_end = first_bezier.end;
loop {
let Some((next_src, next_bezier)) = find_next_in_pool(&mut pool, cur_end, cur_src)
else {
break;
};
while let Some((next_src, next_bezier)) = find_next_in_pool(&mut pool, cur_end, cur_src) {
push_bezier(&mut result, &next_bezier);
last_end = (next_bezier.end.x as f32, next_bezier.end.y as f32);
cur_end = next_bezier.end;

View File

@ -24,8 +24,8 @@ pub use surfaces::{SurfaceId, Surfaces};
use crate::error::{Error, Result};
use crate::math;
use crate::shapes::{
all_with_ancestors, radius_to_sigma, Blur, BlurType, Corners, Fill, Shadow, Shape, SolidColor,
Stroke, StrokeKind, TextContent, Type,
all_with_ancestors, radius_to_sigma, Blur, BlurType, Corners, Fill, Layout, Shadow, Shape,
SolidColor, Stroke, StrokeKind, TextContent, Type,
};
use crate::state::{RulerState, ShapesPoolMutRef, ShapesPoolRef};
use crate::tiles::{self, PendingTiles, TileRect};
@ -37,7 +37,7 @@ use crate::{get_gpu_state, performance};
pub use fonts::*;
pub use images::*;
type ClipStack = Vec<(Rect, Option<Corners>, Matrix)>;
type ClipStack = Vec<(Rect, Option<Corners>, Matrix, Matrix)>;
#[repr(u8)]
pub enum FrameType {
@ -70,22 +70,16 @@ pub struct NodeRenderState {
}
/// 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();
fn get_simplified_children<'a>(tree: ShapesPoolRef<'a>, shape: &'a Shape, result: &mut Vec<Uuid>) {
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));
get_simplified_children(tree, child, result);
} else {
// Child is not flattened: add it directly
result.push(*child_id);
}
}
}
result
}
impl NodeRenderState {
@ -108,7 +102,7 @@ impl NodeRenderState {
/// and need relative positioning adjustments.
fn append_clip(
clip_stack: Option<ClipStack>,
clip: (Rect, Option<Corners>, Matrix),
clip: (Rect, Option<Corners>, Matrix, Matrix),
) -> Option<ClipStack> {
match clip_stack {
Some(mut stack) => {
@ -151,7 +145,10 @@ impl NodeRenderState {
bounds.inset((clip_inset, clip_inset));
}
Self::append_clip(self.clip_bounds.clone(), (bounds, corners, transform))
Self::append_clip(
self.clip_bounds.clone(),
(bounds, corners, transform, transform.invert().unwrap_or_default()),
)
}
/// Calculates the clip bounds for shadow rendering of a given shape.
@ -205,7 +202,10 @@ impl NodeRenderState {
_ => None,
};
Self::append_clip(self.clip_bounds.clone(), (bounds, corners, transform))
Self::append_clip(
self.clip_bounds.clone(),
(bounds, corners, transform, transform.invert().unwrap_or_default()),
)
}
_ => self.clip_bounds.clone(),
}
@ -232,14 +232,14 @@ impl NodeRenderState {
/// - `is_active()` returns whether the current shape is being rendered in focus.
#[derive(Clone)]
pub struct FocusMode {
shapes: Vec<Uuid>,
shapes: HashSet<Uuid>,
active: bool,
}
impl FocusMode {
pub fn new() -> Self {
FocusMode {
shapes: Vec::new(),
shapes: HashSet::new(),
active: false,
}
}
@ -250,7 +250,7 @@ impl FocusMode {
}
pub fn set_shapes(&mut self, shapes: Vec<Uuid>) {
self.shapes = shapes;
self.shapes = shapes.into_iter().collect();
}
/// Returns `true` if the given shape ID should be focused.
@ -289,15 +289,12 @@ impl FocusMode {
fn sort_z_index(tree: ShapesPoolRef, element: &Shape, children_ids: Vec<Uuid>) -> Vec<Uuid> {
if element.has_layout() {
let mut ids = children_ids;
ids.sort_by_cached_key(|id| {
std::cmp::Reverse(tree.get(id).map(|s| s.z_index()).unwrap_or(0))
});
if element.is_flex() && !element.is_flex_reverse() {
ids.reverse();
}
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
@ -368,14 +365,17 @@ pub(crate) struct RenderState {
// Frames contained in groups must reset this nested_fills stack pushing a new empty vector.
pub nested_fills: Vec<Vec<Fill>>,
pub nested_blurs: Vec<Option<Blur>>, // FIXME: why is this an option?
pub cached_layer_blur: Option<Option<Blur>>,
pub nested_shadows: Vec<Vec<Shadow>>,
pub show_grid: Option<Uuid>,
pub empty_grid_frame_ids: HashSet<Uuid>,
pub rulers: RulerState,
pub focus_mode: FocusMode,
/// Viewer-only whitelist for fixed-scroll layer passes.
pub include_filter: Option<HashSet<Uuid>>,
/// Frame id passed as `base_object` for viewer renders; always traversed.
pub viewer_render_root: Option<Uuid>,
pub viewer_visible_set: Option<HashSet<Uuid>>,
pub touched_ids: HashSet<Uuid>,
/// Temporary flag used for off-screen passes (drop-shadow masks, filter surfaces, etc.)
/// where we must render shapes without inheriting ancestor layer blurs. Toggle it through
@ -499,10 +499,6 @@ impl RenderState {
return false;
}
if !self.backbuffer_crop_cache.contains_key(&node_id) {
return false;
}
// Additionally require this node to be safe to serve from a rectangular backbuffer
// crop while moving; otherwise it must be rendered live (e.g. text, overflow frames).
return tree
@ -568,12 +564,15 @@ impl RenderState {
pending_tiles: PendingTiles::new(),
nested_fills: vec![],
nested_blurs: vec![],
cached_layer_blur: None,
nested_shadows: vec![],
show_grid: None,
empty_grid_frame_ids: HashSet::default(),
rulers: RulerState::default(),
focus_mode: FocusMode::new(),
include_filter: None,
viewer_render_root: None,
viewer_visible_set: None,
touched_ids: HashSet::default(),
ignore_nested_blurs: false,
preview_mode: false,
@ -593,7 +592,10 @@ impl RenderState {
/// 3. Convert the total variance back to a radius with `blur_from_variance`.
///
/// This keeps blur math consistent everywhere we need to merge blur sources.
fn combined_layer_blur(&self, shape_blur: Option<Blur>) -> Option<Blur> {
fn combined_layer_blur(&mut self, shape_blur: Option<Blur>) -> Option<Blur> {
if let Some(ref cached) = self.cached_layer_blur {
return *cached;
}
let mut total = 0.;
for nested_blur in self.nested_blurs.iter().flatten() {
@ -602,7 +604,9 @@ impl RenderState {
total += Self::blur_variance(shape_blur);
Self::blur_from_variance(total)
let result = Self::blur_from_variance(total);
self.cached_layer_blur = Some(result);
result
}
/// Returns the variance (radius²) for a visible layer blur, or zero if the
@ -1038,6 +1042,11 @@ impl RenderState {
.draw_into(SurfaceId::InnerShadows, target, Some(&paint));
}
if self.surfaces.is_dirty(SurfaceId::DropShadows) {
self.surfaces
.draw_into(SurfaceId::DropShadows, target, Some(&paint));
}
// Build mask of dirty surfaces that need clearing
let mut dirty_surfaces_to_clear = 0u32;
if self.surfaces.is_dirty(SurfaceId::Strokes) {
@ -1052,6 +1061,9 @@ impl RenderState {
if self.surfaces.is_dirty(SurfaceId::TextDropShadows) {
dirty_surfaces_to_clear |= SurfaceId::TextDropShadows as u32;
}
if self.surfaces.is_dirty(SurfaceId::DropShadows) {
dirty_surfaces_to_clear |= SurfaceId::DropShadows as u32;
}
if dirty_surfaces_to_clear != 0 {
self.surfaces.apply_mut(dirty_surfaces_to_clear, |s| {
@ -1087,20 +1099,35 @@ impl RenderState {
self.surfaces.clear_tile_atlas();
}
/// True when the shape or any descendant is whitelisted.
pub fn shape_visible_in_include_filter(&self, shape_id: &Uuid, tree: ShapesPoolRef) -> bool {
/// Precompute the set of all ancestor ids that are visible for the viewer
/// masked pass, avoiding recursive checks on the hot path.
fn precompute_viewer_visible_set(&mut self, tree: ShapesPoolRef) {
self.viewer_visible_set = None;
let Some(ref include) = self.include_filter else {
return true;
return;
};
if include.contains(shape_id) {
return true;
let mut visible = include.clone();
for id in include.iter() {
let mut current_id = id;
while let Some(raw) = tree.get_raw(current_id) {
match raw.parent_id {
Some(ref parent_id) => {
visible.insert(*parent_id);
current_id = parent_id;
}
None => break,
}
}
}
self.viewer_visible_set = Some(visible);
}
/// True when the shape or any descendant is whitelisted.
pub fn shape_visible_in_include_filter(&self, shape_id: &Uuid, _tree: ShapesPoolRef) -> bool {
match &self.viewer_visible_set {
Some(visible) => visible.contains(shape_id),
None => true,
}
let Some(shape) = tree.get(shape_id) else {
return false;
};
shape
.children_ids_iter(false)
.any(|child_id| self.shape_visible_in_include_filter(child_id, tree))
}
/// When an include whitelist is active, only those ids are painted.
@ -1120,28 +1147,24 @@ impl RenderState {
}
fn get_inherited_drop_shadows(&self) -> Option<Vec<skia_safe::Paint>> {
let drop_shadows: Vec<&Shadow> = self
let drop_shadows: Vec<skia_safe::Paint> = self
.nested_shadows
.iter()
.flat_map(|shadows| shadows.iter())
.filter(|shadow| !shadow.hidden() && shadow.style() == crate::shapes::ShadowStyle::Drop)
.map(|shadow| {
let mut paint = skia_safe::Paint::default();
let filter = shadow.get_drop_shadow_filter();
paint.set_image_filter(filter);
paint
})
.collect();
if drop_shadows.is_empty() {
return None;
None
} else {
Some(drop_shadows)
}
Some(
drop_shadows
.into_iter()
.map(|shadow| {
let mut paint = skia_safe::Paint::default();
let filter = shadow.get_drop_shadow_filter();
paint.set_image_filter(filter);
paint
})
.collect(),
)
}
#[allow(clippy::too_many_arguments)]
@ -1258,7 +1281,7 @@ impl RenderState {
// set clipping
if let Some(clips) = clip_bounds.as_ref() {
let scale = self.get_scale();
for (mut bounds, corners, transform) in clips.iter() {
for (mut bounds, corners, transform, _inverse_transform) in clips.iter() {
self.surfaces.apply_mut(surface_ids, |s| {
s.canvas().concat(transform);
});
@ -1307,6 +1330,7 @@ impl RenderState {
// We don't want to change the value in the global state
let mut shape: Cow<Shape> = Cow::Borrowed(shape);
let shape = shape.to_mut();
// Remove background blur from the shape so it doesn't get processed
// as a layer blur. The actual rendering is done before the save_layer
@ -1320,25 +1344,25 @@ impl RenderState {
.blur
.is_some_and(|b| !b.hidden && b.blur_type == BlurType::BackgroundBlur)
{
shape.to_mut().set_blur(None);
shape.set_blur(None);
}
let frame_has_blur = Self::frame_clip_layer_blur(&shape).is_some();
let frame_has_blur = Self::frame_clip_layer_blur(shape).is_some();
let shape_has_blur = shape.blur.is_some();
if self.ignore_nested_blurs {
if frame_has_blur && shape_has_blur {
shape.to_mut().set_blur(None);
shape.set_blur(None);
}
} else if !frame_has_blur {
if let Some(blur) = self.combined_layer_blur(shape.blur) {
shape.to_mut().set_blur(Some(blur));
shape.set_blur(Some(blur));
}
} else if shape_has_blur {
shape.to_mut().set_blur(None);
shape.set_blur(None);
}
if skip_effects {
shape.to_mut().set_blur(None);
shape.set_blur(None);
}
// For non-text, non-SVG shapes in the normal rendering path, apply blur
@ -1351,7 +1375,7 @@ impl RenderState {
&& !matches!(shape.shape_type, Type::SVGRaw(_))
{
if let Some(blur) = shape.blur.filter(|b| !b.hidden) {
shape.to_mut().set_blur(None);
shape.set_blur(None);
Some(blur.sigma())
} else {
None
@ -1388,7 +1412,7 @@ impl RenderState {
match dom_result {
Ok(dom) => {
dom.render(self.surfaces.canvas_and_mark_dirty(fills_surface_id));
shape.to_mut().set_svg(dom);
shape.set_svg(dom);
}
Err(e) => {
eprintln!("Error parsing SVG. Error: {}", e);
@ -1439,7 +1463,7 @@ impl RenderState {
text::render(
Some(self),
None,
&shape,
shape,
&mut paragraph_builders,
Some(fills_surface_id),
None,
@ -1460,7 +1484,7 @@ impl RenderState {
text::render_inner_stroke(
Some(self),
None,
&shape,
shape,
&mut mask_builders,
stroke_paragraphs,
&mut fill_builders,
@ -1473,7 +1497,7 @@ impl RenderState {
text::render_with_bounds_outset(
Some(self),
None,
&shape,
shape,
stroke_paragraphs,
Some(strokes_surface_id),
None,
@ -1517,7 +1541,7 @@ impl RenderState {
text::render(
Some(self),
None,
&shape,
shape,
&mut paragraphs_with_shadows,
text_drop_shadows_surface_id.into(),
Some(&shadow),
@ -1529,7 +1553,7 @@ impl RenderState {
} else {
shadows::render_text_shadows(
self,
&shape,
shape,
&mut paragraphs_with_shadows,
&mut stroke_paragraphs_with_shadows_list,
text_drop_shadows_surface_id.into(),
@ -1546,7 +1570,7 @@ impl RenderState {
text::render(
Some(self),
None,
&shape,
shape,
&mut paragraphs_with_shadows,
text_drop_shadows_surface_id.into(),
Some(shadow),
@ -1561,7 +1585,7 @@ impl RenderState {
text::render(
Some(self),
None,
&shape,
shape,
&mut paragraph_builders,
Some(fills_surface_id),
None,
@ -1573,7 +1597,7 @@ impl RenderState {
// 3. Stroke drop shadows
shadows::render_text_shadows(
self,
&shape,
shape,
&mut paragraphs_with_shadows,
&mut stroke_paragraphs_with_shadows_list,
text_drop_shadows_surface_id.into(),
@ -1597,7 +1621,7 @@ impl RenderState {
text::render_inner_stroke(
Some(self),
None,
&shape,
shape,
&mut mask_builders,
stroke_paragraphs,
&mut fill_builders,
@ -1610,7 +1634,7 @@ impl RenderState {
text::render_with_bounds_outset(
Some(self),
None,
&shape,
shape,
stroke_paragraphs,
Some(strokes_surface_id),
None,
@ -1625,7 +1649,7 @@ impl RenderState {
// 5. Stroke inner shadows
shadows::render_text_shadows(
self,
&shape,
shape,
&mut paragraphs_with_shadows,
&mut stroke_paragraphs_with_shadows_list,
Some(innershadows_surface_id),
@ -1641,7 +1665,7 @@ impl RenderState {
text::render(
Some(self),
None,
&shape,
shape,
&mut paragraphs_with_shadows,
Some(innershadows_surface_id),
Some(shadow),
@ -1754,12 +1778,12 @@ impl RenderState {
};
if self.options.is_debug_visible() {
let shape_selrect_bounds = self.get_shape_selrect_bounds(&shape);
let shape_selrect_bounds = self.get_shape_selrect_bounds(shape);
debug::render_debug_shape(self, Some(shape_selrect_bounds), None);
}
if apply_to_current_surface {
self.draw_shape_surface_stack_into(Some(&shape), target_surface);
self.draw_shape_surface_stack_into(Some(shape), target_surface);
}
// Only restore if we saved (optimization for simple shapes)
@ -1812,7 +1836,7 @@ impl RenderState {
continue;
}
let doc_bounds = self.get_cached_extrect(shape, tree, 1.0);
let doc_bounds = shape.extrect(tree, 1.0);
if !doc_bounds.intersects(viewport) {
continue;
}
@ -2133,19 +2157,31 @@ impl RenderState {
self.surfaces.gc();
self.pending_nodes.clear();
if self.pending_nodes.capacity() < tree.len() {
self.pending_nodes
.reserve(tree.len() - self.pending_nodes.capacity());
}
self.pending_nodes.reserve(tree.len());
// Clear nested state stacks to avoid residual fills/blurs from previous renders
// being incorrectly applied to new frames
self.nested_fills.clear();
self.nested_blurs.clear();
self.cached_layer_blur = None;
self.nested_shadows.clear();
// reorder by distance to the center.
self.current_tile = None;
self.empty_grid_frame_ids.clear();
if self.show_grid.is_some() {
for shape in tree.iter() {
if shape.id.is_nil() || !shape.children.is_empty() {
continue;
}
if let Type::Frame(frame) = &shape.shape_type {
if matches!(frame.layout, Some(Layout::GridLayout(_, _))) && !shape.deleted() {
self.empty_grid_frame_ids.insert(shape.id);
}
}
}
}
}
pub fn start_render_loop(
@ -2156,6 +2192,7 @@ impl RenderState {
sync_render: bool,
) -> Result<FrameType> {
self.clear(tree);
self.precompute_viewer_visible_set(tree);
let _start = performance::begin_timed_log!("start_render_loop");
let scale = self.get_scale();
@ -2272,7 +2309,7 @@ impl RenderState {
let Some(shape) = tree.get(id) else {
continue;
};
let r = self.get_cached_extrect(shape, tree, 1.0);
let r = shape.extrect(tree, 1.0);
if r.is_empty() {
continue;
}
@ -2486,7 +2523,7 @@ impl RenderState {
.surfaces
.get_render_context_translation(self.render_area, scale);
for (bounds, corners, transform) in clips.iter() {
for (bounds, corners, transform, inverse_transform) in clips.iter() {
let mut total_matrix = Matrix::new_identity();
if target_surface == SurfaceId::Export {
let Some((export_rect, export_scale)) = self.export_context else {
@ -2510,7 +2547,7 @@ impl RenderState {
}
self.surfaces
.canvas(target_surface)
.concat(&total_matrix.invert().unwrap_or_default());
.concat(inverse_transform);
}
}
@ -2660,6 +2697,7 @@ impl RenderState {
Type::Frame(_) | Type::Group(_) => {
self.nested_fills.pop();
self.nested_blurs.pop();
self.cached_layer_blur = None;
self.nested_shadows.pop();
}
_ => {}
@ -2750,7 +2788,7 @@ impl RenderState {
pub fn get_shape_extrect_bounds(&mut self, shape: &Shape, tree: ShapesPoolRef) -> Rect {
let scale = self.get_scale();
let rect = self.get_cached_extrect(shape, tree, scale);
let rect = shape.extrect(tree, scale);
self.get_rect_bounds(rect)
}
@ -3045,11 +3083,10 @@ impl RenderState {
)?;
if !matches!(element.shape_type, Type::Bool(_)) {
let shadow_children = if element.is_recursive() {
get_simplified_children(tree, element)
} else {
Vec::new()
};
let mut shadow_children = Vec::new();
if element.is_recursive() {
get_simplified_children(tree, element, &mut shadow_children);
}
for shadow_shape_id in shadow_children.iter() {
let Some(shadow_shape) = tree.get(shadow_shape_id) else {
@ -3166,7 +3203,7 @@ impl RenderState {
for id in modifier_ids.iter() {
// Current (post-modifier) bounds
if let Some(s) = tree.get(id) {
let r = self.get_cached_extrect(s, tree, 1.0);
let r = s.extrect(tree, 1.0);
acc = Some(match acc {
None => r,
Some(mut prev) => {
@ -3180,7 +3217,7 @@ impl RenderState {
// shape at its original position are considered "unsafe" even after the shape
// has moved away (e.g. dragging a child out of a clipped frame).
if let Some(raw) = tree.get_raw(id) {
let r0 = self.get_cached_extrect(raw, tree, 1.0);
let r0 = raw.extrect(tree, 1.0);
acc = Some(match acc {
None => r0,
Some(mut prev) => {
@ -3466,12 +3503,15 @@ impl RenderState {
match element.shape_type {
Type::Frame(_) if Self::frame_clip_layer_blur(element).is_some() => {
self.nested_blurs.push(None);
self.cached_layer_blur = None;
}
Type::Group(_) if element.masked_group_layer_blur().is_some() => {
self.nested_blurs.push(None);
self.cached_layer_blur = None;
}
Type::Frame(_) | Type::Group(_) => {
self.nested_blurs.push(element.blur);
self.cached_layer_blur = None;
}
_ => {}
}
@ -3501,13 +3541,12 @@ impl RenderState {
clip_inset_for_children,
);
let children_ids: Vec<_> = if can_flatten {
// Container was flattened: get simplified children (which skip this level)
get_simplified_children(tree, element)
let mut children_ids: Vec<_> = Vec::new();
if can_flatten {
get_simplified_children(tree, element, &mut children_ids);
} else {
// Container not flattened: use original children
element.children_ids_iter(false).copied().collect()
};
children_ids = element.children_ids_iter(false).copied().collect();
}
let children_ids = sort_z_index(tree, element, children_ids);
@ -3714,7 +3753,7 @@ impl RenderState {
*/
pub fn get_tiles_for_shape(&mut self, shape: &Shape, tree: ShapesPoolRef) -> TileRect {
let scale = self.get_scale();
let extrect = self.get_cached_extrect(shape, tree, scale);
let extrect = shape.extrect(tree, scale);
let tile_size = tiles::get_tile_size(scale);
let shape_tiles = tiles::get_tiles_for_rect(extrect, tile_size);
let interest_rect = &self.tile_viewbox.interest_rect;
@ -4045,10 +4084,6 @@ impl RenderState {
self.touched_ids.clear();
}
pub fn get_cached_extrect(&mut self, shape: &Shape, tree: ShapesPoolRef, scale: f32) -> Rect {
shape.extrect(tree, scale)
}
pub fn set_view(&mut self, zoom: f32, x: f32, y: f32) {
self.viewbox.set_all(zoom, x, y);
}

View File

@ -163,6 +163,10 @@ pub fn render_debug_shape(
shape_selrect: Option<skia::Rect>,
shape_extrect: Option<skia::Rect>,
) {
if shape_selrect.is_none() && shape_extrect.is_none() {
return;
}
let canvas = render_state.surfaces.canvas(SurfaceId::Debug);
let mut paint = skia::Paint::default();

View File

@ -27,13 +27,18 @@ fn draw_image_fill(
let mut image_paint = skia::Paint::default();
image_paint.set_anti_alias(antialias);
if let Some(filter) = shape.image_filter(1.) {
let filter = shape.image_filter(1.);
if let Some(ref filter) = filter {
image_paint.set_image_filter(filter.clone());
}
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&image_paint);
// Save the current canvas state
canvas.save_layer(&layer_rec);
let has_image_filter = filter.is_some();
if has_image_filter {
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&image_paint);
canvas.save_layer(&layer_rec);
} else {
canvas.save();
}
// Set the clipping rectangle to the container bounds
match &shape.shape_type {
@ -87,7 +92,6 @@ fn draw_image_fill(
paint,
);
// Restore the canvas to remove the clipping
canvas.restore();
}

View File

@ -119,11 +119,18 @@ where
{
let canvas = render_state.surfaces.canvas(filter_id);
canvas.clear(skia::Color::TRANSPARENT);
canvas.save();
// Apply scale first, then translate
canvas.scale((scale, scale));
canvas.translate((-bounds.left, -bounds.top));
let scaled_bounds = Rect::new(
bounds.left,
bounds.top,
bounds.right,
bounds.bottom,
);
canvas.clip_rect(scaled_bounds, skia::ClipOp::Intersect, false);
canvas.clear(skia::Color::TRANSPARENT);
}
draw_fn(render_state, filter_id)?;

View File

@ -26,6 +26,7 @@ pub struct FontStore {
debug_font: Font,
ui_font: Font,
fallback_fonts: HashSet<String>,
registered_families: HashSet<String>,
}
impl FontStore {
@ -55,6 +56,7 @@ impl FontStore {
debug_font,
ui_font,
fallback_fonts: HashSet::new(),
registered_families: HashSet::new(),
})
}
@ -105,7 +107,7 @@ impl FontStore {
};
self.font_provider.register_typeface(typeface, font_name);
self.font_collection.clear_caches();
self.registered_families.insert(font_name.to_string());
if is_fallback {
self.fallback_fonts.insert(alias);
@ -121,13 +123,17 @@ impl FontStore {
} else {
alias.as_str()
};
self.font_provider.family_names().any(|x| x == font_name)
self.registered_families.contains(font_name)
}
pub fn get_fallback(&self) -> &HashSet<String> {
&self.fallback_fonts
}
pub fn flush_caches(&mut self) {
self.font_collection.clear_caches();
}
pub fn get_emoji_font(&self, _size: f32) -> Option<Font> {
None
}

View File

@ -6,6 +6,7 @@ use crate::error::Result;
use crate::get_gpu_state;
use skia_safe::gpu::{surfaces, Budgeted, DirectContext};
use skia_safe::{self as skia, Codec, ISize};
use std::cell::RefCell;
use std::collections::HashMap;
pub type Image = skia::Image;
@ -60,8 +61,8 @@ enum StoredImage {
}
pub struct ImageStore {
images: HashMap<(Uuid, bool), StoredImage>,
context: Box<DirectContext>,
images: RefCell<HashMap<(Uuid, bool), StoredImage>>,
context: RefCell<Box<DirectContext>>,
}
/// Creates a Skia image from an existing WebGL texture.
@ -148,8 +149,8 @@ impl ImageStore {
let gpu_state = get_gpu_state();
let context = &gpu_state.context;
Self {
images: HashMap::with_capacity(2048),
context: Box::new(context.clone()),
images: RefCell::new(HashMap::new()),
context: RefCell::new(Box::new(context.clone())),
}
}
@ -161,16 +162,14 @@ impl ImageStore {
) -> crate::error::Result<()> {
let key = (id, is_thumbnail);
if self.images.contains_key(&key) {
if self.images.borrow().contains_key(&key) {
return Ok(());
}
let raw_data = image_data.to_vec();
if let Some(gpu_image) = decode_image(&mut self.context, &raw_data) {
self.images.insert(key, StoredImage::Gpu(gpu_image));
if let Some(gpu_image) = decode_image(&mut self.context.borrow_mut(), image_data) {
self.images.borrow_mut().insert(key, StoredImage::Gpu(gpu_image));
} else {
self.images.insert(key, StoredImage::Raw(raw_data));
self.images.borrow_mut().insert(key, StoredImage::Raw(image_data.to_vec()));
}
Ok(())
}
@ -188,43 +187,38 @@ impl ImageStore {
) -> Result<()> {
let key = (id, is_thumbnail);
if self.images.contains_key(&key) {
if self.images.borrow().contains_key(&key) {
return Ok(());
}
// Create a Skia image from the existing GL texture
let image = create_image_from_gl_texture(&mut self.context, texture_id, width, height)?;
self.images.insert(key, StoredImage::Gpu(image));
let image = create_image_from_gl_texture(&mut self.context.borrow_mut(), texture_id, width, height)?;
self.images.borrow_mut().insert(key, StoredImage::Gpu(image));
Ok(())
}
pub fn contains(&self, id: &Uuid, is_thumbnail: bool) -> bool {
self.images.contains_key(&(*id, is_thumbnail))
self.images.borrow().contains_key(&(*id, is_thumbnail))
}
pub fn get(&mut self, id: &Uuid) -> Option<&Image> {
// Try to get full image first, fallback to thumbnail
let has_full = self.images.contains_key(&(*id, false));
if has_full {
self.get_internal(id, false)
} else {
self.get_internal(id, true)
}
pub fn get(&self, id: &Uuid) -> Option<&Image> {
self.get_internal(id, false)
.or_else(|| self.get_internal(id, true))
}
fn get_internal(&mut self, id: &Uuid, is_thumbnail: bool) -> Option<&Image> {
fn get_internal(&self, id: &Uuid, is_thumbnail: bool) -> Option<&Image> {
let key = (*id, is_thumbnail);
// Use entry API to mutate the HashMap in-place if needed
if let Some(entry) = self.images.get_mut(&key) {
let mut images = self.images.borrow_mut();
if let Some(entry) = images.get_mut(&key) {
match entry {
StoredImage::Gpu(ref img) => Some(img),
StoredImage::Gpu(ref img) => Some(unsafe { &*(img as *const Image) }),
StoredImage::Raw(raw_data) => {
let gpu_image = decode_image(&mut self.context, raw_data)?;
let gpu_image = decode_image(&mut self.context.borrow_mut(), raw_data)?;
*entry = StoredImage::Gpu(gpu_image);
if let StoredImage::Gpu(ref img) = entry {
Some(img)
Some(unsafe { &*(img as *const Image) })
} else {
None
}

View File

@ -80,7 +80,7 @@ impl RenderOptions {
// TODO: this will likely need to change once we have the tile atlas in place
self.dpr_viewport_interest_area_threshold =
((self.dpr * self.viewport_interest_area_threshold as f32).ceil() as i32)
.min(MIN_DPR_VIEWPORT_INTEREST_AREA_THRESHOLD);
.max(MIN_DPR_VIEWPORT_INTEREST_AREA_THRESHOLD);
}
/// Sets the devicePixelRatio.

View File

@ -297,13 +297,14 @@ fn handle_stroke_caps(
blur: Option<&ImageFilter>,
_antialias: bool,
) {
// Closed shapes don't have caps
if !is_open {
return;
}
// When both ends share the same simple line cap, Skia already drew it
// natively via `PaintCap` on the stroke paint, so skip the manual overlay.
if stroke.cap_start.is_none() && stroke.cap_end.is_none() {
return;
}
if stroke.to_skia_linecap().is_some() {
return;
}

View File

@ -166,8 +166,10 @@ impl DocAtlas {
new_bottom = new_bottom.max(doc_rect.bottom.ceil());
}
// Add padding to reduce realloc frequency.
let pad = tiles::TILE_SIZE;
// Geometric over-allocation: pad by 25% of extent to reduce realloc frequency.
let doc_extent_w = new_right - new_left;
let doc_extent_h = new_bottom - new_top;
let pad = (doc_extent_w.min(doc_extent_h) * 0.25_f32).max(TILE_SIZE as f32);
new_left -= pad;
new_top -= pad;
new_right += pad;
@ -758,7 +760,11 @@ impl Surfaces {
if ids & SurfaceId::Export as u32 != 0 {
f(self.get_mut(SurfaceId::Export));
}
performance::begin_measure!("apply_mut::flags");
performance::end_measure!("apply_mut::flags");
}
pub fn apply_one(&mut self, surface_id: SurfaceId, mut f: impl FnMut(&mut skia::Surface)) {
f(self.get_mut(surface_id));
}
pub fn get_render_context_translation(
@ -789,13 +795,6 @@ impl Surfaces {
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);
self.mark_dirty(SurfaceId::DropShadows);
// Update transformations
self.apply_mut(surface_ids, |s| {
let canvas = s.canvas();
@ -1172,13 +1171,14 @@ impl Surfaces {
self.atlas.tile_doc_rects.insert(*tile, tile_doc_rect);
// Draws current tile into tile atlas
let tile_ref = self.tiles.add(tile_viewbox, tile);
self.tile_atlas.canvas().draw_image_rect(
&tile_image,
None,
tile_ref.rect,
&skia::Paint::default(),
);
if let Some(tile_ref) = self.tiles.add(tile_viewbox, tile) {
self.tile_atlas.canvas().draw_image_rect(
&tile_image,
None,
tile_ref.rect,
&skia::Paint::default(),
);
}
}
}
@ -1305,9 +1305,7 @@ impl Surfaces {
}
pub fn get_tile_image_from_tile_atlas(&mut self, tile: Tile) -> Option<skia::Image> {
let Some(tile_ref) = self.tiles.get(tile) else {
panic!("Tile not found {}:{}", tile.0, tile.1);
};
let tile_ref = self.tiles.get(tile)?;
let rect = IRect::from_ltrb(
tile_ref.rect.left as i32,
@ -1437,7 +1435,7 @@ impl Surfaces {
}
if let Some(surface) = self
.shape_strokes
.shape_fills
.new_surface_with_dimensions((max_w, max_h))
{
self.shape_fills = surface;
@ -1542,8 +1540,9 @@ impl TileTextureCache {
fn gc(&mut self) {
// Make a real remove
for tile in self.removed.iter() {
if let Some(tile_ref) = self.grid.remove(tile) {
let removed = std::mem::take(&mut self.removed);
for tile in removed {
if let Some(tile_ref) = self.grid.remove(&tile) {
self.provider.deallocate(tile_ref);
}
}
@ -1618,7 +1617,7 @@ impl TileTextureCache {
self.grid.contains_key(&tile) && !self.removed.contains(&tile)
}
pub fn add(&mut self, tile_viewbox: &TileViewbox, tile: &Tile) -> TileAtlasTextureRef {
pub fn add(&mut self, tile_viewbox: &TileViewbox, tile: &Tile) -> Option<TileAtlasTextureRef> {
if self.grid.len() > TEXTURES_CACHE_CAPACITY {
// First we try to remove the obsolete tiles.
self.gc();
@ -1631,9 +1630,7 @@ impl TileTextureCache {
self.gc_non_visible(tile_viewbox);
}
let Some(tile_ref) = self.provider.allocate() else {
panic!("Tile texture allocation failed {}:{}", tile.0, tile.1);
};
let tile_ref = self.provider.allocate()?;
self.grid.insert(*tile, tile_ref.clone());
@ -1641,7 +1638,7 @@ impl TileTextureCache {
self.removed.remove(tile);
}
tile_ref.clone()
Some(tile_ref.clone())
}
pub fn get(&mut self, tile: Tile) -> Option<&TileAtlasTextureRef> {

View File

@ -28,24 +28,26 @@ pub fn stroke_paragraph_builder_group_from_text(
let mut group_layer_opacity: Option<f32> = None;
for paragraph in text_content.paragraphs() {
let mut stroke_paragraphs_map: std::collections::HashMap<usize, ParagraphBuilder> =
std::collections::HashMap::new();
let mut stroke_paragraphs: Vec<ParagraphBuilder> = Vec::new();
let (stroke_paints, stroke_layer_opacity) =
get_text_stroke_paints(stroke, bounds, remove_stroke_alpha);
if group_layer_opacity.is_none() {
group_layer_opacity = stroke_layer_opacity;
}
for span in paragraph.children().iter() {
let (stroke_paints, stroke_layer_opacity) =
get_text_stroke_paints(stroke, bounds, remove_stroke_alpha);
if group_layer_opacity.is_none() {
group_layer_opacity = stroke_layer_opacity;
}
let text: String = span.apply_text_transform();
for (paint_idx, stroke_paint) in stroke_paints.iter().enumerate() {
let builder = stroke_paragraphs_map.entry(paint_idx).or_insert_with(|| {
if stroke_paragraphs.len() < stroke_paints.len() {
for _stroke_paint in stroke_paints.iter() {
let paragraph_style = paragraph.paragraph_to_style();
ParagraphBuilder::new(&paragraph_style, fonts)
});
stroke_paragraphs.push(ParagraphBuilder::new(&paragraph_style, fonts));
}
}
for (paint_idx, stroke_paint) in stroke_paints.iter().enumerate() {
let builder = &mut stroke_paragraphs[paint_idx];
let stroke_paint = stroke_paint.clone();
let remove_alpha = use_shadow.unwrap_or(false) && !span.is_transparent();
let stroke_style = span.to_stroke_style(
@ -59,10 +61,6 @@ pub fn stroke_paragraph_builder_group_from_text(
}
}
let stroke_paragraphs: Vec<ParagraphBuilder> = (0..stroke_paragraphs_map.len())
.filter_map(|i| stroke_paragraphs_map.remove(&i))
.collect();
paragraph_group.push(stroke_paragraphs);
}
@ -321,7 +319,7 @@ fn draw_text(
let layer_rec = SaveLayerRec::default().paint(&opacity_paint);
canvas.save_layer(&layer_rec);
} else {
canvas.save_layer(&SaveLayerRec::default());
canvas.save();
}
paint_text(canvas, shape, paragraph_builder_groups);

View File

@ -2,7 +2,6 @@ use skia_safe::{self as skia, Color4f};
use super::{RenderState, ShapesPoolRef, SurfaceId};
use crate::render::{grid_layout, rulers};
use crate::shapes::{Layout, Type};
pub fn render(render_state: &mut RenderState, shapes: ShapesPoolRef) {
let canvas = render_state.surfaces.canvas(SurfaceId::UI);
@ -28,28 +27,13 @@ pub fn render(render_state: &mut RenderState, shapes: ShapesPoolRef) {
}
// Render overlays for empty grid frames
for shape in shapes.iter() {
if shape.id.is_nil() || !shape.children.is_empty() {
let empty_grid_ids: std::collections::HashSet<crate::uuid::Uuid> =
std::mem::take(&mut render_state.empty_grid_frame_ids);
for id in &empty_grid_ids {
if show_grid_id == Some(*id) {
continue;
}
if show_grid_id == Some(shape.id) {
continue;
}
let Type::Frame(frame) = &shape.shape_type else {
continue;
};
if !matches!(frame.layout, Some(Layout::GridLayout(_, _))) {
continue;
}
if shape.deleted() {
continue;
}
if let Some(shape) = shapes.get(&shape.id) {
if let Some(shape) = shapes.get(id) {
grid_layout::render_overlay(
zoom,
render_state.options.antialias_threshold,
@ -59,6 +43,7 @@ pub fn render(render_state: &mut RenderState, shapes: ShapesPoolRef) {
);
}
}
render_state.empty_grid_frame_ids = empty_grid_ids;
let viewbox = render_state.viewbox;
let ruler_state = render_state.rulers;

View File

@ -234,7 +234,7 @@ fn set_auto_multi_span(
track.max_size
};
let aloc = new_size - track.size;
let aloc = new_size - track.size;
dist -= aloc;
track.size += aloc;
}
@ -314,9 +314,7 @@ fn set_flex_multi_span(
// Distribute the space between flex tracks in proportion to the division
for track in tracks[start..end].iter_mut() {
if track.track_type == GridTrackType::Flex {
let new_size = alloc.clamp(track.size, track.max_size);
let aloc = new_size - track.size;
dist -= aloc;
let new_size = alloc.clamp(track.size, track.max_size);
track.size = new_size;
}
}

View File

@ -70,3 +70,8 @@ pub extern "C" fn is_font_uploaded(
res
}
#[no_mangle]
pub extern "C" fn flush_font_caches() {
get_render_state().fonts_mut().flush_caches();
}