use crate::event::{Key, NamedKey, Point, Rect};
use crate::types::{Modifiers, WidgetId};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Switch {
pub id: WidgetId,
#[serde(default)]
pub value: bool,
#[serde(default)]
pub disabled: bool,
#[serde(default)]
pub focused: bool,
}
impl Switch {
pub fn new(id: WidgetId) -> Self {
Self {
id,
value: false,
disabled: false,
focused: false,
}
}
pub fn layout(&self, bounds: Rect) -> SwitchLayout {
SwitchLayout { bounds }
}
pub fn handle_key(&self, key: &Key, _modifiers: &Modifiers) -> Option<SwitchEvent> {
if self.disabled || !self.focused {
return None;
}
match key {
Key::Named(NamedKey::Enter) | Key::Char(' ') => Some(SwitchEvent::Toggled {
id: self.id,
value: !self.value,
}),
_ => None,
}
}
pub fn click(&self) -> Option<SwitchEvent> {
if self.disabled {
return None;
}
Some(SwitchEvent::Toggled {
id: self.id,
value: !self.value,
})
}
}
impl Default for Switch {
fn default() -> Self {
Self::new(WidgetId::new(""))
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SwitchLayout {
pub bounds: Rect,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SwitchHit {
Body,
Outside,
}
impl SwitchLayout {
pub fn hit_test(&self, p: Point) -> SwitchHit {
if self.bounds.contains(p) {
SwitchHit::Body
} else {
SwitchHit::Outside
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SwitchEvent {
Toggled { id: WidgetId, value: bool },
}
#[cfg(any(
feature = "gtk",
feature = "win",
all(feature = "macos", target_os = "macos")
))]
#[allow(dead_code)]
pub(crate) mod native_surface_paint {
use super::{Switch, SwitchLayout};
use crate::event::Rect;
use crate::paint_surface::PaintSurface;
use crate::theme::Theme;
pub(crate) fn paint(
switch: &Switch,
surface: &mut dyn PaintSurface,
theme: &Theme,
rect: Rect,
) -> SwitchLayout {
let layout = switch.layout(rect);
if rect.width <= 0.0 || rect.height <= 0.0 {
return layout;
}
let track_color = if switch.disabled {
theme.surface_bg
} else if switch.value {
theme.accent_bg
} else {
theme.surface_bg
};
let radius = (rect.height / 2.0).max(0.0);
surface.surface_fill_rounded_rect(rect, radius, track_color);
let border = if switch.disabled {
theme.muted_fg
} else if switch.focused {
theme.accent_fg
} else {
theme.border_fg
};
surface.surface_stroke_rounded_rect(rect, radius, border, 1.0);
let thumb_diameter = rect.height.max(0.0);
let thumb_x = if switch.value {
rect.x + (rect.width - thumb_diameter).max(0.0)
} else {
rect.x
};
let thumb_color = if switch.disabled {
theme.muted_fg
} else {
theme.foreground
};
let thumb_rect = Rect::new(thumb_x, rect.y, thumb_diameter, thumb_diameter);
surface.surface_fill_rounded_rect(thumb_rect, thumb_diameter / 2.0, thumb_color);
layout
}
}
#[cfg(test)]
mod tests {
use super::*;
fn switch() -> Switch {
Switch::new(WidgetId::new("settings:wrap"))
}
#[test]
fn new_defaults_to_off_enabled_and_unfocused() {
let s = switch();
assert!(!s.value);
assert!(!s.disabled);
assert!(!s.focused);
}
#[test]
fn hit_test_inside_and_outside() {
let s = switch();
let layout = s.layout(Rect::new(2.0, 2.0, 6.0, 1.0));
assert_eq!(layout.hit_test(Point::new(3.0, 2.0)), SwitchHit::Body);
assert_eq!(layout.hit_test(Point::new(0.0, 0.0)), SwitchHit::Outside);
}
#[test]
fn handle_key_toggles_on_enter_when_focused_and_enabled() {
let mut s = switch();
s.focused = true;
let event = s.handle_key(&Key::Named(NamedKey::Enter), &Modifiers::default());
assert_eq!(
event,
Some(SwitchEvent::Toggled {
id: s.id.clone(),
value: true
})
);
}
#[test]
fn handle_key_toggles_on_space() {
let mut s = switch();
s.focused = true;
s.value = true;
let event = s.handle_key(&Key::Char(' '), &Modifiers::default());
assert_eq!(
event,
Some(SwitchEvent::Toggled {
id: s.id.clone(),
value: false
})
);
}
#[test]
fn handle_key_ignores_unrelated_keys() {
let mut s = switch();
s.focused = true;
assert_eq!(
s.handle_key(&Key::Named(NamedKey::Tab), &Modifiers::default()),
None
);
}
#[test]
fn handle_key_is_inert_when_not_focused() {
let s = switch();
assert_eq!(
s.handle_key(&Key::Named(NamedKey::Enter), &Modifiers::default()),
None
);
}
#[test]
fn handle_key_is_inert_when_disabled() {
let mut s = switch();
s.focused = true;
s.disabled = true;
assert_eq!(
s.handle_key(&Key::Named(NamedKey::Enter), &Modifiers::default()),
None
);
}
#[test]
fn click_toggles_without_requiring_focus() {
let s = switch();
assert_eq!(
s.click(),
Some(SwitchEvent::Toggled {
id: s.id.clone(),
value: true
})
);
}
#[test]
fn click_is_inert_when_disabled() {
let mut s = switch();
s.disabled = true;
assert_eq!(s.click(), None);
}
#[test]
fn serde_round_trip() {
let mut s = switch();
s.value = true;
s.focused = true;
let json = serde_json::to_string(&s).unwrap();
let back: Switch = serde_json::from_str(&json).unwrap();
assert_eq!(s, back);
}
#[test]
fn serde_defaults_value_disabled_focused_to_false_when_omitted() {
let json = r#"{"id":"settings:wrap"}"#;
let s: Switch = serde_json::from_str(json).unwrap();
assert!(!s.value);
assert!(!s.disabled);
assert!(!s.focused);
}
}