use crate::{
hash::{SubtreeHashBuilder, SubtreeHashes, ViewHashes, hash_view_facets},
node::{LayoutCache, LayoutConstraints, NodeId, TreeNode, TreeStats},
reconcile::ReconcileContext,
};
use repose_core::{Modifier, Rect, SubcomposeScope, View, ViewId, ViewKind};
use rustc_hash::{FxHashMap, FxHashSet};
use slotmap::Key;
use slotmap::SlotMap;
use smallvec::SmallVec;
use std::rc::Rc;
use std::sync::Arc;
pub struct ViewTree {
nodes: SlotMap<NodeId, TreeNode>,
root: Option<NodeId>,
dirty: FxHashSet<NodeId>,
dirty_propagated: FxHashSet<NodeId>,
paint_dirty: FxHashSet<NodeId>,
paint_dirty_propagated: FxHashSet<NodeId>,
generation: u64,
view_id_map: FxHashMap<ViewId, NodeId>,
duplicate_view_ids: FxHashSet<ViewId>,
pub stats: TreeStats,
pub removed_ids: Vec<NodeId>,
pending_removals: Vec<NodeId>,
removal_seen: FxHashSet<NodeId>,
subcompose_scope: SubcomposeScope,
subcompose_cache: FxHashMap<NodeId, (SubcomposeScope, Arc<Vec<View>>)>,
subcompose_runs: FxHashMap<NodeId, (u64, u8, SubcomposeScope)>,
scope_invalidation_seen: FxHashSet<NodeId>,
}
impl Default for ViewTree {
fn default() -> Self {
Self::new()
}
}
fn rc_identity_changed<T: ?Sized>(old: &Option<Rc<T>>, new: &Option<Rc<T>>) -> bool {
match (old, new) {
(Some(old), Some(new)) => !Rc::ptr_eq(old, new),
(None, None) => false,
_ => true,
}
}
fn arc_identity_changed<T: ?Sized>(old: &Option<Arc<T>>, new: &Option<Arc<T>>) -> bool {
match (old, new) {
(Some(old), Some(new)) => !Arc::ptr_eq(old, new),
(None, None) => false,
_ => true,
}
}
#[derive(Clone, Copy, Default)]
struct IdentityChange {
paint: bool,
layout: bool,
subcompose: bool,
}
impl IdentityChange {
fn merge(&mut self, other: Self) {
self.paint |= other.paint;
self.layout |= other.layout;
self.subcompose |= other.subcompose;
}
}
fn focus_requester_changed(
old: &Option<repose_core::FocusRequester>,
new: &Option<repose_core::FocusRequester>,
) -> bool {
match (old, new) {
(Some(old), Some(new)) => !Rc::ptr_eq(&old.target, &new.target),
(None, None) => false,
_ => true,
}
}
fn interaction_source_changed(
old: &Option<repose_core::InteractionSource>,
new: &Option<repose_core::InteractionSource>,
) -> bool {
match (old, new) {
(Some(old), Some(new)) => old.stable_id() != new.stable_id(),
(None, None) => false,
_ => true,
}
}
fn scroll_axis_identity_changed(
old: &repose_core::scroll::ScrollAxisBinding,
new: &repose_core::scroll::ScrollAxisBinding,
) -> bool {
rc_identity_changed(&old.on_scroll, &new.on_scroll)
|| rc_identity_changed(&old.set_viewport_main, &new.set_viewport_main)
|| rc_identity_changed(&old.set_content_main, &new.set_content_main)
|| rc_identity_changed(&old.get_offset_main, &new.get_offset_main)
|| rc_identity_changed(&old.set_offset_main, &new.set_offset_main)
|| rc_identity_changed(&old.tick, &new.tick)
|| rc_identity_changed(&old.set_nested_scroll_parent, &new.set_nested_scroll_parent)
}
fn scroll_identity_changed(
old: &Option<repose_core::ScrollBinding>,
new: &Option<repose_core::ScrollBinding>,
) -> bool {
match (old, new) {
(
Some(repose_core::ScrollBinding::Vertical(old)),
Some(repose_core::ScrollBinding::Vertical(new)),
) => scroll_axis_identity_changed(old, new),
(
Some(repose_core::ScrollBinding::Horizontal(old)),
Some(repose_core::ScrollBinding::Horizontal(new)),
) => scroll_axis_identity_changed(old, new),
(
Some(repose_core::ScrollBinding::Both(old)),
Some(repose_core::ScrollBinding::Both(new)),
) => {
rc_identity_changed(&old.on_scroll, &new.on_scroll)
|| rc_identity_changed(&old.set_viewport_width, &new.set_viewport_width)
|| rc_identity_changed(&old.set_viewport_height, &new.set_viewport_height)
|| rc_identity_changed(&old.set_content_width, &new.set_content_width)
|| rc_identity_changed(&old.set_content_height, &new.set_content_height)
|| rc_identity_changed(&old.get_offset_xy, &new.get_offset_xy)
|| rc_identity_changed(&old.set_offset_xy, &new.set_offset_xy)
|| rc_identity_changed(&old.tick, &new.tick)
|| rc_identity_changed(&old.set_nested_scroll_parent, &new.set_nested_scroll_parent)
}
(None, None) => false,
_ => true,
}
}
fn cursor_identity_changed(
old: &Option<repose_core::CursorIcon>,
new: &Option<repose_core::CursorIcon>,
) -> bool {
match (old, new) {
(
Some(repose_core::CursorIcon::Custom(old)),
Some(repose_core::CursorIcon::Custom(new)),
) => !std::sync::Arc::ptr_eq(old, new) || old != new,
(None, None) => false,
_ => true,
}
}
fn text_input_identity_change(
old: Option<&repose_core::TextInputConfig>,
new: Option<&repose_core::TextInputConfig>,
) -> IdentityChange {
let (Some(old), Some(new)) = (old, new) else {
return IdentityChange {
paint: old.is_some() != new.is_some(),
layout: old.is_some() != new.is_some(),
subcompose: false,
};
};
let mut change = IdentityChange::default();
if rc_identity_changed(&old.on_change, &new.on_change)
|| rc_identity_changed(&old.on_submit, &new.on_submit)
|| rc_identity_changed(&old.on_text_layout, &new.on_text_layout)
|| rc_identity_changed(&old.focus_tracker, &new.focus_tracker)
|| interaction_source_changed(&old.interaction_source, &new.interaction_source)
{
change.paint = true;
}
if rc_identity_changed(&old.visual_transformation, &new.visual_transformation) {
change.paint = true;
change.layout = true;
}
match (&old.keyboard_actions, &new.keyboard_actions) {
(Some(old), Some(new)) => {
if rc_identity_changed(&old.on_done, &new.on_done)
|| rc_identity_changed(&old.on_go, &new.on_go)
|| rc_identity_changed(&old.on_next, &new.on_next)
|| rc_identity_changed(&old.on_previous, &new.on_previous)
|| rc_identity_changed(&old.on_search, &new.on_search)
|| rc_identity_changed(&old.on_send, &new.on_send)
{
change.paint = true;
}
}
(None, None) => {}
_ => change.paint = true,
}
change
}
fn modifier_identity_change(old: &Modifier, new: &Modifier) -> IdentityChange {
let mut change = IdentityChange::default();
macro_rules! paint_changed {
($($field:ident),+ $(,)?) => {
if false $(|| rc_identity_changed(&old.$field, &new.$field))+ {
change.paint = true;
}
};
}
macro_rules! layout_changed {
($($field:ident),+ $(,)?) => {
if false $(|| rc_identity_changed(&old.$field, &new.$field))+ {
change.layout = true;
}
};
}
paint_changed!(
on_scroll,
on_pointer_down,
on_pointer_move,
on_pointer_up,
on_pointer_cancel,
on_pointer_enter,
on_pointer_leave,
on_click,
on_double_click,
on_long_click,
on_globally_positioned,
on_size_changed,
on_key_event,
on_preview_key_event,
on_drag_start,
on_drag_end,
on_drag_enter,
on_drag_over,
on_drag_leave,
on_drop,
on_action,
painter,
indication,
drag_preview,
on_focus_changed,
);
layout_changed!(layout);
if arc_identity_changed(&old.paint_callback, &new.paint_callback) {
change.paint = true;
}
if focus_requester_changed(&old.focus_requester, &new.focus_requester)
|| interaction_source_changed(&old.interaction_source, &new.interaction_source)
{
change.paint = true;
}
change.merge(text_input_identity_change(
old.text_input.as_ref(),
new.text_input.as_ref(),
));
match (&old.nested_scroll_connection, &new.nested_scroll_connection) {
(Some(old), Some(new)) => {
if rc_identity_changed(&old.on_pre_scroll, &new.on_pre_scroll)
|| rc_identity_changed(&old.on_post_scroll, &new.on_post_scroll)
|| rc_identity_changed(&old.on_pre_fling, &new.on_pre_fling)
|| rc_identity_changed(&old.on_post_fling, &new.on_post_fling)
{
change.paint = true;
}
}
(None, None) => {}
_ => change.paint = true,
}
if scroll_identity_changed(&old.scroll, &new.scroll)
|| cursor_identity_changed(&old.cursor, &new.cursor)
|| old.scrollbar_style != new.scrollbar_style
{
change.paint = true;
}
change
}
fn view_identity_change(node: &TreeNode, view: &View) -> IdentityChange {
let mut change = modifier_identity_change(&node.modifier, &view.modifier);
if let (
ViewKind::SubcomposeLayout { content: old },
ViewKind::SubcomposeLayout { content: new },
) = (&node.kind, &view.kind)
&& !Arc::ptr_eq(old, new)
{
change.paint = true;
change.layout = true;
change.subcompose = true;
}
change
}
fn semantics_equal(
old: Option<&repose_core::Semantics>,
new: Option<&repose_core::Semantics>,
) -> bool {
match (old, new) {
(Some(old), Some(new)) => {
old.role == new.role
&& old.label == new.label
&& old.focused == new.focused
&& old.enabled == new.enabled
&& old.selectable_group == new.selectable_group
&& old.checked == new.checked
&& old.selected == new.selected
&& old.value == new.value
}
(None, None) => true,
_ => false,
}
}
fn view_kind_equal(old: &ViewKind, new: &ViewKind) -> bool {
match (old, new) {
(
ViewKind::SubcomposeLayout { content: old },
ViewKind::SubcomposeLayout { content: new },
) => Arc::ptr_eq(old, new),
(
ViewKind::Text {
text: old_text,
color: old_color,
font_size: old_font_size,
soft_wrap: old_soft_wrap,
max_lines: old_max_lines,
overflow: old_overflow,
font_family: old_font_family,
annotations: old_annotations,
text_align: old_text_align,
font_weight: old_font_weight,
font_style: old_font_style,
text_decoration: old_text_decoration,
letter_spacing: old_letter_spacing,
line_height: old_line_height,
url: old_url,
font_variation_settings: old_font_variation_settings,
draw_style: old_draw_style,
},
ViewKind::Text {
text: new_text,
color: new_color,
font_size: new_font_size,
soft_wrap: new_soft_wrap,
max_lines: new_max_lines,
overflow: new_overflow,
font_family: new_font_family,
annotations: new_annotations,
text_align: new_text_align,
font_weight: new_font_weight,
font_style: new_font_style,
text_decoration: new_text_decoration,
letter_spacing: new_letter_spacing,
line_height: new_line_height,
url: new_url,
font_variation_settings: new_font_variation_settings,
draw_style: new_draw_style,
},
) => {
old_text == new_text
&& old_color == new_color
&& old_font_size == new_font_size
&& old_soft_wrap == new_soft_wrap
&& old_max_lines == new_max_lines
&& old_overflow == new_overflow
&& old_font_family == new_font_family
&& old_annotations == new_annotations
&& old_text_align == new_text_align
&& old_font_weight == new_font_weight
&& old_font_style == new_font_style
&& old_text_decoration == new_text_decoration
&& old_letter_spacing == new_letter_spacing
&& old_line_height == new_line_height
&& old_url == new_url
&& old_font_variation_settings == new_font_variation_settings
&& old_draw_style == new_draw_style
}
(
ViewKind::Image {
handle: old_handle,
tint: old_tint,
fit: old_fit,
filter: old_filter,
source_rect: old_source_rect,
alignment: old_alignment,
},
ViewKind::Image {
handle: new_handle,
tint: new_tint,
fit: new_fit,
filter: new_filter,
source_rect: new_source_rect,
alignment: new_alignment,
},
) => {
old_handle == new_handle
&& old_tint == new_tint
&& old_fit == new_fit
&& old_filter == new_filter
&& old_source_rect == new_source_rect
&& old_alignment == new_alignment
}
_ => std::mem::discriminant(old) == std::mem::discriminant(new),
}
}
fn next_dedup_key(
original: u64,
index: usize,
parent_id: NodeId,
new_seen_keys: &mut FxHashSet<u64>,
keyed_children: &FxHashMap<u64, NodeId>,
) -> u64 {
let mut candidate = (original.wrapping_mul(0x9E3779B97F4A7C15)
^ (index as u64).wrapping_add(0xBF58476D1CE4E5B9))
.wrapping_add(parent_id.data().as_ffi());
loop {
if candidate != original
&& !new_seen_keys.contains(&candidate)
&& !keyed_children.contains_key(&candidate)
{
new_seen_keys.insert(candidate);
return candidate;
}
candidate = candidate.wrapping_add(1);
if candidate == 0 {
candidate = 1;
}
}
}
impl ViewTree {
pub fn new() -> Self {
Self {
nodes: SlotMap::with_key(),
root: None,
dirty: FxHashSet::default(),
dirty_propagated: FxHashSet::default(),
paint_dirty: FxHashSet::default(),
paint_dirty_propagated: FxHashSet::default(),
generation: 0,
view_id_map: FxHashMap::default(),
duplicate_view_ids: FxHashSet::default(),
stats: TreeStats::default(),
removed_ids: Vec::new(),
pending_removals: Vec::new(),
removal_seen: FxHashSet::default(),
subcompose_scope: SubcomposeScope::UNBOUNDED,
subcompose_cache: FxHashMap::default(),
subcompose_runs: FxHashMap::default(),
scope_invalidation_seen: FxHashSet::default(),
}
}
pub fn set_subcompose_scope(&mut self, scope: SubcomposeScope) {
self.subcompose_scope = SubcomposeScope::new(
scope.min_width,
scope.max_width,
scope.min_height,
scope.max_height,
);
}
pub fn subcompose_scope(&self) -> SubcomposeScope {
self.subcompose_scope
}
fn run_subcompose(
&mut self,
node_id: NodeId,
content: &Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>>,
) -> Arc<Vec<View>> {
let raw_scope = self.compute_scope_for_node(node_id);
let scope = SubcomposeScope::new(
raw_scope.min_width,
raw_scope.max_width,
raw_scope.min_height,
raw_scope.max_height,
);
let cached = self
.subcompose_cache
.get(&node_id)
.map(|(cached_scope, cached_slots)| (*cached_scope, Arc::clone(cached_slots)));
if let Some((cached_scope, cached_slots)) = cached {
let previous_scope = self
.subcompose_runs
.get(&node_id)
.map(|(_, _, previous_scope)| *previous_scope);
let scope_changed =
previous_scope.is_some_and(|previous_scope| previous_scope != scope);
if cached_scope == scope && !scope_changed {
self.subcompose_runs.remove(&node_id);
return cached_slots;
}
let cur_gen = self.generation;
let streak = match self.subcompose_runs.get(&node_id) {
Some((last_gen, count, _)) if *last_gen == cur_gen => count.saturating_add(1),
Some((last_gen, count, _)) if last_gen.wrapping_add(1) == cur_gen => {
count.saturating_add(1)
}
_ => 1,
};
self.subcompose_runs
.insert(node_id, (cur_gen, streak, scope));
if streak >= 4 {
if streak == 4 {
log::warn!(
"SubcomposeLayout {:?} oscillating; holding cached result",
node_id
);
}
return cached_slots;
}
if cached_scope == scope {
return cached_slots;
}
} else {
self.subcompose_runs.remove(&node_id);
}
let slots = content(&scope);
let scope_key = format!("subcompose_{:?}", node_id);
let mut views = Vec::with_capacity(slots.len());
for (slot_id, mut view) in slots {
view.modifier.key = Some(slot_id);
view.scope_key = Some(scope_key.clone());
view.modifier.repaint_boundary = true;
views.push(view);
}
let views = Arc::new(views);
let old_cache = self
.subcompose_cache
.insert(node_id, (scope, Arc::clone(&views)));
drop(old_cache);
views
}
fn compute_scope_for_node(&self, node_id: NodeId) -> SubcomposeScope {
let mut scope = self.subcompose_scope;
let mut chain: SmallVec<[NodeId; 16]> = SmallVec::new();
let mut current = Some(node_id);
while let Some(id) = current {
chain.push(id);
match self.nodes.get(id) {
Some(node) => current = node.parent,
None => break,
}
}
chain.reverse();
for ancestor_id in chain {
if let Some(node) = self.nodes.get(ancestor_id) {
scope = intersect_scope_with_modifier(scope, &node.modifier);
if let Some(cache) = &node.layout_cache {
let w = cache.rect.w;
if w >= 0.0 && w.is_finite() {
scope.max_width = scope.max_width.min(repose_core::Dp(w));
}
let h = cache.rect.h;
if h >= 0.0 && h.is_finite() {
scope.max_height = scope.max_height.min(repose_core::Dp(h));
}
}
}
}
scope
}
pub fn invalidate_subcompose_cache(&mut self, node_id: NodeId) {
let removed = self.subcompose_cache.remove(&node_id);
drop(removed);
self.subcompose_runs.remove(&node_id);
}
pub fn generation(&self) -> u64 {
self.generation
}
pub fn root(&self) -> Option<NodeId> {
self.root
}
pub fn get(&self, id: NodeId) -> Option<&TreeNode> {
self.nodes.get(id)
}
pub fn get_mut(&mut self, id: NodeId) -> Option<&mut TreeNode> {
self.nodes.get_mut(id)
}
pub fn get_by_view_id(&self, view_id: ViewId) -> Option<&TreeNode> {
self.view_id_map
.get(&view_id)
.and_then(|id| self.nodes.get(*id))
}
pub fn len(&self) -> usize {
self.nodes.len()
}
pub fn is_empty(&self) -> bool {
self.nodes.is_empty()
}
pub fn is_dirty(&self, id: NodeId) -> bool {
self.dirty.contains(&id)
}
pub fn dirty_nodes(&self) -> &FxHashSet<NodeId> {
&self.dirty
}
pub fn clear_dirty(&mut self) {
self.dirty.clear();
self.dirty_propagated.clear();
}
pub fn paint_dirty_nodes(&self) -> &FxHashSet<NodeId> {
&self.paint_dirty
}
pub fn clear_paint_dirty(&mut self) {
self.paint_dirty.clear();
self.paint_dirty_propagated.clear();
}
fn mark_paint_dirty(&mut self, node_id: NodeId) {
let mut current = Some(node_id);
while let Some(id) = current {
if !self.paint_dirty_propagated.insert(id) {
break;
}
self.paint_dirty.insert(id);
current = self.nodes.get(id).and_then(|node| node.parent);
}
}
fn mark_scope_descendants_dirty(&mut self, id: NodeId) {
if !self.scope_invalidation_seen.insert(id) {
return;
}
let mut stack: SmallVec<[NodeId; 16]> = SmallVec::new();
stack.push(id);
while let Some(current) = stack.pop() {
let Some(node) = self.nodes.get(current) else {
continue;
};
for &child in &node.children {
if self
.nodes
.get(child)
.is_some_and(|child| child.scope_key.is_some())
{
self.dirty.insert(child);
}
stack.push(child);
}
}
}
pub fn mark_dirty(&mut self, id: NodeId) {
let mut current = Some(id);
while let Some(current_id) = current {
if !self.dirty_propagated.insert(current_id) {
break;
}
self.dirty.insert(current_id);
current = self.nodes.get(current_id).and_then(|node| node.parent);
}
let needs_scope_invalidate = self.nodes.get(id).is_some_and(|node| {
node.modifier.transform.is_some()
|| node.modifier.alpha.is_some()
|| node.modifier.graphics_layer.is_some()
});
if needs_scope_invalidate {
self.mark_scope_descendants_dirty(id);
}
}
pub fn update(&mut self, new_root: &View) -> NodeId {
self.removed_ids.clear();
self.pending_removals.clear();
self.removal_seen.clear();
self.paint_dirty.clear();
self.paint_dirty_propagated.clear();
self.dirty_propagated.clear();
self.scope_invalidation_seen.clear();
self.generation += 1;
self.stats = TreeStats::default();
let mut ctx = ReconcileContext::new(self.generation);
let root_id = if let Some(existing_root) = self.root {
self.reconcile_node(existing_root, new_root, None, 0, 0, &mut ctx)
} else {
self.create_node(new_root, None, 0, 0, &mut ctx)
};
self.root = Some(root_id);
self.collect_explicit_removals(&mut ctx);
self.stats.total_nodes = self.nodes.len();
self.stats.dirty_nodes = self.dirty.len();
self.stats.reconciled_nodes = ctx.reconciled;
self.stats.skipped_nodes = ctx.skipped;
self.stats.created_nodes = ctx.created;
self.stats.removed_nodes = ctx.removed;
root_id
}
fn reconcile_node(
&mut self,
node_id: NodeId,
view: &View,
parent: Option<NodeId>,
depth: u32,
index_in_parent: u32,
ctx: &mut ReconcileContext,
) -> NodeId {
let hashes = hash_view_facets(view);
let Some(node) = self.nodes.get(node_id) else {
log::error!(
"reconcile_node: node {:?} not found - creating fresh",
node_id
);
return self.create_node(view, parent, depth, index_in_parent, ctx);
};
let old_hashes = ViewHashes {
layout: node.layout_hash,
measurement: node.measurement_hash,
paint: node.paint_hash,
semantics: node.semantics_hash,
};
let identity = view_identity_change(node, view);
let old_subtree = SubtreeHashes {
layout: node.layout_subtree_hash,
measurement: node.measurement_subtree_hash,
paint: node.paint_subtree_hash,
semantics: node.semantics_subtree_hash,
};
let old_subtree_hash = node.subtree_hash;
let old_scope_key = node.scope_key.clone();
let old_view_id = node.view_id;
let old_is_subcompose = matches!(&node.kind, ViewKind::SubcomposeLayout { .. });
let new_is_subcompose = matches!(&view.kind, ViewKind::SubcomposeLayout { .. });
let hashes_equal = old_hashes == hashes;
let content_hash = hashes.combined();
let content_changed = !hashes_equal;
let view_semantics_changed =
!hashes_equal && !semantics_equal(node.semantics.as_ref(), view.semantics.as_ref());
let modifier_semantics_changed = !hashes_equal
&& !semantics_equal(
node.modifier.semantics.as_ref(),
view.modifier.semantics.as_ref(),
);
let scope_effect_changed = node.modifier.transform != view.modifier.transform
|| node.modifier.alpha != view.modifier.alpha
|| node.modifier.graphics_layer != view.modifier.graphics_layer;
let kind_changed = !view_kind_equal(&node.kind, &view.kind);
let modifier_changed = old_hashes.layout != hashes.layout
|| old_hashes.measurement != hashes.measurement
|| old_hashes.paint != hashes.paint
|| modifier_semantics_changed
|| identity.paint
|| identity.layout;
let scope_changed = old_scope_key.as_deref() != view.scope_key.as_deref();
let node_layout_changed = old_hashes.layout != hashes.layout
|| old_hashes.measurement != hashes.measurement
|| identity.layout
|| identity.subcompose
|| old_is_subcompose != new_is_subcompose;
let subcompose_changed = identity.subcompose
|| (new_is_subcompose
&& (old_hashes.layout != hashes.layout
|| old_hashes.measurement != hashes.measurement
|| identity.layout))
|| old_is_subcompose != new_is_subcompose;
if subcompose_changed {
self.invalidate_subcompose_cache(node_id);
}
let view_id = self.compute_view_id(view, node_id, parent, index_in_parent);
self.assign_view_id(
node_id,
old_view_id,
view_id,
view.id == 0,
"reconcile_node",
);
{
let Some(node) = self.nodes.get_mut(node_id) else {
log::error!("reconcile_node: node {:?} vanished mid-reconcile", node_id);
return node_id;
};
node.parent = parent;
node.depth = depth;
node.generation = self.generation;
if kind_changed {
node.kind = view.kind.clone();
}
if modifier_changed {
node.modifier = view.modifier.clone();
}
if view_semantics_changed {
node.semantics = view.semantics.clone();
}
if scope_changed {
node.scope_key = view.scope_key.clone();
}
node.user_key = view.modifier.key;
if node_layout_changed {
node.layout_cache = None;
}
}
let new_children_hashes = if let ViewKind::SubcomposeLayout { content } = &view.kind {
let subcomposed = self.run_subcompose(node_id, content);
self.reconcile_children(node_id, subcomposed.as_slice(), depth, ctx)
} else {
self.reconcile_children(node_id, &view.children, depth, ctx)
};
let new_subtree = new_children_hashes.finish(hashes);
let new_subtree_hash = new_subtree.combined();
let layout_subtree_changed = old_subtree.layout != new_subtree.layout
|| old_subtree.measurement != new_subtree.measurement
|| identity.layout
|| identity.subcompose;
let paint_subtree_changed =
old_subtree.paint != new_subtree.paint || identity.paint || identity.subcompose;
let semantics_subtree_changed = old_subtree.semantics != new_subtree.semantics;
let subtree_changed = old_subtree_hash != new_subtree_hash || identity.subcompose;
{
let Some(node) = self.nodes.get_mut(node_id) else {
log::error!("reconcile_node: node {:?} vanished mid-reconcile", node_id);
return node_id;
};
if layout_subtree_changed {
node.layout_cache = None;
}
node.content_hash = content_hash;
node.layout_hash = hashes.layout;
node.measurement_hash = hashes.measurement;
node.paint_hash = hashes.paint;
node.semantics_hash = hashes.semantics;
node.subtree_hash = new_subtree_hash;
node.layout_subtree_hash = new_subtree.layout;
node.measurement_subtree_hash = new_subtree.measurement;
node.paint_subtree_hash = new_subtree.paint;
node.semantics_subtree_hash = new_subtree.semantics;
if content_changed || identity.paint || identity.layout || identity.subcompose {
ctx.reconciled += 1;
} else if !subtree_changed {
ctx.skipped += 1;
}
}
if paint_subtree_changed || semantics_subtree_changed {
self.mark_paint_dirty(node_id);
}
if layout_subtree_changed {
self.mark_dirty(node_id);
}
if scope_effect_changed {
self.mark_scope_descendants_dirty(node_id);
}
node_id
}
fn node_subtree_hashes(&self, node_id: NodeId) -> SubtreeHashes {
self.nodes
.get(node_id)
.map_or_default(|node| SubtreeHashes {
layout: node.layout_subtree_hash,
measurement: node.measurement_subtree_hash,
paint: node.paint_subtree_hash,
semantics: node.semantics_subtree_hash,
})
}
fn reconcile_children(
&mut self,
parent_id: NodeId,
new_children: &[View],
parent_depth: u32,
ctx: &mut ReconcileContext,
) -> SubtreeHashBuilder {
let child_depth = parent_depth + 1;
let old_children: SmallVec<[NodeId; 4]> = self
.nodes
.get_mut(parent_id)
.map(|node| std::mem::take(&mut node.children))
.unwrap_or_default();
let mut keyed_children: FxHashMap<u64, NodeId> = FxHashMap::default();
let mut unkeyed_children: SmallVec<[NodeId; 4]> = SmallVec::new();
for &child_id in &old_children {
if let Some(node) = self.nodes.get(child_id) {
match node.user_key {
Some(key) => {
keyed_children.entry(key).or_insert(child_id);
}
None => unkeyed_children.push(child_id),
}
}
}
let mut new_child_ids: SmallVec<[NodeId; 4]> = SmallVec::new();
let mut child_hashes = SubtreeHashBuilder::new(new_children.len());
let mut unkeyed_index = 0;
let mut used_nodes: FxHashSet<NodeId> = FxHashSet::default();
let mut new_seen_keys: FxHashSet<u64> = FxHashSet::default();
for (i, new_child) in new_children.iter().enumerate() {
let mut owned_child: Option<View>;
let child_ref = if let Some(key) = new_child.modifier.key {
if new_seen_keys.insert(key) {
new_child
} else {
log::error!(
"reconcile_children: duplicate modifier.key={} in children of node {:?} - deduplicating (suffixing). Ensure get_key returns unique keys.",
key,
parent_id
);
let salt =
next_dedup_key(key, i, parent_id, &mut new_seen_keys, &keyed_children);
owned_child = Some(new_child.clone());
let child = owned_child.as_mut().expect("owned child inserted");
child.modifier.key = Some(salt);
child
}
} else {
new_child
};
let is_subcompose = matches!(&child_ref.kind, ViewKind::SubcomposeLayout { .. });
let child_id = if let Some(key) = child_ref.modifier.key {
if let Some(&existing_id) = keyed_children.get(&key) {
if used_nodes.insert(existing_id) {
self.reconcile_node(
existing_id,
child_ref,
Some(parent_id),
child_depth,
i as u32,
ctx,
)
} else {
let salt =
next_dedup_key(key, i, parent_id, &mut new_seen_keys, &keyed_children);
let mut fresh = child_ref.clone();
fresh.modifier.key = Some(salt);
self.create_node(&fresh, Some(parent_id), child_depth, i as u32, ctx)
}
} else if is_subcompose && unkeyed_index < unkeyed_children.len() {
let existing_id = unkeyed_children[unkeyed_index];
unkeyed_index += 1;
if used_nodes.insert(existing_id) {
self.reconcile_node(
existing_id,
child_ref,
Some(parent_id),
child_depth,
i as u32,
ctx,
)
} else {
self.create_node(child_ref, Some(parent_id), child_depth, i as u32, ctx)
}
} else {
self.create_node(child_ref, Some(parent_id), child_depth, i as u32, ctx)
}
} else if let Some(&existing_id) = unkeyed_children.get(unkeyed_index) {
unkeyed_index += 1;
if used_nodes.insert(existing_id) {
self.reconcile_node(
existing_id,
child_ref,
Some(parent_id),
child_depth,
i as u32,
ctx,
)
} else {
self.create_node(child_ref, Some(parent_id), child_depth, i as u32, ctx)
}
} else {
self.create_node(child_ref, Some(parent_id), child_depth, i as u32, ctx)
};
new_child_ids.push(child_id);
child_hashes.push(self.node_subtree_hashes(child_id));
}
for &old_child in &old_children {
if !used_nodes.contains(&old_child) {
self.mark_for_removal(old_child);
}
}
if let Some(parent) = self.nodes.get_mut(parent_id) {
parent.children = new_child_ids;
}
child_hashes
}
fn create_node(
&mut self,
view: &View,
parent: Option<NodeId>,
depth: u32,
index_in_parent: u32,
ctx: &mut ReconcileContext,
) -> NodeId {
let hashes = hash_view_facets(view);
let content_hash = hashes.combined();
let node_id = self.nodes.insert_with_key(|id| {
TreeNode::new(
id,
0,
view.kind.clone(),
view.modifier.clone(),
self.generation,
)
});
ctx.created += 1;
{
let node = self
.nodes
.get_mut(node_id)
.expect("create_node: node just inserted");
node.parent = parent;
node.depth = depth;
node.semantics = view.semantics.clone();
node.content_hash = content_hash;
node.layout_hash = hashes.layout;
node.measurement_hash = hashes.measurement;
node.paint_hash = hashes.paint;
node.semantics_hash = hashes.semantics;
node.user_key = view.modifier.key;
node.scope_key = view.scope_key.clone();
}
let view_id = self.compute_view_id(view, node_id, parent, index_in_parent);
self.assign_view_id(node_id, 0, view_id, view.id == 0, "create_node");
let child_depth = depth + 1;
let mut child_ids: SmallVec<[NodeId; 4]> = SmallVec::new();
let subcomposed = if let ViewKind::SubcomposeLayout { content } = &view.kind {
Some(self.run_subcompose(node_id, content))
} else {
None
};
let children_to_create = subcomposed
.as_ref()
.map_or(view.children.as_slice(), |views| views.as_slice());
let mut child_hashes = SubtreeHashBuilder::new(children_to_create.len());
for (i, child_view) in children_to_create.iter().enumerate() {
let child_id = self.create_node(child_view, Some(node_id), child_depth, i as u32, ctx);
child_ids.push(child_id);
child_hashes.push(self.node_subtree_hashes(child_id));
}
let subtree = child_hashes.finish(hashes);
let subtree_hash = subtree.combined();
let node = self
.nodes
.get_mut(node_id)
.expect("create_node: node just inserted");
node.children = child_ids;
node.subtree_hash = subtree_hash;
node.layout_subtree_hash = subtree.layout;
node.measurement_subtree_hash = subtree.measurement;
node.paint_subtree_hash = subtree.paint;
node.semantics_subtree_hash = subtree.semantics;
self.dirty.insert(node_id);
node_id
}
fn compute_view_id(
&self,
view: &View,
_node_id: NodeId,
parent: Option<NodeId>,
index_in_parent: u32,
) -> ViewId {
if view.id != 0 {
return view.id;
}
let parent_id = parent
.and_then(|p| self.nodes.get(p))
.map(|n| n.view_id)
.unwrap_or(0);
let salt = match view.modifier.key {
Some(key) => key.wrapping_mul(2).wrapping_add(1),
None => (index_in_parent as u64).wrapping_mul(2),
};
let mut id = parent_id.wrapping_mul(31).wrapping_add(salt);
id = id.wrapping_mul(0x9E3779B97F4A7C15);
id ^= id >> 30;
if id == 0 {
id = 1;
}
id
}
fn repair_view_id(&mut self, view_id: ViewId) {
let previous = self.view_id_map.get(&view_id).copied();
let mut owner = None;
let mut count = 0usize;
for (node_id, node) in self.nodes.iter() {
if node.generation != 0 && node.view_id == view_id {
count += 1;
if owner.is_none() {
owner = Some(node_id);
}
}
}
let owner = previous
.filter(|node_id| {
self.nodes
.get(*node_id)
.is_some_and(|node| node.generation != 0 && node.view_id == view_id)
})
.or(owner);
if let Some(owner) = owner {
self.view_id_map.insert(view_id, owner);
} else {
self.view_id_map.remove(&view_id);
}
if count > 1 {
self.duplicate_view_ids.insert(view_id);
} else {
self.duplicate_view_ids.remove(&view_id);
}
}
fn rebuild_view_id_map(&mut self) {
let previous = std::mem::take(&mut self.view_id_map);
self.duplicate_view_ids.clear();
let mut counts: FxHashMap<ViewId, (NodeId, usize)> = FxHashMap::default();
for (node_id, node) in self.nodes.iter() {
if node.generation == 0 {
continue;
}
let entry = counts.entry(node.view_id).or_insert((node_id, 0));
entry.1 += 1;
}
for (view_id, (first_node, count)) in counts {
let owner = previous
.get(&view_id)
.copied()
.filter(|node_id| {
self.nodes
.get(*node_id)
.is_some_and(|node| node.generation != 0 && node.view_id == view_id)
})
.unwrap_or(first_node);
self.view_id_map.insert(view_id, owner);
if count > 1 {
self.duplicate_view_ids.insert(view_id);
}
}
}
fn assign_view_id(
&mut self,
node_id: NodeId,
old_view_id: ViewId,
requested_view_id: ViewId,
generated: bool,
phase: &str,
) {
let view_id = if generated {
self.available_generated_view_id(node_id, requested_view_id)
} else {
requested_view_id
};
let old_owner =
old_view_id != 0 && self.view_id_map.get(&old_view_id).copied() == Some(node_id);
if old_owner {
self.view_id_map.remove(&old_view_id);
}
if let Some(existing) = self.view_id_map.get(&view_id).copied()
&& existing != node_id
{
self.duplicate_view_ids.insert(view_id);
log::error!(
"{}: duplicate View.id {}; keeping the first node {:?}",
phase,
view_id,
existing
);
} else {
self.view_id_map.insert(view_id, node_id);
}
if let Some(node) = self.nodes.get_mut(node_id) {
node.view_id = view_id;
}
if old_owner {
self.repair_view_id(old_view_id);
}
}
fn available_generated_view_id(&self, node_id: NodeId, requested: ViewId) -> ViewId {
if self
.view_id_map
.get(&requested)
.map_or(true, |existing| *existing == node_id)
{
return requested;
}
let mut candidate = requested;
loop {
candidate = candidate.wrapping_add(1);
if candidate == 0 {
candidate = 1;
}
if self
.view_id_map
.get(&candidate)
.map_or(true, |existing| *existing == node_id)
{
return candidate;
}
}
}
fn mark_for_removal(&mut self, node_id: NodeId) {
if !self.removal_seen.insert(node_id) {
return;
}
let Some((view_id, children)) = self.nodes.get_mut(node_id).map(|node| {
node.generation = 0;
let children = std::mem::take(&mut node.children);
(node.view_id, children)
}) else {
return;
};
self.pending_removals.push(node_id);
if self.view_id_map.get(&view_id).copied() == Some(node_id) {
self.view_id_map.remove(&view_id);
self.repair_view_id(view_id);
}
self.dirty.remove(&node_id);
self.dirty_propagated.remove(&node_id);
self.paint_dirty.remove(&node_id);
self.paint_dirty_propagated.remove(&node_id);
self.scope_invalidation_seen.remove(&node_id);
let removed_cache = self.subcompose_cache.remove(&node_id);
drop(removed_cache);
self.subcompose_runs.remove(&node_id);
for child_id in children {
self.mark_for_removal(child_id);
}
}
fn collect_explicit_removals(&mut self, ctx: &mut ReconcileContext) {
let pending = std::mem::take(&mut self.pending_removals);
for node_id in pending {
if self.nodes.remove(node_id).is_none() {
continue;
}
ctx.removed += 1;
self.removed_ids.push(node_id);
}
self.removal_seen.clear();
self.rebuild_view_id_map();
}
pub fn set_layout(
&mut self,
id: NodeId,
rect: Rect,
screen_rect: Rect,
constraints: LayoutConstraints,
) {
if let Some(node) = self.nodes.get_mut(id) {
node.layout_cache = Some(LayoutCache {
rect,
screen_rect,
constraints,
generation: self.generation,
});
}
}
pub fn iter(&self) -> impl Iterator<Item = &TreeNode> {
self.nodes.values()
}
pub fn iter_with_ids(&self) -> impl Iterator<Item = (NodeId, &TreeNode)> {
self.nodes.iter()
}
pub fn walk<F>(&self, mut f: F)
where
F: FnMut(&TreeNode, u32) -> bool,
{
if let Some(root_id) = self.root {
self.walk_node(root_id, 0, &mut f);
}
}
fn walk_node<F>(&self, id: NodeId, depth: u32, f: &mut F)
where
F: FnMut(&TreeNode, u32) -> bool,
{
if let Some(node) = self.nodes.get(id) {
if !f(node, depth) {
return;
}
for &child_id in &node.children {
self.walk_node(child_id, depth + 1, f);
}
}
}
pub fn children(&self, id: NodeId) -> Option<&[NodeId]> {
self.nodes.get(id).map(|n| n.children.as_slice())
}
}
fn intersect_scope_with_modifier(scope: SubcomposeScope, modifier: &Modifier) -> SubcomposeScope {
let mut s = scope;
if let Some(sz) = modifier.size {
s.min_width = s.min_width.max(sz.width);
s.max_width = s.max_width.min(sz.width);
s.min_height = s.min_height.max(sz.height);
s.max_height = s.max_height.min(sz.height);
}
if let Some(w) = modifier.width {
s.min_width = s.min_width.max(w);
s.max_width = s.max_width.min(w);
}
if let Some(h) = modifier.height {
s.min_height = s.min_height.max(h);
s.max_height = s.max_height.min(h);
}
if let Some(mw) = modifier.min_width {
s.min_width = s.min_width.max(mw);
}
if let Some(mh) = modifier.min_height {
s.min_height = s.min_height.max(mh);
}
if let Some(mw) = modifier.max_width {
s.max_width = s.max_width.min(mw);
}
if let Some(mh) = modifier.max_height {
s.max_height = s.max_height.min(mh);
}
if let Some(p) = modifier.padding {
let total = p * 2.0;
s.min_width = (s.min_width - total).max(repose_core::Dp::ZERO);
s.max_width = (s.max_width - total).max(repose_core::Dp::ZERO);
s.min_height = (s.min_height - total).max(repose_core::Dp::ZERO);
s.max_height = (s.max_height - total).max(repose_core::Dp::ZERO);
}
if let Some(pv) = modifier.padding_values {
let h_total = pv.left + pv.right;
let v_total = pv.top + pv.bottom;
s.min_width = (s.min_width - h_total).max(repose_core::Dp::ZERO);
s.max_width = (s.max_width - h_total).max(repose_core::Dp::ZERO);
s.min_height = (s.min_height - v_total).max(repose_core::Dp::ZERO);
s.max_height = (s.max_height - v_total).max(repose_core::Dp::ZERO);
}
if let Some(required) = modifier.required_size {
s.min_width = required.width.max(repose_core::Dp::ZERO);
s.max_width = s.min_width;
s.min_height = required.height.max(repose_core::Dp::ZERO);
s.max_height = s.min_height;
}
if let Some(value) = modifier.required_min_width {
s.min_width = value.max(repose_core::Dp::ZERO);
if s.min_width > s.max_width {
s.max_width = s.min_width;
}
}
if let Some(value) = modifier.required_max_width {
s.max_width = value.max(repose_core::Dp::ZERO);
if s.min_width > s.max_width {
s.min_width = s.max_width;
}
}
if let Some(value) = modifier.required_min_height {
s.min_height = value.max(repose_core::Dp::ZERO);
if s.min_height > s.max_height {
s.max_height = s.min_height;
}
}
if let Some(value) = modifier.required_max_height {
s.max_height = value.max(repose_core::Dp::ZERO);
if s.min_height > s.max_height {
s.min_height = s.max_height;
}
}
if s.min_width > s.max_width {
let coerced = s.max_width.max(repose_core::Dp::ZERO);
s.min_width = coerced;
s.max_width = coerced;
}
if s.min_height > s.max_height {
let coerced = s.max_height.max(repose_core::Dp::ZERO);
s.min_height = coerced;
s.max_height = coerced;
}
SubcomposeScope::new(s.min_width, s.max_width, s.min_height, s.max_height)
}
#[cfg(test)]
mod tests {
use super::*;
use repose_core::{
Color, Dp, DrawStyle, FontStyle, FontWeight, Modifier, Sp, SubcomposeScope, TextAlign,
TextDecoration, UnitExt, View, ViewKind,
};
use std::rc::Rc;
use std::sync::Arc;
fn text_view(text: &str) -> View {
View::new(
0,
ViewKind::Text {
text: text.to_string(),
color: Color::WHITE,
font_size: 16.0.sp(),
soft_wrap: true,
max_lines: None,
overflow: repose_core::TextOverflow::Visible,
font_family: None,
annotations: None,
text_align: TextAlign::Unspecified,
font_weight: FontWeight::NORMAL,
font_style: FontStyle::Normal,
text_decoration: TextDecoration::default(),
letter_spacing: Sp::ZERO,
line_height: Sp::ZERO,
url: None,
font_variation_settings: None,
draw_style: DrawStyle::Fill,
},
)
}
fn box_view() -> View {
View::new(0, ViewKind::Box)
}
#[test]
fn test_create_tree() {
let mut tree = ViewTree::new();
let root = box_view().with_children(vec![text_view("Hello"), text_view("World")]);
tree.update(&root);
assert_eq!(tree.len(), 3); assert!(tree.root().is_some());
}
#[test]
fn test_unchanged_tree_skips() {
let mut tree = ViewTree::new();
let root = box_view().with_children(vec![text_view("Hello")]);
tree.update(&root);
let gen1 = tree.generation();
tree.update(&root);
let gen2 = tree.generation();
assert_eq!(gen2, gen1 + 1);
assert!(tree.stats.skipped_nodes > 0);
}
#[test]
fn test_changed_content_reconciles() {
let mut tree = ViewTree::new();
let root1 = box_view().with_children(vec![text_view("Hello")]);
tree.update(&root1);
let root2 = box_view().with_children(vec![text_view("Changed")]);
tree.update(&root2);
assert!(tree.stats.reconciled_nodes > 0);
}
#[test]
fn generated_view_ids_are_unique_and_stable() {
let mut tree = ViewTree::new();
let branch = |text: &str| box_view().with_children(vec![text_view(text)]);
let root = box_view().with_children(vec![
text_view("keyed").modifier(Modifier::new().key(1)),
text_view("unkeyed"),
branch("left"),
branch("right"),
]);
tree.update(&root);
let first: Vec<(NodeId, u64)> = tree
.iter_with_ids()
.map(|(id, node)| (id, node.view_id))
.collect();
let unique: std::collections::HashSet<u64> = first.iter().map(|(_, id)| *id).collect();
assert_eq!(unique.len(), first.len());
for (node_id, view_id) in &first {
assert_eq!(
tree.get_by_view_id(*view_id).map(|node| node.id),
Some(*node_id)
);
}
tree.update(&root);
let second: Vec<(NodeId, u64)> = tree
.iter_with_ids()
.map(|(id, node)| (id, node.view_id))
.collect();
assert_eq!(first, second);
}
#[test]
fn duplicate_explicit_ids_keep_first_lookup_stable() {
let mut tree = ViewTree::new();
let mut first = text_view("first").modifier(Modifier::new().key(1));
let mut second = text_view("second").modifier(Modifier::new().key(2));
first.id = 42;
second.id = 42;
let root = box_view().with_children(vec![first.clone(), second.clone()]);
let root_id = tree.update(&root);
let first_id = tree.children(root_id).unwrap()[0];
assert_eq!(tree.get_by_view_id(42).map(|node| node.id), Some(first_id));
let root = box_view().with_children(vec![second]);
tree.update(&root);
let second_id = tree.children(tree.root().unwrap()).unwrap()[0];
assert_eq!(tree.get_by_view_id(42).map(|node| node.id), Some(second_id));
}
#[test]
fn test_keyed_children_stable() {
let mut tree = ViewTree::new();
let root1 = box_view().with_children(vec![
text_view("A").modifier(Modifier::new().key(1)),
text_view("B").modifier(Modifier::new().key(2)),
text_view("C").modifier(Modifier::new().key(3)),
]);
tree.update(&root1);
let _b_view_id = tree
.root()
.and_then(|r| tree.children(r))
.and_then(|c| c.get(1).copied())
.and_then(|id| tree.get(id))
.map(|n| n.view_id);
let root2 = box_view().with_children(vec![
text_view("C").modifier(Modifier::new().key(3)),
text_view("A").modifier(Modifier::new().key(1)),
text_view("B").modifier(Modifier::new().key(2)),
]);
tree.update(&root2);
assert_eq!(tree.len(), 4); }
fn subcompose_view<F>(f: F) -> View
where
F: Fn(&SubcomposeScope) -> View + 'static,
{
let content: Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>> =
Arc::new(move |scope| vec![(0, f(scope))]);
View {
id: 0,
kind: ViewKind::SubcomposeLayout { content },
modifier: Modifier::default(),
children: Vec::new(),
scope_key: None,
semantics: None,
}
}
#[test]
fn callback_identity_updates_handlers_without_layout_reset() {
let mut tree = ViewTree::new();
let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let content_calls = calls.clone();
let content: Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>> = Arc::new(move |_| {
content_calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
vec![(0, text_view("callback"))]
});
let make = |callback: Rc<dyn Fn()>| {
let callback = callback.clone();
View {
id: 0,
kind: ViewKind::SubcomposeLayout {
content: content.clone(),
},
modifier: Modifier::new().on_click(move || callback()),
children: Vec::new(),
scope_key: None,
semantics: None,
}
};
let root = make(Rc::new(|| {}));
let root_id = tree.update(&root);
tree.set_layout(
root_id,
Rect {
x: 0.0,
y: 0.0,
w: 100.0,
h: 100.0,
},
Rect {
x: 0.0,
y: 0.0,
w: 100.0,
h: 100.0,
},
crate::LayoutConstraints::default(),
);
let root = make(Rc::new(|| {}));
tree.update(&root);
let calls_before = calls.load(std::sync::atomic::Ordering::SeqCst);
tree.update(&root);
assert!(tree.get(root_id).unwrap().has_valid_layout());
assert_eq!(
calls.load(std::sync::atomic::Ordering::SeqCst),
calls_before
);
let calls_before = calls.load(std::sync::atomic::Ordering::SeqCst);
let root = make(Rc::new(|| {}));
tree.update(&root);
assert!(tree.get(root_id).unwrap().has_valid_layout());
assert_eq!(
calls.load(std::sync::atomic::Ordering::SeqCst),
calls_before
);
assert!(tree.paint_dirty_nodes().contains(&root_id));
}
#[test]
fn test_subcompose_invokes_content() {
let mut tree = ViewTree::new();
let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let counter2 = counter.clone();
let root = box_view().with_children(vec![subcompose_view(move |_scope| {
counter2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
text_view("from subcompose")
})]);
tree.update(&root);
assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
assert_eq!(tree.len(), 3); }
#[test]
fn test_subcompose_receives_scope() {
let mut tree = ViewTree::new();
let captured = Arc::new(std::sync::Mutex::new(None));
let captured2 = captured.clone();
let root = box_view().with_children(vec![subcompose_view(move |scope| {
*captured2.lock().unwrap() = Some(*scope);
text_view("hi")
})]);
tree.set_subcompose_scope(SubcomposeScope::new(Dp(0.0), Dp(360.0), Dp(0.0), Dp(640.0)));
tree.update(&root);
let observed = captured.lock().unwrap().expect("scope captured");
assert_eq!(observed.max_width, Dp(360.0));
assert_eq!(observed.max_height, Dp(640.0));
assert_eq!(observed.min_width, Dp(0.0));
assert_eq!(observed.min_height, Dp(0.0));
}
#[test]
fn zero_subcompose_bounds_are_real() {
let mut tree = ViewTree::new();
let captured = Arc::new(std::sync::Mutex::new(None));
let captured2 = captured.clone();
let root = box_view().with_children(vec![subcompose_view(move |scope| {
*captured2.lock().unwrap() = Some(*scope);
text_view("zero")
})]);
tree.set_subcompose_scope(SubcomposeScope::new(Dp(0.0), Dp(0.0), Dp(0.0), Dp(0.0)));
tree.update(&root);
let scope = captured.lock().unwrap().unwrap();
assert_eq!(scope.min_width, Dp(0.0));
assert_eq!(scope.max_width, Dp(0.0));
assert_eq!(scope.min_height, Dp(0.0));
assert_eq!(scope.max_height, Dp(0.0));
}
#[test]
fn test_subcompose_re_invokes_on_update() {
let mut tree = ViewTree::new();
let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let counter2 = counter.clone();
let root = box_view().with_children(vec![subcompose_view(move |_scope| {
counter2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
text_view("hi")
})]);
tree.update(&root);
tree.update(&root);
tree.update(&root);
assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
}
#[test]
fn test_subcompose_reruns_on_scope_change() {
let mut tree = ViewTree::new();
let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let counter2 = counter.clone();
let root = box_view().with_children(vec![subcompose_view(move |_scope| {
counter2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
text_view("hi")
})]);
tree.set_subcompose_scope(SubcomposeScope::new(Dp(0.0), Dp(100.0), Dp(0.0), Dp(100.0)));
tree.update(&root);
assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
tree.update(&root);
assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
tree.set_subcompose_scope(SubcomposeScope::new(Dp(0.0), Dp(200.0), Dp(0.0), Dp(200.0)));
tree.update(&root);
assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 2);
}
#[test]
fn test_subcompose_reruns_on_content_change() {
let mut tree = ViewTree::new();
let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let c1 = counter.clone();
let root1 = box_view().with_children(vec![subcompose_view(move |_scope| {
c1.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
text_view("hi")
})]);
tree.update(&root1);
assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
tree.update(&root1);
assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
let c2 = counter.clone();
let root2 = box_view().with_children(vec![
subcompose_view(move |_scope| {
c2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
text_view("hi")
})
.modifier(Modifier::new().padding(4.0.dp())),
]);
tree.update(&root2);
assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 2);
}
#[test]
fn test_subcompose_cache_drops_on_node_removal() {
let mut tree = ViewTree::new();
tree.set_subcompose_scope(SubcomposeScope::new(Dp(0.0), Dp(100.0), Dp(0.0), Dp(100.0)));
let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let c1 = counter.clone();
let root_with_sub = box_view().with_children(vec![subcompose_view(move |_scope| {
c1.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
text_view("hi")
})]);
tree.update(&root_with_sub);
assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
let root_no_sub = box_view().with_children(vec![text_view("plain")]);
tree.update(&root_no_sub);
assert_eq!(tree.len(), 2);
let c2 = counter.clone();
let root_with_sub_again = box_view().with_children(vec![subcompose_view(move |_scope| {
c2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
text_view("hi")
})]);
tree.update(&root_with_sub_again);
assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 2);
}
fn multi_slot_view<F>(f: F) -> View
where
F: Fn(&SubcomposeScope) -> Vec<(u64, View)> + 'static,
{
let content: Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>> = Arc::new(f);
View {
id: 0,
kind: ViewKind::SubcomposeLayout { content },
modifier: Modifier::default(),
children: Vec::new(),
scope_key: None,
semantics: None,
}
}
#[test]
fn test_subcompose_multi_slot_produces_multiple_children() {
let mut tree = ViewTree::new();
let root = box_view().with_children(vec![multi_slot_view(|_scope| {
vec![
(0, text_view("a")),
(1, text_view("b")),
(2, text_view("c")),
]
})]);
tree.update(&root);
assert_eq!(tree.len(), 5);
let sub_id = tree
.root()
.and_then(|r| tree.children(r))
.and_then(|c| c.first().copied())
.expect("subcompose node");
let sub_children = tree.children(sub_id).expect("subcompose has children");
assert_eq!(sub_children.len(), 3);
}
#[test]
fn test_subcompose_multi_slot_preserves_identity_across_removal() {
let mut tree = ViewTree::new();
let root3 = box_view().with_children(vec![multi_slot_view(|_scope| {
vec![
(0, text_view("a")),
(1, text_view("b")),
(2, text_view("c")),
]
})]);
tree.update(&root3);
let sub_id = tree
.root()
.and_then(|r| tree.children(r))
.and_then(|c| c.first().copied())
.expect("subcompose node");
let before = tree.children(sub_id).expect("children").to_vec();
let a_node = before[0];
let b_node = before[1];
let c_node = before[2];
let root2 = box_view().with_children(vec![
multi_slot_view(|_scope| vec![(0, text_view("a")), (2, text_view("c"))])
.modifier(Modifier::new().padding(4.0.dp())),
]);
tree.update(&root2);
let after = tree.children(sub_id).expect("children after");
assert_eq!(after.len(), 2);
assert_eq!(after[0], a_node);
assert_eq!(after[1], c_node);
assert!(tree.get(b_node).is_none());
}
#[test]
fn test_subcompose_ancestor_modifier_narrows_scope() {
let mut tree = ViewTree::new();
tree.set_subcompose_scope(SubcomposeScope::new(
Dp(0.0),
Dp(1000.0),
Dp(0.0),
Dp(1000.0),
));
let captured = Arc::new(std::sync::Mutex::new(SubcomposeScope::UNBOUNDED));
let cap2 = captured.clone();
let sub = multi_slot_view(move |scope| {
*cap2.lock().unwrap() = *scope;
vec![(0, text_view("hi"))]
});
let root = box_view()
.modifier(Modifier::new().width(200.0.dp()))
.with_children(vec![sub]);
tree.update(&root);
let observed = *captured.lock().unwrap();
assert_eq!(observed.max_width, Dp(200.0));
}
#[test]
fn test_subcompose_chained_ancestor_constraints_intersect() {
let mut tree = ViewTree::new();
tree.set_subcompose_scope(SubcomposeScope::new(
Dp(0.0),
Dp(1000.0),
Dp(0.0),
Dp(1000.0),
));
let captured = Arc::new(std::sync::Mutex::new(SubcomposeScope::UNBOUNDED));
let cap2 = captured.clone();
let sub = multi_slot_view(move |scope| {
*cap2.lock().unwrap() = *scope;
vec![(0, text_view("hi"))]
});
let root = box_view()
.modifier(Modifier::new().width(400.0.dp()))
.with_children(vec![
box_view()
.modifier(Modifier::new().max_width(300.0.dp()))
.with_children(vec![sub]),
]);
tree.update(&root);
let observed = *captured.lock().unwrap();
assert_eq!(observed.max_width, Dp(300.0));
}
#[test]
#[allow(clippy::arc_with_non_send_sync)]
fn test_subcompose_nested_layouts_inherit_narrowed_scope() {
let mut tree = ViewTree::new();
tree.set_subcompose_scope(SubcomposeScope::new(
Dp(0.0),
Dp(1000.0),
Dp(0.0),
Dp(1000.0),
));
let outer_captured = Arc::new(std::sync::Mutex::new(SubcomposeScope::UNBOUNDED));
let inner_captured = Arc::new(std::sync::Mutex::new(SubcomposeScope::UNBOUNDED));
let outer2 = outer_captured.clone();
let inner2 = inner_captured.clone();
let inner = Arc::new(multi_slot_view(move |scope| {
*inner2.lock().unwrap() = *scope;
vec![(0, text_view("inner"))]
}));
let inner_clone = inner.clone();
let outer = multi_slot_view(move |scope| {
*outer2.lock().unwrap() = *scope;
vec![(0, (*inner_clone).clone())]
})
.modifier(Modifier::new().width(400.0.dp()));
let root = box_view().with_children(vec![outer]);
tree.update(&root);
let outer_obs = *outer_captured.lock().unwrap();
let inner_obs = *inner_captured.lock().unwrap();
assert_eq!(outer_obs.max_width, Dp(400.0));
assert_eq!(inner_obs.max_width, Dp(400.0));
}
}