#[cfg(all(not(alloc_frugal), widgets_unstripped))]
use crate::style::WidgetStyle;
#[cfg(all(not(alloc_frugal), widgets_unstripped))]
use crate::widget::Widget;
#[cfg(all(not(alloc_frugal), widgets_unstripped))]
pub(crate) fn apply_active_theme(widget: &mut dyn Widget) {
let Some(kind_name) = crate::widget::capability::factory_name_for_kind(widget.kind()) else {
log::debug!(
"theme: {:?} has no resolvable factory name; leaving its own style in place",
widget.kind()
);
return;
};
let state = widget.widget_state();
let Some(theme_style) = crate::theme::resolved_theme_style_for_state(kind_name, state) else {
return;
};
let mut style = widget.style().clone();
let caller_authored = !style.theme_derived && style != WidgetStyle::default();
refresh_state_style(&mut style, &theme_style, caller_authored);
widget.set_style(style);
}
#[cfg(all(not(alloc_frugal), widgets_unstripped))]
pub fn reapply_active_theme_state(widget: &mut dyn crate::widget::Widget) {
apply_active_theme(widget);
}
#[cfg(any(alloc_frugal, not(widgets_unstripped)))]
pub fn reapply_active_theme_state(_widget: &mut dyn crate::widget::Widget) {}
#[cfg(all(not(alloc_frugal), widgets_unstripped))]
fn refresh_state_style(style: &mut WidgetStyle, theme_style: &WidgetStyle, caller_authored: bool) {
style.merge_theme(theme_style);
if !caller_authored {
style.theme_derived = true;
}
}
#[cfg(all(device_profile, not(widgets_unstripped)))]
pub(crate) fn apply_active_theme(_widget: &mut dyn crate::widget::Widget) {}
#[cfg(alloc_frugal)]
pub(crate) fn apply_active_theme(_widget: &mut dyn crate::widget::Widget) {}
#[cfg(all(test, not(alloc_frugal)))]
mod tests {
use super::*;
use crate::core::{Color, Rect};
use crate::theme::{self, global_theme_manager, AppearanceMode};
fn guard() -> std::sync::MutexGuard<'static, ()> {
theme::theme_test_guard()
}
#[test]
fn a_state_override_reaches_the_control_itself() {
let _guard = guard();
let mut manager = global_theme_manager();
let mut theme = crate::theme::Theme::default();
theme.overrides.styles.insert(
"button:disabled".to_string(),
crate::theme::ThemeStyleToken {
background: Some(Color::rgba(7, 8, 9, 255)),
..Default::default()
},
);
manager.register_theme(theme);
assert!(manager.set_theme("default"));
drop(manager);
let mut button = crate::widget::Button::new("ok".to_string(), Rect::new(0, 0, 10, 10));
button.set_enabled(false);
apply_active_theme(&mut button);
assert_eq!(
button.style().background_color,
Some(Color::rgba(7, 8, 9, 255)),
"the `button:disabled` override must reach the control, which is the state it reports"
);
let mut enabled = crate::widget::Button::new("ok".to_string(), Rect::new(0, 0, 10, 10));
apply_active_theme(&mut enabled);
assert_ne!(
enabled.style().background_color,
Some(Color::rgba(7, 8, 9, 255)),
"an enabled button is not in the disabled state, so that override must not apply"
);
}
#[test]
fn a_widget_with_no_style_gets_the_theme() {
let _guard = guard();
let mut manager = global_theme_manager();
assert!(manager.set_appearance(AppearanceMode::Light));
drop(manager);
let primary = global_theme_manager().current_theme().expect("active").colors.primary;
let mut button = crate::widget::Button::new("ok".to_string(), Rect::new(0, 0, 10, 10));
assert_eq!(button.style().background_color, None, "before: unset");
apply_active_theme(&mut button);
assert_eq!(
button.style().background_color,
Some(primary),
"a button's fill must come from the theme's primary token"
);
}
#[test]
fn an_explicit_style_is_not_overwritten() {
let _guard = guard();
let explicit = Color::rgb(1, 2, 3);
let mut button = crate::widget::Button::new("ok".to_string(), Rect::new(0, 0, 10, 10));
button.set_background_color(Some(explicit));
apply_active_theme(&mut button);
assert_eq!(
button.style().background_color,
Some(explicit),
"the theme must not replace a colour the caller set"
);
}
#[test]
fn switching_appearance_changes_what_a_new_control_gets() {
let _guard = guard();
{
let mut manager = global_theme_manager();
assert!(manager.set_appearance(AppearanceMode::Light));
}
let mut light = crate::widget::Button::new("ok".to_string(), Rect::new(0, 0, 10, 10));
apply_active_theme(&mut light);
{
let mut manager = global_theme_manager();
assert!(manager.set_appearance(AppearanceMode::Dark));
}
let mut dark = crate::widget::Button::new("ok".to_string(), Rect::new(0, 0, 10, 10));
apply_active_theme(&mut dark);
assert_ne!(
light.style().background_color,
dark.style().background_color,
"a light and a dark theme must fill a button differently"
);
global_theme_manager().set_appearance(AppearanceMode::Light);
}
#[test]
fn reapplying_after_a_switch_replaces_the_previous_themes_colours() {
let _guard = guard();
global_theme_manager().set_appearance(AppearanceMode::Light);
let mut button = crate::widget::Button::new("ok".to_string(), Rect::new(0, 0, 10, 10));
apply_active_theme(&mut button);
let light_fill = button.style().background_color;
assert!(light_fill.is_some(), "the first application must style the button");
global_theme_manager().set_appearance(AppearanceMode::Dark);
apply_active_theme(&mut button);
let dark_fill = button.style().background_color;
assert_ne!(
light_fill, dark_fill,
"re-applying after a switch must replace the previous theme's colour, not be
silently refused because the field is already set"
);
global_theme_manager().set_appearance(AppearanceMode::Light);
}
#[test]
fn a_callers_colour_survives_repeated_theme_applications() {
let _guard = guard();
let explicit = Color::rgb(1, 2, 3);
global_theme_manager().set_appearance(AppearanceMode::Light);
let mut button = crate::widget::Button::new("ok".to_string(), Rect::new(0, 0, 10, 10));
button.set_background_color(Some(explicit));
apply_active_theme(&mut button);
global_theme_manager().set_appearance(AppearanceMode::Dark);
apply_active_theme(&mut button);
assert_eq!(
button.style().background_color,
Some(explicit),
"a colour the caller set must survive a theme switch"
);
global_theme_manager().set_appearance(AppearanceMode::Light);
}
#[test]
fn a_high_contrast_override_reaches_a_new_control() {
use crate::style::HighContrastMode;
let _guard = guard();
global_theme_manager().set_high_contrast(HighContrastMode::WhiteOnBlack);
let mut button = crate::widget::Button::new("ok".to_string(), Rect::new(0, 0, 10, 10));
apply_active_theme(&mut button);
assert_eq!(button.style().background_color, Some(Color::BLACK));
assert_eq!(button.style().text_color, Some(Color::WHITE));
global_theme_manager().set_high_contrast(HighContrastMode::None);
}
#[test]
fn different_kinds_get_different_treatments() {
let _guard = guard();
{
let mut manager = global_theme_manager();
assert!(manager.set_appearance(AppearanceMode::Light));
}
let mut button = crate::widget::Button::new("ok".to_string(), Rect::new(0, 0, 10, 10));
let mut label = crate::widget::Label::new("hi".to_string(), Rect::new(0, 0, 10, 10));
apply_active_theme(&mut button);
apply_active_theme(&mut label);
assert_ne!(
button.style().background_color,
label.style().background_color,
"a filled action and a plain label must not share a background"
);
}
#[test]
fn a_hovered_button_is_filled_differently_from_a_resting_one() {
let _guard = guard();
let mut manager = global_theme_manager();
assert!(manager.set_theme("default"), "the default preset must be registered");
drop(manager);
let mut resting = crate::widget::Button::new("ok".to_string(), Rect::new(0, 0, 10, 10));
apply_active_theme(&mut resting);
let mut hovered = crate::widget::Button::new("ok".to_string(), Rect::new(0, 0, 10, 10));
crate::event::EventHandler::handle_event(
&mut hovered,
&crate::event::Event::MouseEnter { pos: crate::core::Point::new(5, 5) },
);
assert_eq!(
hovered.widget_state(),
crate::style::WidgetState::Hover,
"the control must actually report the state the override is keyed on"
);
apply_active_theme(&mut hovered);
assert_ne!(
hovered.style().background_color,
resting.style().background_color,
"`button:hover` in the preset must change the painted fill"
);
}
#[test]
fn a_latch_that_changes_after_the_theme_was_applied_still_repaints() {
use crate::widget::ToggleButton;
let _guard = guard();
let mut manager = global_theme_manager();
assert!(manager.set_theme("default"), "the default preset must be registered");
drop(manager);
let bounds = Rect::new(0, 0, 120, 40);
let mut off = ToggleButton::new("On".to_string(), bounds);
apply_active_theme(&mut off);
let resting = crate::widget::svg::render_widget_to_svg(&mut off, bounds);
let mut on = ToggleButton::new("On".to_string(), bounds);
apply_active_theme(&mut on);
on.set_checked(true);
assert_eq!(on.widget_state(), crate::style::WidgetState::Checked);
let latched = crate::widget::svg::render_widget_to_svg(&mut on, bounds);
assert_ne!(
resting, latched,
"a latch applied after theming must change the painted fill, not only the model"
);
}
#[test]
fn the_other_state_reporting_controls_re_resolve_after_themming() {
use crate::widget::special_widgets::chip::{Chip, ChipItem};
use crate::widget::{CheckBox, RadioButton, Switch};
let _guard = guard();
let mut manager = global_theme_manager();
assert!(manager.set_theme("default"), "the default preset must be registered");
drop(manager);
let bounds = Rect::new(0, 0, 120, 40);
let mut check_box = CheckBox::new(bounds);
check_box.set_checked(false);
apply_active_theme(&mut check_box);
let unchecked = check_box.style().background_color;
check_box.set_checked(true);
assert_ne!(
unchecked,
check_box.style().background_color,
"`check_box:checked` must reach a box that latched after it was themed"
);
let mut radio = RadioButton::new(bounds);
apply_active_theme(&mut radio);
let unselected = radio.style().background_color;
radio.set_checked(true);
assert_ne!(
unselected,
radio.style().background_color,
"`radio_button:checked` must reach a radio selected after it was themed"
);
let mut switch = Switch::new(bounds);
apply_active_theme(&mut switch);
let off = switch.style().background_color;
switch.set_checked(true);
assert_ne!(
off,
switch.style().background_color,
"`switch:checked` must reach a switch that latched after it was themed"
);
let mut chip = Chip::new(bounds);
chip.set_items(vec![ChipItem::new("a", "A"), ChipItem::new("b", "B")]);
apply_active_theme(&mut chip);
assert_eq!(chip.widget_state(), crate::style::WidgetState::Normal);
chip.toggle_index(0);
assert_eq!(chip.widget_state(), crate::style::WidgetState::Selected);
assert_eq!(
chip.style().background_color,
crate::theme::resolved_theme_style_for_state(
"chip",
crate::style::WidgetState::Selected
)
.and_then(|resolved| resolved.background_color),
"a chip selected after theming must resolve `chip:selected`, not the unselected `chip`"
);
}
#[test]
fn a_state_change_after_theming_re_resolves_the_state_key() {
use crate::widget::{Button, ToolButton, Widget};
let _guard = guard();
let mut manager = global_theme_manager();
assert!(manager.set_theme("default"), "the default preset must be registered");
drop(manager);
let bounds = Rect::new(0, 0, 120, 40);
let mut button = Button::new("ok".to_string(), bounds);
apply_active_theme(&mut button);
let enabled_fill = button.style().background_color;
button.set_enabled(false);
assert_eq!(
button.style().background_color,
crate::theme::resolved_theme_style_for_state(
"button",
crate::style::WidgetState::Disabled
)
.and_then(|resolved| resolved.background_color)
.or(enabled_fill),
"disabling a themed button must re-resolve `button:disabled`"
);
let mut tool = ToolButton::new("go".to_string(), bounds);
apply_active_theme(&mut tool);
let tool_enabled = tool.style().background_color;
tool.set_enabled(false);
assert_ne!(
tool.style().background_color,
tool_enabled,
"`tool_button:disabled` must reach a tool button disabled after theming"
);
let mut hovered = Button::new("ok".to_string(), bounds);
apply_active_theme(&mut hovered);
let resting = hovered.style().background_color;
hovered.set_hovered(true);
assert_eq!(
hovered.style().background_color,
crate::theme::resolved_theme_style_for_state(
"button",
crate::style::WidgetState::Hover
)
.and_then(|resolved| resolved.background_color)
.or(resting),
"hovering a themed button must re-resolve `button:hover`"
);
hovered.set_pressed(true);
assert_eq!(
hovered.style().background_color,
crate::theme::resolved_theme_style_for_state(
"button",
crate::style::WidgetState::Pressed
)
.and_then(|resolved| resolved.background_color)
.or(resting),
"pressing a themed button must re-resolve `button:pressed`"
);
}
#[test]
fn the_themes_spacing_tokens_reach_a_controls_padding_and_margin() {
let _guard = guard();
let mut manager = global_theme_manager();
assert!(manager.set_theme("default"), "the default preset must be registered");
let spacing = manager.current_theme().expect("the default theme is active").spacing.medium;
let small = manager.current_theme().expect("the default theme is active").spacing.small;
drop(manager);
let mut button = crate::widget::Button::new("ok".to_string(), Rect::new(0, 0, 120, 40));
assert_eq!(
button.style().padding,
crate::style::Padding::default(),
"the control must start with no padding, so the assertion below is not vacuous"
);
apply_active_theme(&mut button);
assert_eq!(
button.style().padding,
crate::style::Padding::all(spacing),
"`theme.spacing.medium` must reach the control's padding"
);
assert_eq!(
button.style().margin,
crate::style::Margin::all(small),
"`theme.spacing.small` must reach the control's margin"
);
assert_eq!(
*crate::widget::Widget::padding(&button),
crate::style::Padding::all(spacing),
"`Widget::padding()` must agree with the style it reads"
);
}
}