#![warn(missing_docs)]
use std::panic::Location;
use crate::anim::Timing;
use crate::cursor::Cursor;
use crate::event::UiEvent;
use crate::layout::LayoutCtx;
use crate::metrics::MetricsRole;
use crate::style::StyleProfile;
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(key: impl Into<String>, value: bool) -> El {
let layout = |ctx: LayoutCtx| {
let r = ctx.container;
let pad = r.h * (PAD / TRACK_HEIGHT);
let thumb = r.h - 2.0 * pad;
vec![
Rect::new(r.x, r.y, r.w, r.h),
Rect::new(r.x + pad, r.y + pad, thumb, thumb),
]
};
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 };
El::new(Kind::Custom("switch"))
.at_loc(Location::caller())
.style_profile(StyleProfile::Surface)
.metrics_role(MetricsRole::Switch)
.key(key)
.axis(Axis::Overlay)
.focusable()
.paint_overflow(Sides::all(tokens::RING_WIDTH))
.hit_overflow(Sides::all(tokens::HIT_OVERFLOW))
.cursor(Cursor::Pointer)
.layout(layout)
.default_width(Size::Fixed(TRACK_WIDTH))
.default_height(Size::Fixed(TRACK_HEIGHT))
.children([
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(),
])
}
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("demo-switch-off", 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("demo-switch-on", 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("demo-switch-focus", 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("demo-switch-cursor", 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("demo-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 = tree.children[1].computed_rect;
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("demo-switch-off", false);
let on = switch("demo-switch-on", 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("demo-switch-anim", false);
assert!(
s.children[0].animate_timing().is_some(),
"track must animate"
);
assert!(
s.children[1].animate_timing().is_some(),
"thumb must animate"
);
}
}