diff --git a/masonry/src/tests/update.rs b/masonry/src/tests/update.rs index f93756449..d095449d3 100644 --- a/masonry/src/tests/update.rs +++ b/masonry/src/tests/update.rs @@ -805,3 +805,54 @@ fn status_flag_update_order() { assert!(!harness.get_widget(parent1_tag).ctx().is_focus_target()); assert!(!harness.get_widget(parent1_tag).ctx().has_focus_target()); } + +/// `RenderRoot::set_default_properties` should re-fire `property_changed` on +/// every cached property across the tree, so a host can swap the default set at +/// runtime and have widgets react. +#[test] +fn set_default_properties_refires_property_changed() { + use std::any::TypeId; + use std::cell::RefCell; + use std::rc::Rc; + use std::sync::Arc; + + use crate::peniko::color::AlphaColor; + use crate::properties::ContentColor; + + let changed: Rc>> = Rc::new(RefCell::new(Vec::new())); + + let widget = { + let changed = changed.clone(); + ModularWidget::new(()) + // Read `ContentColor` during paint so it lands in the property cache. + .paint_fn(|_, ctx, props, _painter| { + let cache = ctx.property_cache(); + let _ = props.get::(cache); + }) + .property_change_fn(move |_, _ctx, property_type| { + changed.borrow_mut().push(property_type); + }) + }; + + // Seed a default `ContentColor` for this widget type and run a frame so the + // widget caches it. + let mut defaults = test_property_set(); + defaults.insert::, ContentColor>(ContentColor::new(AlphaColor::WHITE)); + let mut harness = TestHarness::create(defaults, NewWidget::new(widget)); + let _ = harness.redraw(); + changed.borrow_mut().clear(); + + // Swap in a different default and run another frame; the update-props pass + // should re-fire `property_changed` for every cached property, including + // `ContentColor`. + let mut new_defaults = test_property_set(); + new_defaults.insert::, ContentColor>(ContentColor::new(AlphaColor::BLACK)); + harness.set_default_properties(Arc::new(new_defaults)); + let _ = harness.redraw(); + + assert!( + changed.borrow().contains(&TypeId::of::()), + "expected property_changed to re-fire for ContentColor after the default swap, got {:?}", + changed.borrow(), + ); +} diff --git a/masonry_core/src/app/render_root.rs b/masonry_core/src/app/render_root.rs index 729bb4ef5..be19143ba 100644 --- a/masonry_core/src/app/render_root.rs +++ b/masonry_core/src/app/render_root.rs @@ -413,6 +413,42 @@ impl RenderRoot { &mut self.property_arena } + /// Replace the tree-wide default properties at runtime. + /// + /// The default property set is normally fixed at construction. This lets + /// a host swap it mid-session, e.g. a light/dark theme toggle that + /// re-colors widgets relying on default `ContentColor` / `Background`. + /// + /// This invalidates the computed properties of the entire widget tree, + /// and calls [`Widget::property_changed`] for every property previously + /// resolved by each widget. + pub fn set_default_properties(&mut self, default_properties: Arc) { + self.property_arena.default_properties = default_properties; + + // Mark the whole tree for the update-properties pass: `needs_update_props` + // so the pass descends to every node, `request_update_props` to enter the + // cache-eviction branch, and `invalidated` so every cached entry re-fires + // `property_changed` regardless of its (unchanged) resolved index. + fn invalidate_props_all_in(node: ArenaMut<'_, WidgetArenaNode>) { + let children = node.children; + let widget = &mut *node.item.widget; + let state = &mut node.item.state; + + state.request_update_props = true; + state.needs_update_props = true; + state.property_cache.invalidated = true; + + let id = state.id; + recurse_on_children(id, widget, children, |node| { + invalidate_props_all_in(node); + }); + } + let root_node = self.widget_arena.get_node_mut(self.root_id()); + invalidate_props_all_in(root_node); + + self.run_rewrite_passes(); + } + pub(crate) fn root_id(&self) -> WidgetId { self.layer_stack.id() } diff --git a/masonry_testing/src/harness.rs b/masonry_testing/src/harness.rs index 59ea10d91..aacfd6ec6 100644 --- a/masonry_testing/src/harness.rs +++ b/masonry_testing/src/harness.rs @@ -875,6 +875,13 @@ impl TestHarness { self.render_root.get_layer_root(0).id() } + /// Replace the tree-wide default properties at runtime. + /// + /// Mirrors [`RenderRoot::set_default_properties`]. + pub fn set_default_properties(&mut self, default_properties: Arc) { + self.render_root.set_default_properties(default_properties); + } + /// Returns a [`WidgetRef`] to the widget with the given id. /// /// # Panics