use std::panic::Location;
use crate::anim::Timing;
use crate::cursor::Cursor;
use crate::event::UiEvent;
use crate::layout::LayoutCtx;
use crate::tokens;
use crate::tree::*;
pub const TRACK_WIDTH: f32 = 36.0;
pub const TRACK_HEIGHT: f32 = 20.0;
pub const THUMB_SIZE: f32 = 14.0;
const PAD: f32 = (TRACK_HEIGHT - THUMB_SIZE) / 2.0;
pub const THUMB_SLIDE: f32 = TRACK_WIDTH - THUMB_SIZE - 2.0 * PAD;
#[track_caller]
pub fn switch(value: bool) -> El {
let layout = |ctx: LayoutCtx| {
let r = ctx.container;
let track_x = r.x + (r.w - TRACK_WIDTH) * 0.5;
let track_y = r.y + (r.h - TRACK_HEIGHT) * 0.5;
let thumb_x = track_x + PAD;
let thumb_y = track_y + PAD;
vec![
Rect::new(track_x, track_y, TRACK_WIDTH, TRACK_HEIGHT),
Rect::new(thumb_x, thumb_y, THUMB_SIZE, THUMB_SIZE),
]
};
let track_fill = if value {
tokens::PRIMARY
} else {
tokens::INPUT
};
let thumb_fill = if value {
tokens::PRIMARY_FOREGROUND
} else {
tokens::FOREGROUND
};
let thumb_translate_x = if value { THUMB_SLIDE } else { 0.0 };
stack([
El::new(Kind::Custom("switch-track"))
.fill(track_fill)
.stroke(tokens::BORDER)
.radius(tokens::RADIUS_PILL)
.animate(Timing::SPRING_QUICK)
.state_follows_interactive_ancestor(),
El::new(Kind::Custom("switch-thumb"))
.fill(thumb_fill)
.radius(tokens::RADIUS_PILL)
.translate(thumb_translate_x, 0.0)
.animate(Timing::SPRING_QUICK)
.state_follows_interactive_ancestor(),
])
.at_loc(Location::caller())
.focusable()
.paint_overflow(Sides::all(tokens::RING_WIDTH))
.hit_overflow(Sides::all(tokens::HIT_OVERFLOW))
.cursor(Cursor::Pointer)
.layout(layout)
.width(Size::Fixed(TRACK_WIDTH))
.height(Size::Fixed(TRACK_HEIGHT))
}
pub fn apply_event(value: &mut bool, event: &UiEvent, key: &str) -> bool {
if event.is_click_or_activate(key) {
*value = !*value;
return true;
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::UiEvent;
#[test]
fn off_switch_paints_input_track_and_foreground_thumb() {
let s = switch(false);
let track = &s.children[0];
let thumb = &s.children[1];
assert_eq!(track.fill, Some(tokens::INPUT));
assert_eq!(thumb.fill, Some(tokens::FOREGROUND));
assert_eq!(track.radius, crate::tree::Corners::all(tokens::RADIUS_PILL));
}
#[test]
fn on_switch_paints_primary_track_and_primary_foreground_thumb() {
let s = switch(true);
let track = &s.children[0];
let thumb = &s.children[1];
assert_eq!(track.fill, Some(tokens::PRIMARY));
assert_eq!(thumb.fill, Some(tokens::PRIMARY_FOREGROUND));
}
#[test]
fn switch_is_focusable_and_paints_focus_ring_outset() {
let s = switch(false);
assert!(s.focusable);
assert!(s.paint_overflow.left > 0.0);
assert_eq!(s.hit_overflow, Sides::all(tokens::HIT_OVERFLOW));
}
#[test]
fn switch_declares_pointer_cursor() {
assert_eq!(switch(false).cursor, Some(Cursor::Pointer));
}
#[test]
fn apply_event_toggles_on_click() {
let mut value = false;
assert!(apply_event(
&mut value,
&UiEvent::synthetic_click("save"),
"save"
));
assert!(value);
assert!(apply_event(
&mut value,
&UiEvent::synthetic_click("save"),
"save"
));
assert!(!value);
}
#[test]
fn apply_event_ignores_unrelated_keys() {
let mut value = true;
assert!(!apply_event(
&mut value,
&UiEvent::synthetic_click("other"),
"save",
));
assert!(value, "value preserved when key doesn't match");
}
#[test]
fn layout_pins_thumb_to_off_position_regardless_of_value() {
use crate::layout::layout;
use crate::state::UiState;
for value in [false, true] {
let mut tree = switch(value);
let mut state = UiState::new();
let viewport = Rect::new(0.0, 0.0, TRACK_WIDTH, TRACK_HEIGHT);
layout(&mut tree, &mut state, viewport);
let thumb_rect = state.rect(&tree.children[1].computed_id);
assert!(
(thumb_rect.x - PAD).abs() < 1e-3,
"value={value}: layout-rect thumb.x={}, expected={PAD}",
thumb_rect.x,
);
}
}
#[test]
fn translate_carries_the_thumb_slide_when_on() {
let off = switch(false);
let on = switch(true);
assert_eq!(off.children[1].translate, (0.0, 0.0));
assert!(
(on.children[1].translate.0 - THUMB_SLIDE).abs() < 1e-3,
"thumb translate.x = {}, expected {THUMB_SLIDE}",
on.children[1].translate.0,
);
}
#[test]
fn track_and_thumb_animate_so_state_changes_ease() {
let s = switch(false);
assert!(s.children[0].animate.is_some(), "track must animate");
assert!(s.children[1].animate.is_some(), "thumb must animate");
}
}