use super::super::colors::{AppearanceMode, WidgetState};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TrafficLightButton {
Close,
Minimize,
Maximize,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MaximizeMode {
Zoom,
Fullscreen,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TrafficLightGroupState {
Default,
Hovered,
Unfocused,
}
pub struct TrafficLightTheme {
pub mode: AppearanceMode,
}
impl TrafficLightTheme {
pub fn new(mode: AppearanceMode) -> Self {
Self { mode }
}
pub fn button_diameter(&self) -> f64 {
12.0
}
pub fn button_radius(&self) -> f64 {
6.0
}
pub fn button_gap(&self) -> f64 {
8.0
}
pub fn container_padding_x(&self) -> f64 {
12.0
}
pub fn container_padding_y(&self) -> f64 {
0.0
}
pub fn total_width(&self) -> f64 {
self.button_diameter() * 3.0 + self.button_gap() * 2.0 + self.container_padding_x() * 2.0
}
pub fn button_color(
&self,
button: TrafficLightButton,
group_state: TrafficLightGroupState,
widget_state: WidgetState,
) -> &'static str {
if group_state == TrafficLightGroupState::Unfocused {
return "#80808080";
}
match button {
TrafficLightButton::Close => match widget_state {
WidgetState::Pressed => "#E0443E",
WidgetState::Hovered => "#FF6F67",
_ => "#FF5F57",
},
TrafficLightButton::Minimize => match widget_state {
WidgetState::Pressed => "#DFA52A",
WidgetState::Hovered => "#FFCC4D",
_ => "#FEBC2E",
},
TrafficLightButton::Maximize => match widget_state {
WidgetState::Pressed => "#1DAD36",
WidgetState::Hovered => "#39D956",
_ => "#28C840",
},
}
}
pub fn show_icon(&self, group_state: TrafficLightGroupState) -> bool {
group_state == TrafficLightGroupState::Hovered
}
pub fn icon_color(&self, button: TrafficLightButton) -> &'static str {
match button {
TrafficLightButton::Close => "#4C0002", TrafficLightButton::Minimize => "#995700", TrafficLightButton::Maximize => "#006500", }
}
pub fn border_color(&self) -> &'static str {
match self.mode {
AppearanceMode::Light
| AppearanceMode::VibrantLight
| AppearanceMode::AccessibleLight
| AppearanceMode::AccessibleVibrantLight => "#00000020", _ => "#00000000", }
}
pub fn hit_test_radius(&self) -> f64 {
7.0
}
pub fn hover_transition_ms(&self) -> u64 {
100
}
pub fn hover_scale(&self) -> f64 {
1.0
}
pub fn button_positions(&self, container_x: f64, container_y: f64) -> [(f64, f64); 3] {
let x_start = container_x + self.container_padding_x() + self.button_radius();
let button_spacing = self.button_diameter() + self.button_gap();
let close_x = x_start;
let minimize_x = x_start + button_spacing;
let maximize_x = x_start + button_spacing * 2.0;
let y = container_y;
[(close_x, y), (minimize_x, y), (maximize_x, y)]
}
pub fn hit_test(
&self,
container_x: f64,
container_y: f64,
mouse_x: f64,
mouse_y: f64,
) -> Option<TrafficLightButton> {
let positions = self.button_positions(container_x, container_y);
let hit_radius_sq = self.hit_test_radius() * self.hit_test_radius();
for (i, &(cx, cy)) in positions.iter().enumerate() {
let dx = mouse_x - cx;
let dy = mouse_y - cy;
let dist_sq = dx * dx + dy * dy;
if dist_sq <= hit_radius_sq {
return Some(match i {
0 => TrafficLightButton::Close,
1 => TrafficLightButton::Minimize,
2 => TrafficLightButton::Maximize,
_ => unreachable!(),
});
}
}
None
}
pub fn is_group_hovered(
&self,
container_x: f64,
container_y: f64,
mouse_x: f64,
mouse_y: f64,
) -> bool {
let group_width = self.button_diameter() * 3.0 + self.button_gap() * 2.0;
let group_height = self.button_diameter();
let x_start = container_x + self.container_padding_x();
let y_start = container_y - self.button_radius();
mouse_x >= x_start
&& mouse_x <= x_start + group_width
&& mouse_y >= y_start
&& mouse_y <= y_start + group_height
}
}
impl Default for TrafficLightTheme {
fn default() -> Self {
Self {
mode: AppearanceMode::Dark,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_geometry() {
let theme = TrafficLightTheme::default();
assert_eq!(theme.button_diameter(), 12.0);
assert_eq!(theme.button_radius(), 6.0);
assert_eq!(theme.button_gap(), 8.0);
assert_eq!(theme.total_width(), 76.0);
}
#[test]
fn test_button_positions() {
let theme = TrafficLightTheme::default();
let positions = theme.button_positions(0.0, 20.0);
assert_eq!(positions[0], (18.0, 20.0));
assert_eq!(positions[1], (38.0, 20.0));
assert_eq!(positions[2], (58.0, 20.0));
}
#[test]
fn test_hit_test() {
let theme = TrafficLightTheme::default();
let result = theme.hit_test(0.0, 20.0, 18.0, 20.0);
assert_eq!(result, Some(TrafficLightButton::Close));
let result = theme.hit_test(0.0, 20.0, 38.0, 20.0);
assert_eq!(result, Some(TrafficLightButton::Minimize));
let result = theme.hit_test(0.0, 20.0, 58.0, 20.0);
assert_eq!(result, Some(TrafficLightButton::Maximize));
let result = theme.hit_test(0.0, 20.0, 100.0, 100.0);
assert_eq!(result, None);
}
#[test]
fn test_group_hover() {
let theme = TrafficLightTheme::default();
assert!(theme.is_group_hovered(0.0, 20.0, 30.0, 20.0));
assert!(!theme.is_group_hovered(0.0, 20.0, 100.0, 20.0));
}
#[test]
fn test_button_colors() {
let theme = TrafficLightTheme::default();
let color = theme.button_color(
TrafficLightButton::Close,
TrafficLightGroupState::Unfocused,
WidgetState::Normal,
);
assert_eq!(color, "#80808080");
let color = theme.button_color(
TrafficLightButton::Close,
TrafficLightGroupState::Default,
WidgetState::Normal,
);
assert_eq!(color, "#FF5F57");
let color = theme.button_color(
TrafficLightButton::Close,
TrafficLightGroupState::Hovered,
WidgetState::Pressed,
);
assert_eq!(color, "#E0443E");
}
#[test]
fn test_show_icon() {
let theme = TrafficLightTheme::default();
assert!(!theme.show_icon(TrafficLightGroupState::Default));
assert!(theme.show_icon(TrafficLightGroupState::Hovered));
assert!(!theme.show_icon(TrafficLightGroupState::Unfocused));
}
#[test]
fn test_icon_colors() {
let theme = TrafficLightTheme::default();
assert_eq!(
theme.icon_color(TrafficLightButton::Close),
"#4C0002"
);
assert_eq!(
theme.icon_color(TrafficLightButton::Minimize),
"#995700"
);
assert_eq!(
theme.icon_color(TrafficLightButton::Maximize),
"#006500"
);
}
#[test]
fn test_border_color() {
let light_theme = TrafficLightTheme::new(AppearanceMode::Light);
assert_eq!(light_theme.border_color(), "#00000020");
let dark_theme = TrafficLightTheme::new(AppearanceMode::Dark);
assert_eq!(dark_theme.border_color(), "#00000000");
}
}