use bevy_app::{Propagate, PropagateOver};
use bevy_color::{palettes, Color};
use bevy_ecs::{
change_detection::DetectChanges,
component::Component,
entity::Entity,
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, Copy, PartialEq, Eq, Hash, Debug, Reflect, Default)]
#[reflect(Default, Debug)]
pub enum SurfaceLevel {
#[default]
Base,
Higher,
Highest,
Floating,
}
#[derive(Component, Clone, Copy, PartialEq, Eq, Debug, Default, Reflect)]
#[reflect(Default, Debug)]
pub struct ThemeContext(pub SurfaceLevel);
#[derive(Clone, PartialEq, Eq, Hash, Reflect, Default)]
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(Clone, PartialEq, Eq, Hash, Reflect, Default)]
pub struct SemanticToken(SmolStr);
impl SemanticToken {
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 SemanticToken {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}", self.0)
}
}
impl core::fmt::Debug for SemanticToken {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "SemanticToken({:?})", self.0)
}
}
#[derive(Default, Clone, Reflect, Debug)]
#[reflect(Default, Debug)]
pub struct ThemeProps {
pub token_assignments: HashMap<ThemeToken, SemanticToken>,
pub semantic_base: HashMap<SemanticToken, Color>,
pub semantic_overrides: HashMap<SurfaceLevel, HashMap<SemanticToken, Color>>,
}
impl ThemeProps {
pub fn new_non_contextual(init: HashMap<ThemeToken, Color>) -> Self {
let mut token_assignments = HashMap::new();
let mut semantic_base = HashMap::new();
for (theme_token, color) in init {
let semantic_token = SemanticToken::new(theme_token.0.clone());
token_assignments.insert(theme_token, semantic_token.clone());
semantic_base.insert(semantic_token, color);
}
Self {
token_assignments,
semantic_base,
semantic_overrides: HashMap::default(),
}
}
pub fn color(&self, token: &ThemeToken) -> Color {
self.context_color(token, SurfaceLevel::Base)
}
pub fn context_color(&self, token: &ThemeToken, context: SurfaceLevel) -> Color {
let Some(semantic_token) = self.token_assignments.get(token) else {
warn_once!("Theme color {} not found.", token);
return palettes::basic::FUCHSIA.into();
};
if let Some(color) = self
.semantic_overrides
.get(&context)
.and_then(|m| m.get(semantic_token))
{
return *color;
}
if let Some(color) = self.semantic_base.get(semantic_token) {
return *color;
}
warn_once!("Theme semantic color {:?} not found.", semantic_token);
palettes::basic::FUCHSIA.into()
}
}
#[derive(Resource, Default, Reflect, Debug)]
#[reflect(Resource, Default, Debug)]
pub struct UiTheme(pub ThemeProps);
impl UiTheme {
pub fn color(&self, token: &ThemeToken) -> Color {
self.0.color(token)
}
pub fn context_color(&self, token: &ThemeToken, context: SurfaceLevel) -> Color {
self.0.context_color(token, context)
}
}
#[derive(Component, Clone, Default)]
#[require(BackgroundColor)]
#[component(immutable)]
#[derive(Reflect)]
#[reflect(Component, Clone)]
pub struct ThemeBackgroundColor(pub ThemeToken);
#[derive(Component, Clone, Default)]
#[require(BorderColor)]
#[component(immutable)]
#[derive(Reflect)]
#[reflect(Component, Clone)]
pub struct ThemeBorderColor(pub ThemeToken);
#[derive(Component, Clone, Default)]
#[component(immutable)]
#[derive(Reflect)]
#[reflect(Component, Clone)]
#[require(ThemedText, PropagateOver::<TextColor>)]
pub struct InheritableThemeTextColor(pub ThemeToken);
#[derive(Component, Clone, Default)]
#[component(immutable)]
#[derive(Reflect)]
#[reflect(Component, Clone)]
#[require(ThemedText, PropagateOver::<TextColor>)]
pub struct ThemeTextColor(pub ThemeToken);
#[derive(Component, Reflect, Default, Clone)]
#[reflect(Component)]
pub struct ThemedText;
pub(crate) fn update_theme(
mut q_background: Query<(
&mut BackgroundColor,
&ThemeBackgroundColor,
Option<&ThemeContext>,
)>,
mut q_border: Query<(&mut BorderColor, &ThemeBorderColor, Option<&ThemeContext>)>,
mut q_text_color: Query<(&mut TextColor, &ThemeTextColor, Option<&ThemeContext>)>,
q_context_changed: Query<Entity, Changed<ThemeContext>>,
theme: Res<UiTheme>,
) {
if theme.is_changed() {
for (mut bg, ThemeBackgroundColor(token), ctx) in q_background.iter_mut() {
let context = ctx.map(|tc| tc.0).unwrap_or(SurfaceLevel::Base);
bg.0 = theme.context_color(token, context);
}
for (mut border, ThemeBorderColor(token), ctx) in q_border.iter_mut() {
let context = ctx.map(|tc| tc.0).unwrap_or(SurfaceLevel::Base);
border.set_all(theme.context_color(token, context));
}
for (mut text_color, ThemeTextColor(token), ctx) in q_text_color.iter_mut() {
let context = ctx.map(|tc| tc.0).unwrap_or(SurfaceLevel::Base);
text_color.0 = theme.context_color(token, context);
}
}
for ent in q_context_changed.iter() {
if let Ok((mut bg, ThemeBackgroundColor(token), ctx)) = q_background.get_mut(ent) {
let context = ctx.map(|tc| tc.0).unwrap_or(SurfaceLevel::Base);
bg.0 = theme.context_color(token, context);
}
if let Ok((mut border, ThemeBorderColor(token), ctx)) = q_border.get_mut(ent) {
let context = ctx.map(|tc| tc.0).unwrap_or(SurfaceLevel::Base);
border.set_all(theme.context_color(token, context));
}
if let Ok((mut text_color, ThemeTextColor(token), ctx)) = q_text_color.get_mut(ent) {
let context = ctx.map(|tc| tc.0).unwrap_or(SurfaceLevel::Base);
text_color.0 = theme.context_color(token, context);
}
}
}
pub(crate) fn on_changed_background(
insert: On<Insert<ThemeBackgroundColor>>,
mut q_background: Query<(
&mut BackgroundColor,
&ThemeBackgroundColor,
Option<&ThemeContext>,
)>,
theme: Res<UiTheme>,
) {
if let Ok((mut bg, ThemeBackgroundColor(token), theme_context)) =
q_background.get_mut(insert.entity)
{
let context = theme_context.map(|tc| tc.0).unwrap_or(SurfaceLevel::Base);
bg.0 = theme.context_color(token, context);
}
}
pub(crate) fn on_changed_border(
insert: On<Insert<ThemeBorderColor>>,
mut q_border: Query<(&mut BorderColor, &ThemeBorderColor, Option<&ThemeContext>)>,
theme: Res<UiTheme>,
) {
if let Ok((mut border, ThemeBorderColor(token), theme_context)) =
q_border.get_mut(insert.entity)
{
let context = theme_context.map(|tc| tc.0).unwrap_or(SurfaceLevel::Base);
border.set_all(theme.context_color(token, context));
}
}
pub(crate) fn on_changed_text_color(
insert: On<Insert<ThemeTextColor>>,
mut q_span: Query<(&mut TextColor, &ThemeTextColor, Option<&ThemeContext>)>,
theme: Res<UiTheme>,
) {
if let Ok((mut text_color, ThemeTextColor(token), theme_context)) =
q_span.get_mut(insert.entity)
{
let context = theme_context.map(|tc| tc.0).unwrap_or(SurfaceLevel::Base);
text_color.0 = theme.context_color(token, context);
}
}
pub(crate) fn on_changed_font_color(
insert: On<Insert<InheritableThemeTextColor>>,
font_color: Query<(&InheritableThemeTextColor, Option<&ThemeContext>)>,
theme: Res<UiTheme>,
mut commands: Commands,
) {
if let Ok((InheritableThemeTextColor(token), theme_context)) = font_color.get(insert.entity) {
let context = theme_context.map(|tc| tc.0).unwrap_or(SurfaceLevel::Base);
let color = theme.context_color(token, context);
commands
.entity(insert.entity)
.insert(Propagate(TextColor(color)));
}
}