use iced::keyboard::key::Named;
use iced::widget::{self, toggler::Status};
use iced::{Background, Color, Element};
use crate::keys::{self, Chord, Keymap};
use crate::theme::{Tokens, fade, mix, space as spacing, text_size};
pub const TRACK_HEIGHT: f32 = 20.0;
pub struct Switch<'a, Message> {
is_on: bool,
label: Option<widget::text::Fragment<'a>>,
on_toggle: Option<Box<dyn Fn(bool) -> Message + 'a>>,
}
impl<Message> std::fmt::Debug for Switch<'_, Message> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Switch")
.field("is_on", &self.is_on)
.field("label", &self.label)
.field("enabled", &self.on_toggle.is_some())
.finish()
}
}
pub fn switch<'a, Message>(is_on: bool) -> Switch<'a, Message> {
Switch {
is_on,
label: None,
on_toggle: None,
}
}
impl<'a, Message> Switch<'a, Message> {
pub fn label(mut self, label: impl widget::text::IntoFragment<'a>) -> Self {
self.label = Some(label.into_fragment());
self
}
pub fn on_toggle(mut self, on_toggle: impl Fn(bool) -> Message + 'a) -> Self {
self.on_toggle = Some(Box::new(on_toggle));
self
}
pub fn on_toggle_maybe(mut self, on_toggle: Option<impl Fn(bool) -> Message + 'a>) -> Self {
self.on_toggle = on_toggle.map(|f| Box::new(f) as Box<dyn Fn(bool) -> Message + 'a>);
self
}
pub fn is_on(&self) -> bool {
self.is_on
}
pub fn is_enabled(&self) -> bool {
self.on_toggle.is_some()
}
}
impl<'a, Message: 'a> From<Switch<'a, Message>> for Element<'a, Message> {
fn from(switch: Switch<'a, Message>) -> Self {
let mut toggler = widget::toggler(switch.is_on)
.size(TRACK_HEIGHT)
.text_size(text_size::SM)
.spacing(spacing::SM)
.style(|theme, status| style(&Tokens::of(theme), status));
if let Some(label) = switch.label {
toggler = toggler.label(label);
}
if let Some(on_toggle) = switch.on_toggle {
toggler = toggler.on_toggle(on_toggle);
}
toggler.into()
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Colours {
pub track: Color,
pub thumb: Color,
pub label: Color,
}
pub fn colours(tokens: &Tokens, status: Status) -> Colours {
let (is_on, hover, disabled) = match status {
Status::Active { is_toggled } => (is_toggled, false, false),
Status::Hovered { is_toggled } => (is_toggled, true, false),
Status::Disabled { is_toggled } => (is_toggled, false, true),
};
let off_track = mix(tokens.border, tokens.foreground, 0.1);
let track = match (is_on, hover) {
(true, false) => tokens.primary,
(true, true) => mix(tokens.primary, tokens.background, 0.15),
(false, false) => off_track,
(false, true) => mix(off_track, tokens.foreground, 0.15),
};
let colours = Colours {
track,
thumb: tokens.background,
label: tokens.foreground,
};
if !disabled {
return colours;
}
Colours {
track: fade(colours.track, 0.5),
thumb: colours.thumb,
label: fade(colours.label, 0.5),
}
}
pub fn style(tokens: &Tokens, status: Status) -> widget::toggler::Style {
let colours = colours(tokens, status);
widget::toggler::Style {
background: Background::Color(colours.track),
background_border_width: 0.0,
background_border_color: Color::TRANSPARENT,
foreground: Background::Color(colours.thumb),
foreground_border_width: 0.0,
foreground_border_color: Color::TRANSPARENT,
text_color: Some(colours.label),
border_radius: None,
padding_ratio: 0.1,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Action {
Toggle,
}
impl Action {
pub fn apply(self, is_on: bool) -> bool {
match self {
Action::Toggle => !is_on,
}
}
}
impl keys::Action for Action {
const ALL: &'static [Self] = &[Action::Toggle];
fn defaults() -> Keymap<Self> {
default_keymap()
}
fn name(self) -> &'static str {
"Toggle"
}
fn description(self) -> &'static str {
"Turns the current switch on or off."
}
}
pub fn default_keymap() -> Keymap<Action> {
Keymap::new().bind(Chord::named(Named::Space), Action::Toggle)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::theme::{dark, light};
#[test]
fn toggle_flips_the_value() {
assert!(Action::Toggle.apply(false));
assert!(!Action::Toggle.apply(true));
}
#[test]
fn default_keymap_and_overrides() {
let space = iced::keyboard::Key::Named(Named::Space);
let keymap = default_keymap();
assert_eq!(
keymap.resolve(&space, Default::default()),
Some(Action::Toggle)
);
let custom = keymap
.unbind(&Chord::named(Named::Space))
.bind("T".parse().unwrap(), Action::Toggle);
assert_eq!(custom.resolve(&space, Default::default()), None);
assert_eq!(
custom.resolve(
&iced::keyboard::Key::Character("t".into()),
Default::default()
),
Some(Action::Toggle)
);
}
fn statuses(is_toggled: bool) -> [Status; 3] {
[
Status::Active { is_toggled },
Status::Hovered { is_toggled },
Status::Disabled { is_toggled },
]
}
#[test]
fn default_builder_is_unlabelled_and_disabled() {
let s: Switch<'_, ()> = switch(true);
assert!(s.is_on());
assert!(s.label.is_none());
assert!(!s.is_enabled());
}
#[test]
fn on_toggle_enables_and_maybe_none_disables() {
assert!(switch::<bool>(false).on_toggle(|v| v).is_enabled());
let s = switch::<bool>(false).on_toggle_maybe(None::<fn(bool) -> bool>);
assert!(!s.is_enabled());
}
#[test]
fn on_track_is_primary_and_off_track_is_not() {
for theme in [light(), dark()] {
let tokens = Tokens::of(&theme);
let on = colours(&tokens, Status::Active { is_toggled: true });
let off = colours(&tokens, Status::Active { is_toggled: false });
assert_eq!(on.track, tokens.primary);
assert_ne!(off.track, tokens.primary);
assert_eq!(on.thumb, tokens.background);
}
}
#[test]
fn hover_changes_the_track() {
for theme in [light(), dark()] {
let tokens = Tokens::of(&theme);
for is_toggled in [false, true] {
let active = colours(&tokens, Status::Active { is_toggled });
let hovered = colours(&tokens, Status::Hovered { is_toggled });
assert_ne!(active.track, hovered.track);
}
}
}
#[test]
fn disabled_fades_track_and_label() {
for theme in [light(), dark()] {
let tokens = Tokens::of(&theme);
for is_toggled in [false, true] {
let active = colours(&tokens, Status::Active { is_toggled });
let disabled = colours(&tokens, Status::Disabled { is_toggled });
assert!((disabled.track.a - active.track.a * 0.5).abs() < 1e-6);
assert!((disabled.label.a - active.label.a * 0.5).abs() < 1e-6);
}
}
}
#[test]
fn every_status_has_a_round_track_and_label_colour() {
let tokens = Tokens::of(&dark());
for status in statuses(true).into_iter().chain(statuses(false)) {
let style = style(&tokens, status);
assert!(style.border_radius.is_none());
assert!(style.text_color.is_some());
}
}
}