use bevy_app::{Propagate, PropagateOver};
use bevy_color::{palettes, Color};
use bevy_ecs::{
change_detection::DetectChanges,
component::Component,
lifecycle::Insert,
observer::On,
query::Changed,
reflect::{ReflectComponent, ReflectResource},
resource::Resource,
system::{Commands, Query, Res},
};
use bevy_log::warn_once;
use bevy_platform::collections::HashMap;
use bevy_reflect::{prelude::ReflectDefault, Reflect};
use bevy_text::TextColor;
use bevy_ui::{BackgroundColor, BorderColor};
use smol_str::SmolStr;
#[derive(Clone, PartialEq, Eq, Hash, Reflect)]
pub struct ThemeToken(SmolStr);
impl ThemeToken {
pub const fn new(text: SmolStr) -> Self {
Self(text)
}
pub const fn new_static(text: &'static str) -> Self {
Self(SmolStr::new_static(text))
}
}
impl core::fmt::Display for ThemeToken {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}", self.0)
}
}
impl core::fmt::Debug for ThemeToken {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "ThemeToken({:?})", self.0)
}
}
#[derive(Default, Clone, Reflect, Debug)]
#[reflect(Default, Debug)]
pub struct ThemeProps {
pub color: HashMap<ThemeToken, Color>,
}
#[derive(Resource, Default, Reflect, Debug)]
#[reflect(Resource, Default, Debug)]
pub struct UiTheme(pub ThemeProps);
impl UiTheme {
pub fn color(&self, token: &ThemeToken) -> Color {
let color = self.0.color.get(token);
match color {
Some(c) => *c,
None => {
warn_once!("Theme color {} not found.", token);
palettes::basic::FUCHSIA.into()
}
}
}
pub fn set_color(&mut self, token: &str, color: Color) {
self.0
.color
.insert(ThemeToken::new(SmolStr::new(token)), color);
}
}
#[derive(Component, Clone)]
#[require(BackgroundColor)]
#[component(immutable)]
#[derive(Reflect)]
#[reflect(Component, Clone)]
pub struct ThemeBackgroundColor(pub ThemeToken);
#[derive(Component, Clone)]
#[require(BorderColor)]
#[component(immutable)]
#[derive(Reflect)]
#[reflect(Component, Clone)]
pub struct ThemeBorderColor(pub ThemeToken);
#[derive(Component, Clone)]
#[component(immutable)]
#[derive(Reflect)]
#[reflect(Component, Clone)]
#[require(ThemedText, PropagateOver::<TextColor>::default())]
pub struct ThemeFontColor(pub ThemeToken);
#[derive(Component, Reflect, Default)]
#[reflect(Component)]
pub struct ThemedText;
pub(crate) fn update_theme(
mut q_background: Query<(&mut BackgroundColor, &ThemeBackgroundColor)>,
mut q_border: Query<(&mut BorderColor, &ThemeBorderColor)>,
theme: Res<UiTheme>,
) {
if theme.is_changed() {
for (mut bg, theme_bg) in q_background.iter_mut() {
bg.0 = theme.color(&theme_bg.0);
}
for (mut border, theme_border) in q_border.iter_mut() {
border.set_all(theme.color(&theme_border.0));
}
}
}
pub(crate) fn on_changed_background(
insert: On<Insert, ThemeBackgroundColor>,
mut q_background: Query<
(&mut BackgroundColor, &ThemeBackgroundColor),
Changed<ThemeBackgroundColor>,
>,
theme: Res<UiTheme>,
) {
if let Ok((mut bg, theme_bg)) = q_background.get_mut(insert.entity) {
bg.0 = theme.color(&theme_bg.0);
}
}
pub(crate) fn on_changed_border(
insert: On<Insert, ThemeBorderColor>,
mut q_border: Query<(&mut BorderColor, &ThemeBorderColor), Changed<ThemeBorderColor>>,
theme: Res<UiTheme>,
) {
if let Ok((mut border, theme_border)) = q_border.get_mut(insert.entity) {
border.set_all(theme.color(&theme_border.0));
}
}
pub(crate) fn on_changed_font_color(
insert: On<Insert, ThemeFontColor>,
font_color: Query<&ThemeFontColor>,
theme: Res<UiTheme>,
mut commands: Commands,
) {
if let Ok(token) = font_color.get(insert.entity) {
let color = theme.color(&token.0);
commands
.entity(insert.entity)
.insert(Propagate(TextColor(color)));
}
}