Skip to main content

rosace_widgets/tree/
material.rs

1//! The material cascade (D124/Phase 33): resolve which [`ShaderMaterial`] a
2//! widget draws with, `instance → theme → none`.
3//!
4//! This is deliberately the SAME shape as `resolve_physics`
5//! (`scroll_view.rs`) — the codebase's established precedent for
6//! `explicit → theme.ext → default` resolution — just returning `Option`
7//! (a widget with no material at any level renders normally, its
8//! pre-material default).
9//!
10//! **"Global" is per widget KIND**, not one material for every surface.
11//! Each material-capable widget declares its own key newtype implementing
12//! [`MaterialKey`] (`CardMaterial(ShaderMaterial)`, `ContainerMaterial(..)`,
13//! …) and stashes it on the theme via `ThemeData::with_ext` (D105's
14//! type-keyed extension map). A `Card` can be glass while `Button`s stay
15//! solid; a third-party widget defines its OWN key + material slot without
16//! editing rosace — the same extensibility bar the icon registry (D115) set.
17
18use rosace_shader::ShaderMaterial;
19use rosace_theme::ThemeData;
20
21/// A theme-extension newtype that carries a widget-kind's default material.
22///
23/// Implement on a `pub struct FooMaterial(pub ShaderMaterial)` for widget
24/// `Foo`, register it with `theme.with_ext(FooMaterial(m))`, and `Foo`'s
25/// paint resolves it via [`resolve_material`]. The newtype (not a bare
26/// `ShaderMaterial`) is what type-keys the theme slot per widget-kind — two
27/// widgets can hold different app-wide materials at once.
28pub trait MaterialKey: std::any::Any + Send + Sync + 'static {
29    fn material(&self) -> &ShaderMaterial;
30}
31
32/// Resolve the effective material for a widget: an explicit per-instance
33/// `.material(...)` wins; else the theme's `K` default (if registered);
34/// else `None` (render normally). Mirrors `resolve_physics`'s
35/// `explicit.or_else(theme.ext)` precedent exactly.
36pub fn resolve_material<K: MaterialKey>(
37    theme: &ThemeData,
38    explicit: Option<&ShaderMaterial>,
39) -> Option<ShaderMaterial> {
40    explicit
41        .cloned()
42        .or_else(|| theme.ext::<K>().map(|k| k.material().clone()))
43}
44
45/// `Container`'s theme-default material slot — `theme.with_ext(ContainerMaterial(m))`.
46pub struct ContainerMaterial(pub ShaderMaterial);
47impl MaterialKey for ContainerMaterial {
48    fn material(&self) -> &ShaderMaterial { &self.0 }
49}
50
51/// `Card`'s theme-default material slot — `theme.with_ext(CardMaterial(m))`.
52pub struct CardMaterial(pub ShaderMaterial);
53impl MaterialKey for CardMaterial {
54    fn material(&self) -> &ShaderMaterial { &self.0 }
55}
56
57/// `Dialog`'s theme-default material slot (D124 Phase 33 Step 5).
58pub struct DialogMaterial(pub ShaderMaterial);
59impl MaterialKey for DialogMaterial {
60    fn material(&self) -> &ShaderMaterial { &self.0 }
61}
62
63/// `Sheet`'s theme-default material slot (D124 Phase 33 Step 5).
64pub struct SheetMaterial(pub ShaderMaterial);
65impl MaterialKey for SheetMaterial {
66    fn material(&self) -> &ShaderMaterial { &self.0 }
67}
68
69/// `Drawer`'s theme-default material slot (D124 Phase 33 Step 5).
70pub struct DrawerMaterial(pub ShaderMaterial);
71impl MaterialKey for DrawerMaterial {
72    fn material(&self) -> &ShaderMaterial { &self.0 }
73}
74
75/// `AppBar`'s theme-default material slot (D124 Phase 33 Step 5).
76pub struct AppBarMaterial(pub ShaderMaterial);
77impl MaterialKey for AppBarMaterial {
78    fn material(&self) -> &ShaderMaterial { &self.0 }
79}
80
81/// `BottomNavigationBar`'s theme-default material slot (D124 Phase 33 Step 5).
82pub struct BottomNavMaterial(pub ShaderMaterial);
83impl MaterialKey for BottomNavMaterial {
84    fn material(&self) -> &ShaderMaterial { &self.0 }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use rosace_shader::PipelineId;
91
92    struct TestKey(ShaderMaterial);
93    impl MaterialKey for TestKey {
94        fn material(&self) -> &ShaderMaterial { &self.0 }
95    }
96
97    fn mat(id: u64) -> ShaderMaterial {
98        ShaderMaterial::new(PipelineId::user(0x1000 + id), vec![id as u8])
99    }
100
101    #[test]
102    fn instance_material_wins_over_theme() {
103        let theme = rosace_theme::built_in::dark_theme().with_ext(TestKey(mat(1)));
104        let explicit = mat(2);
105        let resolved = resolve_material::<TestKey>(&theme, Some(&explicit));
106        assert_eq!(resolved, Some(mat(2)), "explicit instance material must win");
107    }
108
109    #[test]
110    fn theme_material_used_when_no_instance() {
111        let theme = rosace_theme::built_in::dark_theme().with_ext(TestKey(mat(3)));
112        let resolved = resolve_material::<TestKey>(&theme, None);
113        assert_eq!(resolved, Some(mat(3)), "theme default applies with no instance override");
114    }
115
116    #[test]
117    fn none_when_neither_set() {
118        let theme = rosace_theme::built_in::dark_theme();
119        let resolved = resolve_material::<TestKey>(&theme, None);
120        assert!(resolved.is_none(), "no material anywhere → render normally");
121    }
122
123    #[test]
124    fn distinct_keys_hold_independent_materials() {
125        struct OtherKey(ShaderMaterial);
126        impl MaterialKey for OtherKey {
127            fn material(&self) -> &ShaderMaterial { &self.0 }
128        }
129        // Same theme, two widget-kind keys, two different materials — proves
130        // "global is per widget kind", not one shader for everything.
131        let theme = rosace_theme::built_in::dark_theme()
132            .with_ext(TestKey(mat(5)))
133            .with_ext(OtherKey(mat(6)));
134        assert_eq!(resolve_material::<TestKey>(&theme, None), Some(mat(5)));
135        assert_eq!(resolve_material::<OtherKey>(&theme, None), Some(mat(6)));
136    }
137}