use crate::prelude::*;
use crate::ui::theme;
use crate::ui::widgets::{
Button, Checkbox, ParagraphStyle, Placeholder, ProgressBar, Slider, Switch, TabBar, Text,
TextAlign, TextInput,
};
pub struct ThemeChoice(pub ThemeId);
pub const ACCENT: ColorToken = ColorToken::Tertiary;
pub fn dark_with_accent() -> Theme {
Theme::dark()
.with_info(ThemeInfo::new(
"midnight-amber",
"Midnight Amber",
"Dark surfaces with a warm accent",
))
.with(ACCENT, Color::rgb(255, 200, 60))
}
pub fn light_with_accent() -> Theme {
Theme::light()
.with_info(ThemeInfo::new(
"paper-rose",
"Paper Rose",
"Light surfaces with a rose accent",
))
.with(ACCENT, Color::rgb(220, 60, 90))
}
pub fn custom_theme() -> Theme {
Theme::dark()
.with_info(ThemeInfo::new(
"plum",
"Plum",
"Deep plum surfaces with cyan accents",
))
.with_many([
(ColorToken::Primary, Color::rgb(255, 105, 180)),
(ColorToken::OnPrimary, Color::rgb(20, 20, 30)),
(ColorToken::Success, Color::rgb(255, 200, 60)),
(ColorToken::Surface, Color::rgb(38, 28, 50)),
(ColorToken::SurfaceVariant, Color::rgb(70, 50, 90)),
(ColorToken::OnSurface, Color::rgb(245, 235, 255)),
(ColorToken::OnSurfaceVariant, Color::rgb(180, 150, 200)),
(ACCENT, Color::rgb(140, 200, 220)),
])
}
#[compose]
pub fn build_widgets() {
ui! {
Column (
grow: 1.0,
align: AlignItems::Center,
padding: Padding::all(16),
row_gap: 10,
bg_color: ColorToken::Surface
) {
Text (
"LIVE THEME TOKENS",
width: Dimension::percent(100),
max_width: 480,
height: 24,
font_size: 16,
text_color: ColorToken::OnSurface
)
Row (
width: Dimension::percent(100),
max_width: 480,
height: 36,
column_gap: 8
) {
Button (
grow: 1.0,
height: 36,
border_radius: 10,
text_color: ColorToken::OnPrimary,
normal_color: Color::rgb(40, 50, 70),
pressed_color: Color::rgb(20, 25, 35)
) [
ThemeChoice(ThemeId::new("midnight-amber")),
Text::label("Dark"),
] on Tap {
if let Some(id) = ctx.world.get::<ThemeChoice>(ctx.entity).map(|choice| choice.0) {
let _ = theme::set_theme(ctx.world, id);
}
}
Button (
grow: 1.0,
height: 36,
border_radius: 10,
text_color: ColorToken::OnPrimary,
normal_color: Color::rgb(0, 100, 200),
pressed_color: Color::rgb(0, 70, 150)
) [
ThemeChoice(ThemeId::new("paper-rose")),
Text::label("Light"),
] on Tap {
if let Some(id) = ctx.world.get::<ThemeChoice>(ctx.entity).map(|choice| choice.0) {
let _ = theme::set_theme(ctx.world, id);
}
}
Button (
id: "theme_custom",
grow: 1.0,
height: 36,
border_radius: 10,
text_color: ColorToken::OnPrimary,
normal_color: Color::rgb(255, 105, 180),
pressed_color: Color::rgb(200, 70, 140)
) [
ThemeChoice(ThemeId::new("plum")),
Text::label("Custom"),
] on Tap {
if let Some(id) = ctx.world.get::<ThemeChoice>(ctx.entity).map(|choice| choice.0) {
let _ = theme::set_theme(ctx.world, id);
}
}
}
Row (
id: "theme_token_grid",
width: Dimension::percent(100),
max_width: 480,
grow: 1.0,
max_height: 340,
padding: Padding::all(10),
wrap: FlexWrap::Wrap,
align: AlignItems::FlexStart,
row_gap: 6,
column_gap: 8,
bg_color: ColorToken::SurfaceVariant,
border_radius: 14
) {
Row (
width: 200,
grow: 1.0,
height: 38,
align: AlignItems::Center,
column_gap: 6
) {
Text (
"Slider",
width: 48,
height: 38,
paragraph: ParagraphStyle::label().with_align(TextAlign::Start)
)
Slider (
min: Fixed::ZERO,
max: Fixed::from_int(100),
grow: 1.0,
height: 20
)
}
Row (
width: 200,
grow: 1.0,
height: 38,
align: AlignItems::Center,
column_gap: 6
) {
Text (
"Switch",
width: 48,
height: 38,
paragraph: ParagraphStyle::label().with_align(TextAlign::Start)
)
Switch (
width: 44,
height: 22,
off_color: ColorToken::Outline
)
}
Row (
width: 200,
grow: 1.0,
height: 38,
align: AlignItems::Center,
column_gap: 6
) {
Text (
"Checkbox",
width: 68,
height: 38,
paragraph: ParagraphStyle::label().with_align(TextAlign::Start)
)
Checkbox (
width: 20,
height: 20,
border_radius: 4,
border_color: ColorToken::Outline,
border_width: 1
)
}
Row (
width: 200,
grow: 1.0,
height: 38,
align: AlignItems::Center,
column_gap: 6
) {
Text (
"Progress",
width: 68,
height: 38,
paragraph: ParagraphStyle::label().with_align(TextAlign::Start)
)
ProgressBar (grow: 1.0, height: 10, border_radius: 5, value: 0.6)
}
Row (
width: 200,
grow: 1.0,
height: 38,
align: AlignItems::Center,
column_gap: 6
) {
Text (
"Input",
width: 44,
height: 38,
paragraph: ParagraphStyle::label().with_align(TextAlign::Start)
)
TextInput (
grow: 1.0,
height: 28,
border_radius: 8,
bg_color: ColorToken::Surface,
border_color: ColorToken::Outline,
border_width: 1
) [
Placeholder("Theme-aware input"),
]
}
Row (
width: 200,
grow: 1.0,
height: 38,
align: AlignItems::Center,
column_gap: 6
) {
Text (
"Tabs",
width: 32,
height: 38,
paragraph: ParagraphStyle::label().with_align(TextAlign::Start)
)
TabBar (count: 3, grow: 1.0, height: 24) {
Text ("A", grow: 1.0, paragraph: ParagraphStyle::label())
Text ("B", grow: 1.0, paragraph: ParagraphStyle::label())
Text ("C", grow: 1.0, paragraph: ParagraphStyle::label())
}
}
Row (
id: "theme_accent_row",
width: 200,
grow: 1.0,
height: 38,
align: AlignItems::Center,
column_gap: 6
) {
Text (
"Accent",
width: 48,
height: 38,
paragraph: ParagraphStyle::label().with_align(TextAlign::Start)
)
View (grow: 1.0, height: 22, border_radius: 6, bg_color: ACCENT)
}
}
}
};
}
#[cfg(feature = "std")]
pub fn setup_app<B, F>(app: &mut App<B, F>, parent: Entity)
where
B: Surface,
F: RendererFactory<B>,
{
app.register_theme(dark_with_accent())
.register_theme(light_with_accent())
.register_theme(custom_theme());
app.set_theme("midnight-amber").expect("registered theme");
app.compose(parent, build_widgets);
}
pub const DEMO_SIZE: crate::gallery::DemoSize = crate::gallery::DemoSize::at_most(480, 320);
#[cfg(test)]
mod tests {
use super::*;
use crate::ui::IdMap;
use crate::ui::UiScope;
use crate::input::event::GestureHandler;
use crate::input::event::gesture::GestureEvent;
#[test]
fn build_widgets_smoke() {
let mut world = World::new();
world.insert_resource(IdMap::new());
theme::set_theme(&mut world, dark_with_accent()).unwrap();
theme::register(&mut world, light_with_accent());
theme::register(&mut world, custom_theme());
let parent = WidgetBuilder::new(&mut world).id();
let mut cx = UiScope::new(&mut world, parent);
build_widgets(&mut cx);
drop(cx);
assert!(
world
.get::<crate::ui::Children>(parent)
.is_some_and(|c| !c.0.is_empty())
);
}
#[test]
fn tap_button_swaps_global_theme() {
let mut world = World::new();
world.insert_resource(IdMap::new());
theme::set_theme(&mut world, dark_with_accent()).unwrap();
theme::register(&mut world, light_with_accent());
theme::register(&mut world, custom_theme());
let parent = WidgetBuilder::new(&mut world).id();
let mut cx = UiScope::new(&mut world, parent);
build_widgets(&mut cx);
drop(cx);
let custom_btn = world.find_by_id("theme_custom").unwrap();
GestureHandler::trigger(
&mut world,
custom_btn,
&GestureEvent::Tap {
x: Fixed::ZERO,
y: Fixed::ZERO,
target: custom_btn,
},
);
let theme = world.resource::<Theme>().unwrap();
assert_eq!(theme.resolve(ACCENT), Color::rgb(140, 200, 220));
}
#[test]
fn token_grid_stays_inside_landscape_and_portrait_viewports() {
use crate::types::Viewport;
use crate::ui::ComputedRect;
use crate::ui::render_system::update_layout;
for (width, height) in [(480, 320), (320, 480)] {
let mut app = App::headless(width, height);
app.with_default_widgets()
.with_default_systems()
.with_theme(dark_with_accent());
let root = app.spawn_root().id();
app.compose(root, build_widgets);
app.set_root(root);
update_layout(
&mut app.world,
root,
&Viewport::new(width, height, Fixed::ONE),
);
let grid = app.world.find_by_id("theme_token_grid").unwrap();
let accent = app.world.find_by_id("theme_accent_row").unwrap();
let grid = app.world.get::<ComputedRect>(grid).unwrap().0;
let accent = app.world.get::<ComputedRect>(accent).unwrap().0;
assert!(grid.x >= Fixed::ZERO);
assert!(grid.y >= Fixed::ZERO);
assert!(grid.x + grid.w <= Fixed::from_int(width as i32));
assert!(grid.y + grid.h <= Fixed::from_int(height as i32));
assert!(accent.x >= grid.x);
assert!(accent.y >= grid.y);
assert!(accent.x + accent.w <= grid.x + grid.w);
assert!(accent.y + accent.h <= grid.y + grid.h);
}
}
}