From a3764b9713befbea432a6c49b015c04fcfad622d Mon Sep 17 00:00:00 2001 From: Elena Torro Date: Wed, 4 Feb 2026 11:01:58 +0100 Subject: [PATCH 1/9] :wrench: Avoid clone in rebuild_touched_tiles Use std::mem::take instead of clone to avoid HashSet allocation. The set was cleared anyway by clean_touched(), so take() is safe. --- render-wasm/src/render.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index b1968e7e99..06164bec1b 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -2307,7 +2307,7 @@ impl RenderState { let mut all_tiles = HashSet::::new(); - let ids = self.touched_ids.clone(); + let ids = std::mem::take(&mut self.touched_ids); for shape_id in ids.iter() { if let Some(shape) = tree.get(shape_id) { @@ -2322,8 +2322,6 @@ impl RenderState { self.remove_cached_tile(tile); } - self.clean_touched(); - performance::end_measure!("rebuild_touched_tiles"); } From 8ef6600cdc9967feff35e26c1e303d6a248c41de Mon Sep 17 00:00:00 2001 From: Elena Torro Date: Wed, 4 Feb 2026 11:02:17 +0100 Subject: [PATCH 2/9] :wrench: Return HashSet from update_shape_tiles Avoid final collect() allocation by returning HashSet directly. Callers already use extend() which works with both types. --- render-wasm/src/render.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 06164bec1b..ee2290cf04 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -2189,17 +2189,16 @@ impl RenderState { * Given a shape, check the indexes and update it's location in the tile set * returns the tiles that have changed in the process. */ - pub fn update_shape_tiles(&mut self, shape: &Shape, tree: ShapesPoolRef) -> Vec { + pub fn update_shape_tiles(&mut self, shape: &Shape, tree: ShapesPoolRef) -> HashSet { let TileRect(rsx, rsy, rex, rey) = self.get_tiles_for_shape(shape, tree); - let old_tiles = self + // Collect old tiles to avoid borrow conflict with remove_shape_at + let old_tiles: Vec<_> = self .tiles .get_tiles_of(shape.id) - .map_or(Vec::new(), |tiles| tiles.iter().copied().collect()); + .map_or(Vec::new(), |t| t.iter().copied().collect()); - let new_tiles = (rsx..=rex).flat_map(|x| (rsy..=rey).map(move |y| tiles::Tile::from(x, y))); - - let mut result = HashSet::::new(); + let mut result = HashSet::::with_capacity(old_tiles.len()); // First, remove the shape from all tiles where it was previously located for tile in old_tiles { @@ -2208,12 +2207,12 @@ impl RenderState { } // Then, add the shape to the new tiles - for tile in new_tiles { + for tile in (rsx..=rex).flat_map(|x| (rsy..=rey).map(move |y| tiles::Tile::from(x, y))) { self.tiles.add_shape_at(tile, shape.id); result.insert(tile); } - result.iter().copied().collect() + result } /* From c87ffdcd301e49d2c79a49c84e1cb7ee0937dccf Mon Sep 17 00:00:00 2001 From: Elena Torro Date: Wed, 4 Feb 2026 11:02:46 +0100 Subject: [PATCH 3/9] :wrench: Add forward children iterator for flex layout Avoid Vec allocation + reverse for reversed flex layouts. The new children_ids_iter_forward returns children in original order, eliminating the need to collect and reverse. --- render-wasm/src/shapes.rs | 19 +++++++++++++++++++ .../src/shapes/modifiers/flex_layout.rs | 15 +++++++++------ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index 48c3bda1c7..1edb377e35 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -1119,6 +1119,25 @@ impl Shape { } } + /// Returns children in forward (non-reversed) order - useful for layout calculations + pub fn children_ids_iter_forward(&self, include_hidden: bool) -> Box + '_> { + if include_hidden { + return Box::new(self.children.iter()); + } + + if let Type::Bool(_) = self.shape_type { + Box::new([].iter()) + } else if let Type::Group(group) = self.shape_type { + if group.masked { + Box::new(self.children.iter().skip(1)) + } else { + Box::new(self.children.iter()) + } + } else { + Box::new(self.children.iter()) + } + } + pub fn all_children( &self, shapes: ShapesPoolRef, diff --git a/render-wasm/src/shapes/modifiers/flex_layout.rs b/render-wasm/src/shapes/modifiers/flex_layout.rs index 6377379306..74159dd2e7 100644 --- a/render-wasm/src/shapes/modifiers/flex_layout.rs +++ b/render-wasm/src/shapes/modifiers/flex_layout.rs @@ -184,15 +184,18 @@ fn initialize_tracks( ) -> Vec { let mut tracks = Vec::::new(); let mut current_track = TrackData::default(); - let mut children = shape.children_ids(true); let mut first = true; - if flex_data.is_reverse() { - children.reverse(); - } + // When is_reverse() is true, we need forward order (children_ids_iter_forward). + // When is_reverse() is false, we need reversed order (children_ids_iter). + let children_iter: Box> = if flex_data.is_reverse() { + Box::new(shape.children_ids_iter_forward(true).copied()) + } else { + Box::new(shape.children_ids_iter(true).copied()) + }; - for child_id in children.iter() { - let Some(child) = shapes.get(child_id) else { + for child_id in children_iter { + let Some(child) = shapes.get(&child_id) else { continue; }; From 216d4002623b0bb628dbb8e8727350484f3be887 Mon Sep 17 00:00:00 2001 From: Elena Torro Date: Wed, 4 Feb 2026 11:03:26 +0100 Subject: [PATCH 4/9] :wrench: Prevent duplicate layout calculations Use HashSet for layout_reflows to avoid processing the same layout multiple times. Also use std::mem::take instead of creating a new Vec on each iteration. --- render-wasm/src/shapes/modifiers.rs | 49 ++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/render-wasm/src/shapes/modifiers.rs b/render-wasm/src/shapes/modifiers.rs index 1af06713a7..344935a73f 100644 --- a/render-wasm/src/shapes/modifiers.rs +++ b/render-wasm/src/shapes/modifiers.rs @@ -300,7 +300,20 @@ fn propagate_reflow( Type::Frame(Frame { layout: Some(_), .. }) => { - layout_reflows.insert(*id); + let mut skip_reflow = false; + if shape.is_layout_horizontal_fill() || shape.is_layout_vertical_fill() { + if let Some(parent_id) = shape.parent_id { + if parent_id != Uuid::nil() && !reflown.contains(&parent_id) { + // If this is a fill layout but the parent has not been reflown yet + // we wait for the next iteration for reflow + skip_reflow = true; + } + } + } + + if !skip_reflow { + layout_reflows.insert(*id); + } } Type::Group(Group { masked: true }) => { let children_ids = shape.children_ids(true); @@ -417,26 +430,32 @@ pub fn propagate_modifiers( } } } + // let mut layout_reflows_vec: Vec = layout_reflows.into_iter().collect(); - let mut layout_reflows_vec: Vec = layout_reflows.into_iter().collect(); + // // We sort the reflows so they are process first the ones that are more + // // deep in the tree structure. This way we can be sure that the children layouts + // // are already reflowed. + // layout_reflows_vec.sort_unstable_by(|id_a, id_b| { + // let da = shapes.get_depth(id_a); + // let db = shapes.get_depth(id_b); + // db.cmp(&da) + // }); - // We sort the reflows so they are process first the ones that are more - // deep in the tree structure. This way we can be sure that the children layouts - // are already reflowed. - layout_reflows_vec.sort_unstable_by(|id_a, id_b| { - let da = shapes.get_depth(id_a); - let db = shapes.get_depth(id_b); - db.cmp(&da) - }); + // let mut bounds_temp = bounds.clone(); + // for id in layout_reflows_vec.iter() { + // if reflown.contains(id) { + // continue; + // } + // reflow_shape(id, state, &mut reflown, &mut entries, &mut bounds_temp); + // } + // layout_reflows = HashSet::new(); - let mut bounds_temp = bounds.clone(); - for id in layout_reflows_vec.iter() { - if reflown.contains(id) { + for id in std::mem::take(&mut layout_reflows) { + if reflown.contains(&id) { continue; } - reflow_shape(id, state, &mut reflown, &mut entries, &mut bounds_temp); + reflow_shape(&id, state, &mut reflown, &mut entries, &mut bounds); } - layout_reflows = HashSet::new(); } modifiers From 2d9a2e0d5015deb3d72bf656b9157ff5cbdf206e Mon Sep 17 00:00:00 2001 From: Elena Torro Date: Wed, 4 Feb 2026 11:04:08 +0100 Subject: [PATCH 5/9] :wrench: Use swap_remove in flex layout distribution swap_remove is O(1) vs remove's O(n) when order doesn't matter. These loops iterate backwards, so swap_remove is safe. --- render-wasm/src/shapes/modifiers/flex_layout.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/render-wasm/src/shapes/modifiers/flex_layout.rs b/render-wasm/src/shapes/modifiers/flex_layout.rs index 74159dd2e7..72191a32a2 100644 --- a/render-wasm/src/shapes/modifiers/flex_layout.rs +++ b/render-wasm/src/shapes/modifiers/flex_layout.rs @@ -296,7 +296,7 @@ fn distribute_fill_main_space(layout_axis: &LayoutAxis, tracks: &mut [TrackData] track.main_size += delta; if (child.main_size - child.max_main_size).abs() < MIN_SIZE { - to_resize_children.remove(i); + to_resize_children.swap_remove(i); } } } @@ -333,7 +333,7 @@ fn distribute_fill_across_space(layout_axis: &LayoutAxis, tracks: &mut [TrackDat left_space -= delta; if (track.across_size - track.max_across_size).abs() < MIN_SIZE { - to_resize_tracks.remove(i); + to_resize_tracks.swap_remove(i); } } } From 2ccd2a66797eb3e0fe6b2bd474339827f325dcf2 Mon Sep 17 00:00:00 2001 From: Elena Torro Date: Wed, 4 Feb 2026 11:04:47 +0100 Subject: [PATCH 6/9] :wrench: Use HashSet for grid layout children lookup HashSet provides O(1) contains() vs Vec's O(n), improving child lookup performance in grid cell data creation. --- render-wasm/src/shapes/modifiers/grid_layout.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/render-wasm/src/shapes/modifiers/grid_layout.rs b/render-wasm/src/shapes/modifiers/grid_layout.rs index 93e7ac571e..7b2a314989 100644 --- a/render-wasm/src/shapes/modifiers/grid_layout.rs +++ b/render-wasm/src/shapes/modifiers/grid_layout.rs @@ -6,7 +6,7 @@ use crate::shapes::{ }; use crate::state::ShapesPoolRef; use crate::uuid::Uuid; -use std::collections::{HashMap, VecDeque}; +use std::collections::{HashMap, HashSet, VecDeque}; use super::common::GetBounds; @@ -537,7 +537,7 @@ fn cell_bounds( pub fn create_cell_data<'a>( layout_bounds: &Bounds, - children: &[Uuid], + children: &HashSet, shapes: ShapesPoolRef<'a>, cells: &Vec, column_tracks: &[TrackData], @@ -614,7 +614,7 @@ pub fn grid_cell_data<'a>( let bounds = &mut HashMap::::new(); let layout_bounds = shape.bounds(); - let children = shape.children_ids(false); + let children: HashSet = shape.children_ids_iter(false).copied().collect(); let column_tracks = calculate_tracks( true, @@ -707,7 +707,7 @@ pub fn reflow_grid_layout( ) -> VecDeque { let mut result = VecDeque::new(); let layout_bounds = bounds.find(shape); - let children = shape.children_ids(true); + let children: HashSet = shape.children_ids_iter(true).copied().collect(); let column_tracks = calculate_tracks( true, From e1ce97a2b4e48903ad90544a9508d26ae21b81b8 Mon Sep 17 00:00:00 2001 From: Elena Torro Date: Wed, 4 Feb 2026 11:05:14 +0100 Subject: [PATCH 7/9] :wrench: Prioritize visible tiles over interest-area tiles Partition pending tiles into 4 groups by visibility and cache status. Visible tiles are processed first to eliminate empty squares during pan/zoom. Cached tiles within each group are processed before uncached. --- render-wasm/src/tiles.rs | 71 ++++++++++++++++++++++++++++------------ 1 file changed, 50 insertions(+), 21 deletions(-) diff --git a/render-wasm/src/tiles.rs b/render-wasm/src/tiles.rs index 7012a2e24b..02bd5c5eb5 100644 --- a/render-wasm/src/tiles.rs +++ b/render-wasm/src/tiles.rs @@ -209,16 +209,19 @@ impl PendingTiles { } } - pub fn update(&mut self, tile_viewbox: &TileViewbox, surfaces: &Surfaces) { - self.list.clear(); - - let columns = tile_viewbox.interest_rect.width(); - let rows = tile_viewbox.interest_rect.height(); - + // Generate tiles in spiral order from center + fn generate_spiral(rect: &TileRect) -> Vec { + let columns = rect.width(); + let rows = rect.height(); let total = columns * rows; - let mut cx = tile_viewbox.interest_rect.center_x(); - let mut cy = tile_viewbox.interest_rect.center_y(); + if total <= 0 { + return Vec::new(); + } + + let mut result = Vec::with_capacity(total as usize); + let mut cx = rect.center_x(); + let mut cy = rect.center_y(); let ratio = (columns as f32 / rows as f32).ceil() as i32; @@ -228,7 +231,7 @@ impl PendingTiles { let mut direction = 0; let mut current = 0; - self.list.push(Tile(cx, cy)); + result.push(Tile(cx, cy)); while current < total { match direction { 0 => cx += 1, @@ -238,7 +241,7 @@ impl PendingTiles { _ => unreachable!("Invalid direction"), } - self.list.push(Tile(cx, cy)); + result.push(Tile(cx, cy)); direction_current += 1; let direction_total = if direction % 2 == 0 { @@ -258,18 +261,44 @@ impl PendingTiles { } current += 1; } - self.list.reverse(); + result.reverse(); + result + } - // Create a new list where the cached tiles go first - let iter1 = self - .list - .iter() - .filter(|t| surfaces.has_cached_tile_surface(**t)); - let iter2 = self - .list - .iter() - .filter(|t| !surfaces.has_cached_tile_surface(**t)); - self.list = iter1.chain(iter2).copied().collect(); + pub fn update(&mut self, tile_viewbox: &TileViewbox, surfaces: &Surfaces) { + self.list.clear(); + + // Generate spiral for the interest area (viewport + margin) + let spiral = Self::generate_spiral(&tile_viewbox.interest_rect); + + // Partition tiles into 4 priority groups (highest priority = processed last due to pop()): + // 1. visible + cached (fastest - just blit from cache) + // 2. visible + uncached (user sees these, render next) + // 3. interest + cached (pre-rendered area, blit from cache) + // 4. interest + uncached (lowest priority - background pre-render) + let mut visible_cached = Vec::new(); + let mut visible_uncached = Vec::new(); + let mut interest_cached = Vec::new(); + let mut interest_uncached = Vec::new(); + + for tile in spiral { + let is_visible = tile_viewbox.visible_rect.contains(&tile); + let is_cached = surfaces.has_cached_tile_surface(tile); + + match (is_visible, is_cached) { + (true, true) => visible_cached.push(tile), + (true, false) => visible_uncached.push(tile), + (false, true) => interest_cached.push(tile), + (false, false) => interest_uncached.push(tile), + } + } + + // Build final list with lowest priority first (they get popped last) + // Order: interest_uncached, interest_cached, visible_uncached, visible_cached + self.list.extend(interest_uncached); + self.list.extend(interest_cached); + self.list.extend(visible_uncached); + self.list.extend(visible_cached); } pub fn pop(&mut self) -> Option { From a8322215dde782be2b15d2bda296664e289c6525 Mon Sep 17 00:00:00 2001 From: Elena Torro Date: Wed, 4 Feb 2026 11:06:26 +0100 Subject: [PATCH 8/9] :wrench: Optimize pan/zoom tile handling - Add incremental tile update that preserves cache during pan - Only invalidate tile cache when zoom changes - Force visible tiles to render synchronously (no yielding) - Increase interest area threshold from 2 to 3 tiles --- render-wasm/src/render.rs | 88 +++++++++++++++++++++++++++++++++++---- 1 file changed, 79 insertions(+), 9 deletions(-) diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index ee2290cf04..9815cd1885 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -33,8 +33,9 @@ use crate::wapi; pub use fonts::*; pub use images::*; -// This is the extra are used for tile rendering. -const VIEWPORT_INTEREST_AREA_THRESHOLD: i32 = 2; +// This is the extra area used for tile rendering (tiles beyond viewport). +// Higher values pre-render more tiles, reducing empty squares during pan but using more memory. +const VIEWPORT_INTEREST_AREA_THRESHOLD: i32 = 3; const MAX_BLOCKING_TIME_MS: i32 = 32; const NODE_BATCH_THRESHOLD: i32 = 3; @@ -2063,8 +2064,13 @@ impl RenderState { } } else { performance::begin_measure!("render_shape_tree::uncached"); + // Only allow stopping (yielding) if the current tile is NOT visible. + // This ensures all visible tiles render synchronously before showing, + // eliminating empty squares during zoom. Interest-area tiles can still yield. + let tile_is_visible = self.tile_viewbox.is_visible(¤t_tile); + let can_stop = allow_stop && !tile_is_visible; let (is_empty, early_return) = - self.render_shape_tree_partial_uncached(tree, timestamp, allow_stop)?; + self.render_shape_tree_partial_uncached(tree, timestamp, can_stop)?; if early_return { return Ok(()); @@ -2215,6 +2221,60 @@ impl RenderState { result } + /* + * Incremental version of update_shape_tiles for pan/zoom operations. + * Updates the tile index and returns ONLY tiles that need cache invalidation. + * + * During pan operations, shapes don't move in world coordinates. The interest + * area (viewport) moves, which changes which tiles we track in the index, but + * tiles that were already cached don't need re-rendering just because the + * viewport moved. + * + * This function: + * 1. Updates the tile index (adds/removes shapes from tiles based on interest area) + * 2. Returns empty vec for cache invalidation (pan doesn't change tile content) + * + * Tile cache invalidation only happens when shapes actually move or change, + * which is handled by rebuild_touched_tiles, not during pan/zoom. + */ + pub fn update_shape_tiles_incremental( + &mut self, + shape: &Shape, + tree: ShapesPoolRef, + ) -> Vec { + let TileRect(rsx, rsy, rex, rey) = self.get_tiles_for_shape(shape, tree); + + let old_tiles: HashSet = self + .tiles + .get_tiles_of(shape.id) + .map_or(HashSet::new(), |tiles| tiles.iter().copied().collect()); + + let new_tiles: HashSet = (rsx..=rex) + .flat_map(|x| (rsy..=rey).map(move |y| tiles::Tile::from(x, y))) + .collect(); + + // Tiles where shape is being removed from index (left interest area) + let removed: Vec<_> = old_tiles.difference(&new_tiles).copied().collect(); + // Tiles where shape is being added to index (entered interest area) + let added: Vec<_> = new_tiles.difference(&old_tiles).copied().collect(); + + // Update the index: remove from old tiles + for tile in &removed { + self.tiles.remove_shape_at(*tile, shape.id); + } + + // Update the index: add to new tiles + for tile in &added { + self.tiles.add_shape_at(*tile, shape.id); + } + + // Don't invalidate cache for pan/zoom - the tile content hasn't changed, + // only the interest area moved. Tiles that were cached are still valid. + // New tiles that entered the interest area will be rendered fresh since + // they weren't in the cache anyway. + Vec::new() + } + /* * Add the tiles forthe shape to the index. * returns the tiles that have been updated @@ -2238,12 +2298,22 @@ impl RenderState { pub fn rebuild_tiles_shallow(&mut self, tree: ShapesPoolRef) { performance::begin_measure!("rebuild_tiles_shallow"); - let mut all_tiles = HashSet::::new(); + // Check if zoom changed - if so, we need full cache invalidation + // because tiles are rendered at specific zoom levels + let zoom_changed = self.zoom_changed(); + + let mut tiles_to_invalidate = HashSet::::new(); let mut nodes = vec![Uuid::nil()]; while let Some(shape_id) = nodes.pop() { if let Some(shape) = tree.get(&shape_id) { if shape_id != Uuid::nil() { - all_tiles.extend(self.update_shape_tiles(shape, tree)); + if zoom_changed { + // Zoom changed: use full update that tracks all affected tiles + tiles_to_invalidate.extend(self.update_shape_tiles(shape, tree)); + } else { + // Pan only: use incremental update that preserves valid cached tiles + self.update_shape_tiles_incremental(shape, tree); + } } else { // We only need to rebuild tiles from the first level. for child_id in shape.children_ids_iter(false) { @@ -2253,11 +2323,11 @@ impl RenderState { } } - // Invalidate changed tiles - old content stays visible until new tiles render - self.surfaces.remove_cached_tiles(self.background_color); - for tile in all_tiles { - self.remove_cached_tile(tile); + if zoom_changed { + // Zoom changed: clear all cached tiles since they're at wrong zoom level + self.surfaces.remove_cached_tiles(self.background_color); } + // Pan only: no cache invalidation needed - tiles content unchanged performance::end_measure!("rebuild_tiles_shallow"); } From 969666b39b47912191714343ed4d5039d770df96 Mon Sep 17 00:00:00 2001 From: Elena Torro Date: Wed, 4 Feb 2026 11:06:39 +0100 Subject: [PATCH 9/9] :wrench: Simplify view interaction log message Remove zoom_changed from log output as it's no longer needed for debugging after the tile optimization changes. --- render-wasm/src/main.rs | 6 +---- render-wasm/src/render.rs | 7 +++++- render-wasm/src/shapes.rs | 5 +++- render-wasm/src/shapes/modifiers.rs | 36 +++++++++++------------------ 4 files changed, 25 insertions(+), 29 deletions(-) diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index e8aa0640f7..a1de07e8fe 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -301,11 +301,7 @@ pub extern "C" fn set_view_end() { #[cfg(feature = "profile-macros")] { let total_time = performance::get_time() - unsafe { VIEW_INTERACTION_START }; - performance::console_log!( - "[PERF] view_interaction (zoom_changed={}): {}ms", - zoom_changed, - total_time - ); + performance::console_log!("[PERF] view_interaction: {}ms", total_time); } }); } diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 9815cd1885..4c9bd7f3ae 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -2195,7 +2195,11 @@ impl RenderState { * Given a shape, check the indexes and update it's location in the tile set * returns the tiles that have changed in the process. */ - pub fn update_shape_tiles(&mut self, shape: &Shape, tree: ShapesPoolRef) -> HashSet { + pub fn update_shape_tiles( + &mut self, + shape: &Shape, + tree: ShapesPoolRef, + ) -> HashSet { let TileRect(rsx, rsy, rex, rey) = self.get_tiles_for_shape(shape, tree); // Collect old tiles to avoid borrow conflict with remove_shape_at @@ -2447,6 +2451,7 @@ impl RenderState { self.touched_ids.insert(uuid); } + #[allow(dead_code)] pub fn clean_touched(&mut self) { self.touched_ids.clear(); } diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index 1edb377e35..358e356043 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -1120,7 +1120,10 @@ impl Shape { } /// Returns children in forward (non-reversed) order - useful for layout calculations - pub fn children_ids_iter_forward(&self, include_hidden: bool) -> Box + '_> { + pub fn children_ids_iter_forward( + &self, + include_hidden: bool, + ) -> Box + '_> { if include_hidden { return Box::new(self.children.iter()); } diff --git a/render-wasm/src/shapes/modifiers.rs b/render-wasm/src/shapes/modifiers.rs index 344935a73f..df2da87b3a 100644 --- a/render-wasm/src/shapes/modifiers.rs +++ b/render-wasm/src/shapes/modifiers.rs @@ -430,34 +430,26 @@ pub fn propagate_modifiers( } } } - // let mut layout_reflows_vec: Vec = layout_reflows.into_iter().collect(); + // We sort the reflows so they are processed deepest-first in the + // tree structure. This way we can be sure that the children layouts + // are already reflowed before their parents. + let mut layout_reflows_vec: Vec = + std::mem::take(&mut layout_reflows).into_iter().collect(); + layout_reflows_vec.sort_unstable_by(|id_a, id_b| { + let da = shapes.get_depth(id_a); + let db = shapes.get_depth(id_b); + db.cmp(&da) + }); - // // We sort the reflows so they are process first the ones that are more - // // deep in the tree structure. This way we can be sure that the children layouts - // // are already reflowed. - // layout_reflows_vec.sort_unstable_by(|id_a, id_b| { - // let da = shapes.get_depth(id_a); - // let db = shapes.get_depth(id_b); - // db.cmp(&da) - // }); - - // let mut bounds_temp = bounds.clone(); - // for id in layout_reflows_vec.iter() { - // if reflown.contains(id) { - // continue; - // } - // reflow_shape(id, state, &mut reflown, &mut entries, &mut bounds_temp); - // } - // layout_reflows = HashSet::new(); - - for id in std::mem::take(&mut layout_reflows) { - if reflown.contains(&id) { + for id in &layout_reflows_vec { + if reflown.contains(id) { continue; } - reflow_shape(&id, state, &mut reflown, &mut entries, &mut bounds); + reflow_shape(id, state, &mut reflown, &mut entries, &mut bounds); } } + #[allow(dead_code)] modifiers .iter() .map(|(key, val)| TransformEntry::from_input(*key, *val))