use nalgebra_glm::{Vec2, Vec4};
use crate::ecs::ui::components::UiNodeShadow;
#[derive(Copy, Clone, Debug, Default)]
pub enum RectEffect {
#[default]
Solid,
VerticalGradient {
secondary: Vec4,
},
HorizontalGradient {
secondary: Vec4,
},
RadialGlow {
glow_color: Vec4,
intensity: f32,
},
FrostedGlass {
tint: Vec4,
blur_amount: f32,
},
InsetEmboss {
strength: f32,
edge_softness: f32,
},
OutsetEmboss {
strength: f32,
edge_softness: f32,
},
ColorWheel {
value: f32,
alpha: f32,
},
}
impl RectEffect {
pub fn encode(&self) -> (u32, [f32; 4]) {
match *self {
Self::Solid => (0, [0.0; 4]),
Self::VerticalGradient { secondary } => {
(1, [secondary.x, secondary.y, secondary.z, secondary.w])
}
Self::HorizontalGradient { secondary } => {
(2, [secondary.x, secondary.y, secondary.z, secondary.w])
}
Self::RadialGlow {
glow_color,
intensity,
} => (3, [glow_color.x, glow_color.y, glow_color.z, intensity]),
Self::InsetEmboss {
strength,
edge_softness,
} => (4, [strength, edge_softness, 0.0, 0.0]),
Self::OutsetEmboss {
strength,
edge_softness,
} => (5, [strength, edge_softness, 0.0, 0.0]),
Self::FrostedGlass { tint, blur_amount } => (6, [tint.x, tint.y, tint.z, blur_amount]),
Self::ColorWheel { value, alpha } => (7, [value, alpha, 0.0, 0.0]),
}
}
}
fn default_panel_shadow() -> UiNodeShadow {
UiNodeShadow {
color: Vec4::new(0.0, 0.0, 0.0, 0.25),
offset: Vec2::new(0.0, 2.0),
blur: 8.0,
spread: 0.0,
}
}
fn default_button_shadow() -> UiNodeShadow {
UiNodeShadow {
color: Vec4::new(0.0, 0.0, 0.0, 0.18),
offset: Vec2::new(0.0, 1.0),
blur: 3.0,
spread: 0.0,
}
}
fn default_button_shadow_hover() -> UiNodeShadow {
UiNodeShadow {
color: Vec4::new(0.0, 0.0, 0.0, 0.28),
offset: Vec2::new(0.0, 3.0),
blur: 8.0,
spread: 0.0,
}
}
fn default_button_shadow_pressed() -> UiNodeShadow {
UiNodeShadow {
color: Vec4::new(0.0, 0.0, 0.0, 0.35),
offset: Vec2::new(0.0, 1.0),
blur: 2.0,
spread: 0.0,
}
}
fn default_modal_shadow() -> UiNodeShadow {
UiNodeShadow {
color: Vec4::new(0.0, 0.0, 0.0, 0.5),
offset: Vec2::new(0.0, 8.0),
blur: 32.0,
spread: 4.0,
}
}
#[derive(Clone, Debug)]
pub struct UiTheme {
pub name: String,
pub dark_mode: bool,
pub text_color: Vec4,
pub text_color_disabled: Vec4,
pub text_color_accent: Vec4,
pub background_color: Vec4,
pub background_color_hovered: Vec4,
pub background_color_active: Vec4,
pub panel_color: Vec4,
pub panel_header_color: Vec4,
pub border_color: Vec4,
pub border_color_focused: Vec4,
pub accent_color: Vec4,
pub accent_color_hovered: Vec4,
pub accent_color_active: Vec4,
pub success_color: Vec4,
pub warning_color: Vec4,
pub error_color: Vec4,
pub slider_track_color: Vec4,
pub slider_fill_color: Vec4,
pub input_background_color: Vec4,
pub input_background_focused: Vec4,
pub selection_color: Vec4,
pub scrollbar_color: Vec4,
pub scrollbar_color_hovered: Vec4,
pub border_width: f32,
pub corner_radius: f32,
pub padding: f32,
pub spacing: f32,
pub font_size: f32,
pub button_height: f32,
pub slider_height: f32,
pub checkbox_size: f32,
pub toggle_width: f32,
pub toggle_height: f32,
pub panel_header_height: f32,
pub heading_font_size: f32,
pub body_font_size: f32,
pub caption_font_size: f32,
pub monospace_font_size: f32,
pub panel_shadow: UiNodeShadow,
pub button_shadow: UiNodeShadow,
pub button_shadow_hover: UiNodeShadow,
pub button_shadow_pressed: UiNodeShadow,
pub modal_shadow: UiNodeShadow,
pub focus_glow: Option<UiNodeShadow>,
pub panel_effect: RectEffect,
pub button_effect: RectEffect,
pub input_effect: RectEffect,
pub transition_speed_enter: f32,
pub transition_speed_exit: f32,
pub use_spring: bool,
}
impl Default for UiTheme {
fn default() -> Self {
Self::dark()
}
}
#[derive(Clone, Copy, Debug)]
pub struct ThemePalette {
pub name: &'static str,
pub dark_mode: bool,
pub text_color: Vec4,
pub text_color_disabled: Vec4,
pub text_color_accent: Vec4,
pub background_color: Vec4,
pub background_color_hovered: Vec4,
pub background_color_active: Vec4,
pub panel_color: Vec4,
pub panel_header_color: Vec4,
pub border_color: Vec4,
pub border_color_focused: Vec4,
pub accent_color: Vec4,
pub accent_color_hovered: Vec4,
pub accent_color_active: Vec4,
pub success_color: Vec4,
pub warning_color: Vec4,
pub error_color: Vec4,
pub slider_track_color: Vec4,
pub slider_fill_color: Vec4,
pub input_background_color: Vec4,
pub input_background_focused: Vec4,
pub selection_color: Vec4,
pub scrollbar_color: Vec4,
pub scrollbar_color_hovered: Vec4,
}
fn nudge_brightness(color: Vec4, amount: f32) -> Vec4 {
Vec4::new(
(color.x + amount).clamp(0.0, 1.0),
(color.y + amount).clamp(0.0, 1.0),
(color.z + amount).clamp(0.0, 1.0),
color.w,
)
}
impl UiTheme {
pub fn from_palette(palette: ThemePalette) -> Self {
let panel_secondary = nudge_brightness(palette.panel_color, -0.04);
let button_secondary = nudge_brightness(palette.background_color, -0.04);
let focus_color = Vec4::new(
palette.border_color_focused.x,
palette.border_color_focused.y,
palette.border_color_focused.z,
0.7,
);
let focus_glow = Some(UiNodeShadow {
color: focus_color,
offset: Vec2::new(0.0, 0.0),
blur: 4.0,
spread: 1.5,
});
Self {
name: palette.name.to_string(),
dark_mode: palette.dark_mode,
text_color: palette.text_color,
text_color_disabled: palette.text_color_disabled,
text_color_accent: palette.text_color_accent,
background_color: palette.background_color,
background_color_hovered: palette.background_color_hovered,
background_color_active: palette.background_color_active,
panel_color: palette.panel_color,
panel_header_color: palette.panel_header_color,
border_color: palette.border_color,
border_color_focused: palette.border_color_focused,
accent_color: palette.accent_color,
accent_color_hovered: palette.accent_color_hovered,
accent_color_active: palette.accent_color_active,
success_color: palette.success_color,
warning_color: palette.warning_color,
error_color: palette.error_color,
slider_track_color: palette.slider_track_color,
slider_fill_color: palette.slider_fill_color,
input_background_color: palette.input_background_color,
input_background_focused: palette.input_background_focused,
selection_color: palette.selection_color,
scrollbar_color: palette.scrollbar_color,
scrollbar_color_hovered: palette.scrollbar_color_hovered,
border_width: 1.0,
corner_radius: 4.0,
padding: 8.0,
spacing: 6.0,
font_size: 18.0,
button_height: 32.0,
slider_height: 24.0,
checkbox_size: 20.0,
toggle_width: 40.0,
toggle_height: 22.0,
panel_header_height: 28.0,
heading_font_size: 22.0,
body_font_size: 16.0,
caption_font_size: 12.0,
monospace_font_size: 14.0,
panel_shadow: default_panel_shadow(),
button_shadow: default_button_shadow(),
button_shadow_hover: default_button_shadow_hover(),
button_shadow_pressed: default_button_shadow_pressed(),
modal_shadow: default_modal_shadow(),
focus_glow,
panel_effect: RectEffect::VerticalGradient {
secondary: panel_secondary,
},
button_effect: RectEffect::VerticalGradient {
secondary: button_secondary,
},
input_effect: RectEffect::Solid,
transition_speed_enter: 8.0,
transition_speed_exit: 6.0,
use_spring: false,
}
}
}
impl UiTheme {
pub fn dark() -> Self {
Self::from_palette(ThemePalette {
name: "Dark",
dark_mode: true,
text_color: Vec4::new(1.0, 1.0, 1.0, 1.0),
text_color_disabled: Vec4::new(0.5, 0.5, 0.5, 1.0),
text_color_accent: Vec4::new(0.4, 0.8, 1.0, 1.0),
background_color: Vec4::new(0.15, 0.15, 0.15, 1.0),
background_color_hovered: Vec4::new(0.25, 0.25, 0.25, 1.0),
background_color_active: Vec4::new(0.35, 0.35, 0.35, 1.0),
panel_color: Vec4::new(0.12, 0.12, 0.12, 1.0),
panel_header_color: Vec4::new(0.18, 0.18, 0.18, 1.0),
border_color: Vec4::new(0.4, 0.4, 0.4, 1.0),
border_color_focused: Vec4::new(0.5, 0.7, 1.0, 1.0),
accent_color: Vec4::new(0.3, 0.5, 0.9, 1.0),
accent_color_hovered: Vec4::new(0.4, 0.6, 1.0, 1.0),
accent_color_active: Vec4::new(0.2, 0.4, 0.8, 1.0),
success_color: Vec4::new(0.3, 0.8, 0.4, 1.0),
warning_color: Vec4::new(1.0, 0.7, 0.2, 1.0),
error_color: Vec4::new(0.9, 0.3, 0.3, 1.0),
slider_track_color: Vec4::new(0.25, 0.25, 0.25, 1.0),
slider_fill_color: Vec4::new(0.3, 0.5, 0.9, 1.0),
input_background_color: Vec4::new(0.1, 0.1, 0.1, 1.0),
input_background_focused: Vec4::new(0.12, 0.12, 0.15, 1.0),
selection_color: Vec4::new(0.3, 0.5, 0.9, 0.4),
scrollbar_color: Vec4::new(0.3, 0.3, 0.3, 0.6),
scrollbar_color_hovered: Vec4::new(0.4, 0.4, 0.4, 0.8),
})
}
pub fn dracula() -> Self {
Self::from_palette(ThemePalette {
name: "Dracula",
dark_mode: true,
text_color: Vec4::new(0.973, 0.973, 0.949, 1.0),
text_color_disabled: Vec4::new(0.384, 0.447, 0.643, 1.0),
text_color_accent: Vec4::new(0.545, 0.914, 0.992, 1.0),
background_color: Vec4::new(0.157, 0.165, 0.212, 1.0),
background_color_hovered: Vec4::new(0.267, 0.278, 0.353, 1.0),
background_color_active: Vec4::new(0.337, 0.349, 0.424, 1.0),
panel_color: Vec4::new(0.157, 0.165, 0.212, 1.0),
panel_header_color: Vec4::new(0.267, 0.278, 0.353, 1.0),
border_color: Vec4::new(0.384, 0.447, 0.643, 1.0),
border_color_focused: Vec4::new(0.741, 0.576, 0.976, 1.0),
accent_color: Vec4::new(0.741, 0.576, 0.976, 1.0),
accent_color_hovered: Vec4::new(0.827, 0.686, 1.0, 1.0),
accent_color_active: Vec4::new(0.639, 0.475, 0.855, 1.0),
success_color: Vec4::new(0.314, 0.980, 0.482, 1.0),
warning_color: Vec4::new(1.0, 0.722, 0.424, 1.0),
error_color: Vec4::new(1.0, 0.333, 0.333, 1.0),
slider_track_color: Vec4::new(0.267, 0.278, 0.353, 1.0),
slider_fill_color: Vec4::new(0.741, 0.576, 0.976, 1.0),
input_background_color: Vec4::new(0.118, 0.125, 0.165, 1.0),
input_background_focused: Vec4::new(0.157, 0.165, 0.212, 1.0),
selection_color: Vec4::new(0.384, 0.447, 0.643, 0.5),
scrollbar_color: Vec4::new(0.384, 0.447, 0.643, 0.4),
scrollbar_color_hovered: Vec4::new(0.384, 0.447, 0.643, 0.7),
})
}
pub fn nord() -> Self {
Self::from_palette(ThemePalette {
name: "Nord",
dark_mode: true,
text_color: Vec4::new(0.847, 0.871, 0.914, 1.0),
text_color_disabled: Vec4::new(0.369, 0.420, 0.506, 1.0),
text_color_accent: Vec4::new(0.533, 0.753, 0.816, 1.0),
background_color: Vec4::new(0.180, 0.204, 0.251, 1.0),
background_color_hovered: Vec4::new(0.231, 0.259, 0.322, 1.0),
background_color_active: Vec4::new(0.263, 0.298, 0.369, 1.0),
panel_color: Vec4::new(0.180, 0.204, 0.251, 1.0),
panel_header_color: Vec4::new(0.231, 0.259, 0.322, 1.0),
border_color: Vec4::new(0.369, 0.420, 0.506, 1.0),
border_color_focused: Vec4::new(0.533, 0.753, 0.816, 1.0),
accent_color: Vec4::new(0.533, 0.753, 0.816, 1.0),
accent_color_hovered: Vec4::new(0.565, 0.792, 0.863, 1.0),
accent_color_active: Vec4::new(0.427, 0.671, 0.757, 1.0),
success_color: Vec4::new(0.639, 0.745, 0.549, 1.0),
warning_color: Vec4::new(0.922, 0.796, 0.545, 1.0),
error_color: Vec4::new(0.749, 0.380, 0.416, 1.0),
slider_track_color: Vec4::new(0.231, 0.259, 0.322, 1.0),
slider_fill_color: Vec4::new(0.533, 0.753, 0.816, 1.0),
input_background_color: Vec4::new(0.141, 0.165, 0.204, 1.0),
input_background_focused: Vec4::new(0.180, 0.204, 0.251, 1.0),
selection_color: Vec4::new(0.533, 0.753, 0.816, 0.4),
scrollbar_color: Vec4::new(0.369, 0.420, 0.506, 0.4),
scrollbar_color_hovered: Vec4::new(0.369, 0.420, 0.506, 0.7),
})
}
pub fn gruvbox_dark() -> Self {
Self::from_palette(ThemePalette {
name: "Gruvbox Dark",
dark_mode: true,
text_color: Vec4::new(0.922, 0.859, 0.698, 1.0),
text_color_disabled: Vec4::new(0.502, 0.502, 0.502, 1.0),
text_color_accent: Vec4::new(0.514, 0.647, 0.596, 1.0),
background_color: Vec4::new(0.157, 0.157, 0.157, 1.0),
background_color_hovered: Vec4::new(0.235, 0.220, 0.212, 1.0),
background_color_active: Vec4::new(0.314, 0.294, 0.278, 1.0),
panel_color: Vec4::new(0.157, 0.157, 0.157, 1.0),
panel_header_color: Vec4::new(0.235, 0.220, 0.212, 1.0),
border_color: Vec4::new(0.400, 0.361, 0.329, 1.0),
border_color_focused: Vec4::new(0.514, 0.647, 0.596, 1.0),
accent_color: Vec4::new(0.514, 0.647, 0.596, 1.0),
accent_color_hovered: Vec4::new(0.557, 0.694, 0.639, 1.0),
accent_color_active: Vec4::new(0.427, 0.561, 0.518, 1.0),
success_color: Vec4::new(0.722, 0.733, 0.149, 1.0),
warning_color: Vec4::new(0.980, 0.741, 0.184, 1.0),
error_color: Vec4::new(0.984, 0.286, 0.204, 1.0),
slider_track_color: Vec4::new(0.235, 0.220, 0.212, 1.0),
slider_fill_color: Vec4::new(0.514, 0.647, 0.596, 1.0),
input_background_color: Vec4::new(0.114, 0.125, 0.129, 1.0),
input_background_focused: Vec4::new(0.157, 0.157, 0.157, 1.0),
selection_color: Vec4::new(0.400, 0.361, 0.329, 0.5),
scrollbar_color: Vec4::new(0.400, 0.361, 0.329, 0.4),
scrollbar_color_hovered: Vec4::new(0.400, 0.361, 0.329, 0.7),
})
}
pub fn catppuccin_mocha() -> Self {
Self::from_palette(ThemePalette {
name: "Catppuccin Mocha",
dark_mode: true,
text_color: Vec4::new(0.804, 0.839, 0.957, 1.0),
text_color_disabled: Vec4::new(0.345, 0.357, 0.439, 1.0),
text_color_accent: Vec4::new(0.537, 0.706, 0.980, 1.0),
background_color: Vec4::new(0.118, 0.118, 0.180, 1.0),
background_color_hovered: Vec4::new(0.192, 0.196, 0.267, 1.0),
background_color_active: Vec4::new(0.271, 0.278, 0.353, 1.0),
panel_color: Vec4::new(0.118, 0.118, 0.180, 1.0),
panel_header_color: Vec4::new(0.192, 0.196, 0.267, 1.0),
border_color: Vec4::new(0.345, 0.357, 0.439, 1.0),
border_color_focused: Vec4::new(0.537, 0.706, 0.980, 1.0),
accent_color: Vec4::new(0.537, 0.706, 0.980, 1.0),
accent_color_hovered: Vec4::new(0.608, 0.765, 1.0, 1.0),
accent_color_active: Vec4::new(0.431, 0.612, 0.878, 1.0),
success_color: Vec4::new(0.651, 0.890, 0.631, 1.0),
warning_color: Vec4::new(0.976, 0.886, 0.686, 1.0),
error_color: Vec4::new(0.953, 0.545, 0.659, 1.0),
slider_track_color: Vec4::new(0.192, 0.196, 0.267, 1.0),
slider_fill_color: Vec4::new(0.537, 0.706, 0.980, 1.0),
input_background_color: Vec4::new(0.067, 0.067, 0.106, 1.0),
input_background_focused: Vec4::new(0.118, 0.118, 0.180, 1.0),
selection_color: Vec4::new(0.345, 0.357, 0.439, 0.5),
scrollbar_color: Vec4::new(0.345, 0.357, 0.439, 0.4),
scrollbar_color_hovered: Vec4::new(0.345, 0.357, 0.439, 0.7),
})
}
pub fn all_presets() -> Vec<Self> {
vec![
Self::dark(),
Self::dracula(),
Self::nord(),
Self::gruvbox_dark(),
Self::catppuccin_mocha(),
]
}
}
#[derive(Clone, Debug)]
pub struct ThemeState {
pub current_theme: UiTheme,
pub presets: Vec<UiTheme>,
pub selected_preset_index: Option<usize>,
pub preview_theme_index: Option<usize>,
pub generation: u64,
pub last_applied_generation: u64,
pub transition_from: Option<UiTheme>,
pub transition_progress: f32,
pub transition_duration: f32,
pub transition_theme: Option<UiTheme>,
}
impl Default for ThemeState {
fn default() -> Self {
let presets = UiTheme::all_presets();
Self {
current_theme: presets[0].clone(),
presets,
selected_preset_index: Some(0),
preview_theme_index: None,
generation: 0,
last_applied_generation: 0,
transition_from: None,
transition_progress: 0.0,
transition_duration: 0.25,
transition_theme: None,
}
}
}
impl ThemeState {
pub fn active_theme(&self) -> &UiTheme {
if let Some(transition) = self.transition_theme.as_ref() {
return transition;
}
self.preview_theme_index
.and_then(|index| self.presets.get(index))
.unwrap_or(&self.current_theme)
}
pub fn select_theme(&mut self, index: usize) {
if let Some(preset) = self.presets.get(index) {
let from = self.active_theme().clone();
self.transition_from = Some(from);
self.transition_progress = 0.0;
self.transition_theme = None;
self.current_theme = preset.clone();
self.selected_preset_index = Some(index);
self.preview_theme_index = None;
self.generation += 1;
}
}
pub fn set_preview(&mut self, index: Option<usize>) {
if self.preview_theme_index != index {
self.preview_theme_index = index;
self.generation += 1;
}
}
pub fn clear_preview(&mut self) {
if self.preview_theme_index.is_some() {
self.preview_theme_index = None;
self.generation += 1;
}
}
pub fn tick_transition(&mut self, delta_time: f32) {
let Some(from) = self.transition_from.as_ref() else {
return;
};
let duration = self.transition_duration.max(0.0001);
self.transition_progress = (self.transition_progress + delta_time / duration).min(1.0);
if self.transition_progress >= 1.0 {
self.transition_from = None;
self.transition_theme = None;
self.generation += 1;
return;
}
let t = ease_in_out_cubic(self.transition_progress);
let to = &self.current_theme;
self.transition_theme = Some(lerp_theme_visual(from, to, t));
self.generation += 1;
}
}
fn ease_in_out_cubic(t: f32) -> f32 {
if t < 0.5 {
4.0 * t * t * t
} else {
1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
}
}
fn lerp_vec4(a: Vec4, b: Vec4, t: f32) -> Vec4 {
a + (b - a) * t
}
fn lerp_vec2(a: Vec2, b: Vec2, t: f32) -> Vec2 {
a + (b - a) * t
}
fn lerp_shadow(a: UiNodeShadow, b: UiNodeShadow, t: f32) -> UiNodeShadow {
UiNodeShadow {
color: lerp_vec4(a.color, b.color, t),
offset: lerp_vec2(a.offset, b.offset, t),
blur: a.blur + (b.blur - a.blur) * t,
spread: a.spread + (b.spread - a.spread) * t,
}
}
fn lerp_optional_shadow(
a: Option<UiNodeShadow>,
b: Option<UiNodeShadow>,
t: f32,
) -> Option<UiNodeShadow> {
match (a, b) {
(Some(a), Some(b)) => Some(lerp_shadow(a, b, t)),
(Some(a), None) => {
let mut out = a;
out.color.w *= 1.0 - t;
Some(out)
}
(None, Some(b)) => {
let mut out = b;
out.color.w *= t;
Some(out)
}
(None, None) => None,
}
}
fn lerp_theme_visual(from: &UiTheme, to: &UiTheme, t: f32) -> UiTheme {
let mut out = to.clone();
out.text_color = lerp_vec4(from.text_color, to.text_color, t);
out.text_color_disabled = lerp_vec4(from.text_color_disabled, to.text_color_disabled, t);
out.text_color_accent = lerp_vec4(from.text_color_accent, to.text_color_accent, t);
out.background_color = lerp_vec4(from.background_color, to.background_color, t);
out.background_color_hovered = lerp_vec4(
from.background_color_hovered,
to.background_color_hovered,
t,
);
out.background_color_active =
lerp_vec4(from.background_color_active, to.background_color_active, t);
out.panel_color = lerp_vec4(from.panel_color, to.panel_color, t);
out.panel_header_color = lerp_vec4(from.panel_header_color, to.panel_header_color, t);
out.border_color = lerp_vec4(from.border_color, to.border_color, t);
out.border_color_focused = lerp_vec4(from.border_color_focused, to.border_color_focused, t);
out.accent_color = lerp_vec4(from.accent_color, to.accent_color, t);
out.accent_color_hovered = lerp_vec4(from.accent_color_hovered, to.accent_color_hovered, t);
out.accent_color_active = lerp_vec4(from.accent_color_active, to.accent_color_active, t);
out.success_color = lerp_vec4(from.success_color, to.success_color, t);
out.warning_color = lerp_vec4(from.warning_color, to.warning_color, t);
out.error_color = lerp_vec4(from.error_color, to.error_color, t);
out.slider_track_color = lerp_vec4(from.slider_track_color, to.slider_track_color, t);
out.slider_fill_color = lerp_vec4(from.slider_fill_color, to.slider_fill_color, t);
out.input_background_color =
lerp_vec4(from.input_background_color, to.input_background_color, t);
out.input_background_focused = lerp_vec4(
from.input_background_focused,
to.input_background_focused,
t,
);
out.selection_color = lerp_vec4(from.selection_color, to.selection_color, t);
out.scrollbar_color = lerp_vec4(from.scrollbar_color, to.scrollbar_color, t);
out.scrollbar_color_hovered =
lerp_vec4(from.scrollbar_color_hovered, to.scrollbar_color_hovered, t);
out.panel_shadow = lerp_shadow(from.panel_shadow, to.panel_shadow, t);
out.button_shadow = lerp_shadow(from.button_shadow, to.button_shadow, t);
out.button_shadow_hover = lerp_shadow(from.button_shadow_hover, to.button_shadow_hover, t);
out.button_shadow_pressed =
lerp_shadow(from.button_shadow_pressed, to.button_shadow_pressed, t);
out.modal_shadow = lerp_shadow(from.modal_shadow, to.modal_shadow, t);
out.focus_glow = lerp_optional_shadow(from.focus_glow, to.focus_glow, t);
out
}