Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_feathers/src/theme.rs
6849 views
1
//! A framework for theming.
2
use bevy_app::{Propagate, PropagateOver};
3
use bevy_color::{palettes, Color};
4
use bevy_ecs::{
5
change_detection::DetectChanges,
6
component::Component,
7
lifecycle::Insert,
8
observer::On,
9
query::Changed,
10
reflect::{ReflectComponent, ReflectResource},
11
resource::Resource,
12
system::{Commands, Query, Res},
13
};
14
use bevy_log::warn_once;
15
use bevy_platform::collections::HashMap;
16
use bevy_reflect::{prelude::ReflectDefault, Reflect};
17
use bevy_text::TextColor;
18
use bevy_ui::{BackgroundColor, BorderColor};
19
use smol_str::SmolStr;
20
21
/// A design token for the theme. This serves as the lookup key for the theme properties.
22
#[derive(Clone, PartialEq, Eq, Hash, Reflect)]
23
pub struct ThemeToken(SmolStr);
24
25
impl ThemeToken {
26
/// Construct a new [`ThemeToken`] from a [`SmolStr`].
27
pub const fn new(text: SmolStr) -> Self {
28
Self(text)
29
}
30
31
/// Construct a new [`ThemeToken`] from a static string.
32
pub const fn new_static(text: &'static str) -> Self {
33
Self(SmolStr::new_static(text))
34
}
35
}
36
37
impl core::fmt::Display for ThemeToken {
38
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
39
write!(f, "{}", self.0)
40
}
41
}
42
43
impl core::fmt::Debug for ThemeToken {
44
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
45
write!(f, "ThemeToken({:?})", self.0)
46
}
47
}
48
49
/// A collection of properties that make up a theme.
50
#[derive(Default, Clone, Reflect, Debug)]
51
#[reflect(Default, Debug)]
52
pub struct ThemeProps {
53
/// Map of design tokens to colors.
54
pub color: HashMap<ThemeToken, Color>,
55
// Other style property types to be added later.
56
}
57
58
/// The currently selected user interface theme. Overwriting this resource changes the theme.
59
#[derive(Resource, Default, Reflect, Debug)]
60
#[reflect(Resource, Default, Debug)]
61
pub struct UiTheme(pub ThemeProps);
62
63
impl UiTheme {
64
/// Lookup a color by design token. If the theme does not have an entry for that token,
65
/// logs a warning and returns an error color.
66
pub fn color(&self, token: &ThemeToken) -> Color {
67
let color = self.0.color.get(token);
68
match color {
69
Some(c) => *c,
70
None => {
71
warn_once!("Theme color {} not found.", token);
72
// Return a bright obnoxious color to make the error obvious.
73
palettes::basic::FUCHSIA.into()
74
}
75
}
76
}
77
78
/// Associate a design token with a given color.
79
pub fn set_color(&mut self, token: &str, color: Color) {
80
self.0
81
.color
82
.insert(ThemeToken::new(SmolStr::new(token)), color);
83
}
84
}
85
86
/// Component which causes the background color of an entity to be set based on a theme color.
87
#[derive(Component, Clone)]
88
#[require(BackgroundColor)]
89
#[component(immutable)]
90
#[derive(Reflect)]
91
#[reflect(Component, Clone)]
92
pub struct ThemeBackgroundColor(pub ThemeToken);
93
94
/// Component which causes the border color of an entity to be set based on a theme color.
95
/// Only supports setting all borders to the same color.
96
#[derive(Component, Clone)]
97
#[require(BorderColor)]
98
#[component(immutable)]
99
#[derive(Reflect)]
100
#[reflect(Component, Clone)]
101
pub struct ThemeBorderColor(pub ThemeToken);
102
103
/// Component which causes the inherited text color of an entity to be set based on a theme color.
104
#[derive(Component, Clone)]
105
#[component(immutable)]
106
#[derive(Reflect)]
107
#[reflect(Component, Clone)]
108
#[require(ThemedText, PropagateOver::<TextColor>::default())]
109
pub struct ThemeFontColor(pub ThemeToken);
110
111
/// A marker component that is used to indicate that the text entity wants to opt-in to using
112
/// inherited text styles.
113
#[derive(Component, Reflect, Default)]
114
#[reflect(Component)]
115
pub struct ThemedText;
116
117
pub(crate) fn update_theme(
118
mut q_background: Query<(&mut BackgroundColor, &ThemeBackgroundColor)>,
119
mut q_border: Query<(&mut BorderColor, &ThemeBorderColor)>,
120
theme: Res<UiTheme>,
121
) {
122
if theme.is_changed() {
123
// Update all background colors
124
for (mut bg, theme_bg) in q_background.iter_mut() {
125
bg.0 = theme.color(&theme_bg.0);
126
}
127
128
// Update all border colors
129
for (mut border, theme_border) in q_border.iter_mut() {
130
border.set_all(theme.color(&theme_border.0));
131
}
132
}
133
}
134
135
pub(crate) fn on_changed_background(
136
insert: On<Insert, ThemeBackgroundColor>,
137
mut q_background: Query<
138
(&mut BackgroundColor, &ThemeBackgroundColor),
139
Changed<ThemeBackgroundColor>,
140
>,
141
theme: Res<UiTheme>,
142
) {
143
// Update background colors where the design token has changed.
144
if let Ok((mut bg, theme_bg)) = q_background.get_mut(insert.entity) {
145
bg.0 = theme.color(&theme_bg.0);
146
}
147
}
148
149
pub(crate) fn on_changed_border(
150
insert: On<Insert, ThemeBorderColor>,
151
mut q_border: Query<(&mut BorderColor, &ThemeBorderColor), Changed<ThemeBorderColor>>,
152
theme: Res<UiTheme>,
153
) {
154
// Update background colors where the design token has changed.
155
if let Ok((mut border, theme_border)) = q_border.get_mut(insert.entity) {
156
border.set_all(theme.color(&theme_border.0));
157
}
158
}
159
160
/// An observer which looks for changes to the [`ThemeFontColor`] component on an entity, and
161
/// propagates downward the text color to all participating text entities.
162
pub(crate) fn on_changed_font_color(
163
insert: On<Insert, ThemeFontColor>,
164
font_color: Query<&ThemeFontColor>,
165
theme: Res<UiTheme>,
166
mut commands: Commands,
167
) {
168
if let Ok(token) = font_color.get(insert.entity) {
169
let color = theme.color(&token.0);
170
commands
171
.entity(insert.entity)
172
.insert(Propagate(TextColor(color)));
173
}
174
}
175
176