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