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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
//! Material Design 3 Theme System
//!
//! Provides a complete color scheme and theming system based on MD3 guidelines.
//! Reference: <https://m3.material.io/styles/color/overview>
use bevy::prelude::*;
use crate::color::MaterialColorScheme;
/// Theme mode (light or dark)
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ThemeMode {
/// Light theme
Light,
/// Dark theme (default for game applications)
#[default]
Dark,
}
/// Color scheme variant
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ColorScheme {
/// Default Material You purple/violet scheme
#[default]
Default,
/// Custom scheme (use with `MaterialTheme::from_seed`)
Custom,
}
/// Material Design 3 Theme Resource
///
/// Contains all color tokens for the Material Design 3 color system.
/// Use this resource to style your UI components consistently.
///
/// # Example
///
/// ```rust,no_run
/// use bevy::prelude::*;
/// use bevy_material_ui::theme::MaterialTheme;
///
/// fn setup_ui(theme: Res<MaterialTheme>, mut commands: Commands) {
/// commands.spawn((
/// Node {
/// width: Val::Percent(100.0),
/// height: Val::Percent(100.0),
/// ..default()
/// },
/// BackgroundColor(theme.surface),
/// ));
/// }
/// ```
#[derive(Resource, Debug, Clone)]
pub struct MaterialTheme {
/// Current theme mode
pub mode: ThemeMode,
// Primary colors
/// Primary brand color
pub primary: Color,
/// Color for content on primary
pub on_primary: Color,
/// Primary container color
pub primary_container: Color,
/// Color for content on primary container
pub on_primary_container: Color,
// Secondary colors
/// Secondary brand color
pub secondary: Color,
/// Color for content on secondary
pub on_secondary: Color,
/// Secondary container color
pub secondary_container: Color,
/// Color for content on secondary container
pub on_secondary_container: Color,
// Tertiary colors
/// Tertiary accent color
pub tertiary: Color,
/// Color for content on tertiary
pub on_tertiary: Color,
/// Tertiary container color
pub tertiary_container: Color,
/// Color for content on tertiary container
pub on_tertiary_container: Color,
// Error colors
/// Error state color
pub error: Color,
/// Color for content on error
pub on_error: Color,
/// Error container color
pub error_container: Color,
/// Color for content on error container
pub on_error_container: Color,
// Surface colors
/// Base surface color
pub surface: Color,
/// Color for content on surface
pub on_surface: Color,
/// Variant of on_surface for less emphasis
pub on_surface_variant: Color,
/// Lowest surface container
pub surface_container_lowest: Color,
/// Low surface container
pub surface_container_low: Color,
/// Default surface container
pub surface_container: Color,
/// High surface container
pub surface_container_high: Color,
/// Highest surface container
pub surface_container_highest: Color,
// Other colors
/// Outline color for borders
pub outline: Color,
/// Variant outline for subtle borders
pub outline_variant: Color,
/// Inverse surface for contrast
pub inverse_surface: Color,
/// Content on inverse surface
pub inverse_on_surface: Color,
/// Inverse primary for contrast
pub inverse_primary: Color,
/// Scrim overlay color
pub scrim: Color,
/// Shadow color
pub shadow: Color,
// Custom game-specific colors
/// Color for selected/active states
pub selected: Color,
/// Color for unselected/inactive states
pub unselected: Color,
}
impl Default for MaterialTheme {
fn default() -> Self {
Self::dark()
}
}
impl MaterialTheme {
/// Create a theme from a seed color using MD3 scheme generation.
pub fn from_seed(seed: Color, mode: ThemeMode) -> Self {
let scheme = match mode {
ThemeMode::Dark => MaterialColorScheme::dark_from_bevy_color(seed),
ThemeMode::Light => MaterialColorScheme::light_from_bevy_color(seed),
};
Self {
mode,
primary: scheme.primary,
on_primary: scheme.on_primary,
primary_container: scheme.primary_container,
on_primary_container: scheme.on_primary_container,
secondary: scheme.secondary,
on_secondary: scheme.on_secondary,
secondary_container: scheme.secondary_container,
on_secondary_container: scheme.on_secondary_container,
tertiary: scheme.tertiary,
on_tertiary: scheme.on_tertiary,
tertiary_container: scheme.tertiary_container,
on_tertiary_container: scheme.on_tertiary_container,
error: scheme.error,
on_error: scheme.on_error,
error_container: scheme.error_container,
on_error_container: scheme.on_error_container,
surface: scheme.surface,
on_surface: scheme.on_surface,
on_surface_variant: scheme.on_surface_variant,
surface_container_lowest: scheme.surface_container_lowest,
surface_container_low: scheme.surface_container_low,
surface_container: scheme.surface_container,
surface_container_high: scheme.surface_container_high,
surface_container_highest: scheme.surface_container_highest,
outline: scheme.outline,
outline_variant: scheme.outline_variant,
inverse_surface: scheme.inverse_surface,
inverse_on_surface: scheme.inverse_on_surface,
inverse_primary: scheme.inverse_primary,
scrim: scheme.scrim,
shadow: scheme.shadow,
selected: scheme.primary,
unselected: scheme.outline,
}
}
/// Create a dark theme (recommended for games)
pub fn dark() -> Self {
Self {
mode: ThemeMode::Dark,
// Primary - Purple/Violet
primary: Color::srgb(0.82, 0.71, 1.0), // #D0B4FF
on_primary: Color::srgb(0.25, 0.09, 0.46), // #402076
primary_container: Color::srgb(0.38, 0.23, 0.58), // #61398E
on_primary_container: Color::srgb(0.92, 0.85, 1.0), // #EBDAFF
// Secondary
secondary: Color::srgb(0.80, 0.78, 0.90), // #CCC6E0
on_secondary: Color::srgb(0.21, 0.19, 0.31), // #343046
secondary_container: Color::srgb(0.32, 0.30, 0.43), // #4B465E
on_secondary_container: Color::srgb(0.92, 0.90, 1.0), // #E9E1FC
// Tertiary
tertiary: Color::srgb(0.94, 0.73, 0.78), // #F0BAC7
on_tertiary: Color::srgb(0.29, 0.14, 0.20), // #4A2532
tertiary_container: Color::srgb(0.42, 0.26, 0.34), // #633B49
on_tertiary_container: Color::srgb(1.0, 0.85, 0.89), // #FFD9E3
// Error
error: Color::srgb(1.0, 0.71, 0.68), // #FFB4AB
on_error: Color::srgb(0.41, 0.0, 0.04), // #690006
error_container: Color::srgb(0.58, 0.0, 0.07), // #93000A
on_error_container: Color::srgb(1.0, 0.85, 0.82), // #FFD9D4
// Surface - Dark theme
surface: Color::srgb(0.08, 0.07, 0.09), // #141316
on_surface: Color::srgb(0.90, 0.87, 0.92), // #E6E1E9
on_surface_variant: Color::srgb(0.78, 0.74, 0.82), // #C9C4D0
surface_container_lowest: Color::srgb(0.05, 0.04, 0.06), // #0D0C0F
surface_container_low: Color::srgb(0.11, 0.10, 0.12), // #1C1B1E
surface_container: Color::srgb(0.13, 0.12, 0.14), // #211F23
surface_container_high: Color::srgb(0.17, 0.16, 0.18), // #2B292D
surface_container_highest: Color::srgb(0.21, 0.20, 0.23), // #363438
// Other
outline: Color::srgb(0.58, 0.55, 0.62), // #938E9A
outline_variant: Color::srgb(0.29, 0.27, 0.32), // #48454F
inverse_surface: Color::srgb(0.90, 0.87, 0.92), // #E6E1E9
inverse_on_surface: Color::srgb(0.19, 0.18, 0.20), // #302E32
inverse_primary: Color::srgb(0.50, 0.35, 0.71), // #7F58B5
scrim: Color::srgb(0.0, 0.0, 0.0), // #000000
shadow: Color::srgb(0.0, 0.0, 0.0), // #000000
// Game-specific
selected: Color::srgb(0.82, 0.71, 1.0), // Same as primary
unselected: Color::srgb(0.58, 0.55, 0.62), // Same as outline
}
}
/// Create a light theme
pub fn light() -> Self {
Self {
mode: ThemeMode::Light,
// Primary - Purple/Violet
primary: Color::srgb(0.50, 0.35, 0.71), // #7F58B5
on_primary: Color::srgb(1.0, 1.0, 1.0), // #FFFFFF
primary_container: Color::srgb(0.92, 0.85, 1.0), // #EBDAFF
on_primary_container: Color::srgb(0.15, 0.0, 0.34), // #260052
// Secondary
secondary: Color::srgb(0.38, 0.36, 0.50), // #605D75
on_secondary: Color::srgb(1.0, 1.0, 1.0), // #FFFFFF
secondary_container: Color::srgb(0.92, 0.90, 1.0), // #E9E1FD
on_secondary_container: Color::srgb(0.11, 0.09, 0.20), // #1C1930
// Tertiary
tertiary: Color::srgb(0.52, 0.33, 0.41), // #7D5260
on_tertiary: Color::srgb(1.0, 1.0, 1.0), // #FFFFFF
tertiary_container: Color::srgb(1.0, 0.85, 0.89), // #FFD9E3
on_tertiary_container: Color::srgb(0.19, 0.05, 0.13), // #31101D
// Error
error: Color::srgb(0.73, 0.11, 0.15), // #BA1A24
on_error: Color::srgb(1.0, 1.0, 1.0), // #FFFFFF
error_container: Color::srgb(1.0, 0.85, 0.82), // #FFD9D4
on_error_container: Color::srgb(0.26, 0.0, 0.02), // #410003
// Surface - Light theme
surface: Color::srgb(0.99, 0.97, 1.0), // #FDF8FF
on_surface: Color::srgb(0.11, 0.10, 0.12), // #1C1B1E
on_surface_variant: Color::srgb(0.29, 0.27, 0.32), // #48454F
surface_container_lowest: Color::srgb(1.0, 1.0, 1.0), // #FFFFFF
surface_container_low: Color::srgb(0.97, 0.95, 0.98), // #F7F2FA
surface_container: Color::srgb(0.95, 0.93, 0.96), // #F1ECF4
surface_container_high: Color::srgb(0.92, 0.90, 0.93), // #EBE6EE
surface_container_highest: Color::srgb(0.90, 0.87, 0.91), // #E5E1E9
// Other
outline: Color::srgb(0.47, 0.44, 0.51), // #79757F
outline_variant: Color::srgb(0.78, 0.75, 0.82), // #C9C4D0
inverse_surface: Color::srgb(0.19, 0.18, 0.20), // #302E32
inverse_on_surface: Color::srgb(0.96, 0.94, 0.97), // #F4EFF7
inverse_primary: Color::srgb(0.82, 0.71, 1.0), // #D0B4FF
scrim: Color::srgb(0.0, 0.0, 0.0), // #000000
shadow: Color::srgb(0.0, 0.0, 0.0), // #000000
// Game-specific
selected: Color::srgb(0.50, 0.35, 0.71), // Same as primary
unselected: Color::srgb(0.47, 0.44, 0.51), // Same as outline
}
}
/// Toggle between light and dark mode
pub fn toggle_mode(&mut self) {
*self = match self.mode {
ThemeMode::Light => Self::dark(),
ThemeMode::Dark => Self::light(),
};
}
/// Get the appropriate state layer opacity for a given interaction state
pub fn state_layer_opacity(state: StateLayer) -> f32 {
match state {
StateLayer::None => 0.0,
StateLayer::Hover => 0.08,
StateLayer::Focus => 0.12,
StateLayer::Pressed => 0.12,
StateLayer::Dragged => 0.16,
}
}
/// Apply a state layer color on top of a base color
pub fn with_state_layer(&self, base: Color, state: StateLayer, content_color: Color) -> Color {
let opacity = Self::state_layer_opacity(state);
if opacity == 0.0 {
return base;
}
// Blend the content color over the base with the state layer opacity
let base_linear = base.to_linear();
let content_linear = content_color.to_linear();
Color::linear_rgba(
base_linear.red + (content_linear.red - base_linear.red) * opacity,
base_linear.green + (content_linear.green - base_linear.green) * opacity,
base_linear.blue + (content_linear.blue - base_linear.blue) * opacity,
base_linear.alpha,
)
}
}
/// State layer for interaction feedback
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum StateLayer {
/// No state layer
#[default]
None,
/// Hover state (8% opacity)
Hover,
/// Focus state (12% opacity)
Focus,
/// Pressed state (12% opacity)
Pressed,
/// Dragged state (16% opacity)
Dragged,
}
/// Blend a state layer color over a base color with given opacity
///
/// This is a standalone helper function for applying MD3 state layers.
/// The state layer is a semi-transparent overlay of the content/state color.
///
/// # Arguments
/// * `base` - The base background color
/// * `state_layer_color` - The color of the state layer (usually "on" color like on_primary)
/// * `opacity` - The opacity of the state layer (0.08 for hover, 0.12 for pressed)
pub fn blend_state_layer(base: Color, state_layer_color: Color, opacity: f32) -> Color {
if opacity <= 0.0 {
return base;
}
let base_linear = base.to_linear();
let layer_linear = state_layer_color.to_linear();
// Alpha blending: result = base * (1 - opacity) + layer * opacity
Color::linear_rgba(
base_linear.red * (1.0 - opacity) + layer_linear.red * opacity,
base_linear.green * (1.0 - opacity) + layer_linear.green * opacity,
base_linear.blue * (1.0 - opacity) + layer_linear.blue * opacity,
base_linear.alpha,
)
}