use std::sync::Arc;
use gpui::{
Animation, AnimationExt, ClickEvent, Div, ElementId, Hsla, InteractiveElement, IntoElement,
ParentElement, RenderOnce, StatefulInteractiveElement, Styled, div, px,
};
use crate::{
animation,
component::{
ToggleCallback, compute_toggle_style, create_internal_state, resolve_state_value_simple,
use_internal_state_simple,
},
theme::ActiveTheme,
};
use crate::animation::ease_in_out_clamped;
pub fn switch(id: impl Into<ElementId>) -> Switch {
Switch::new().id(id)
}
#[derive(IntoElement)]
pub struct Switch {
element_id: ElementId,
base: Div,
checked: bool,
disabled: bool,
on_toggle: Option<ToggleCallback>,
tone: Option<Hsla>,
}
impl Default for Switch {
fn default() -> Self {
Self::new()
}
}
impl Switch {
pub fn new() -> Self {
Self {
element_id: "ui:switch".into(),
base: div().w(px(34.)).h(px(18.)),
checked: false,
disabled: false,
on_toggle: None,
tone: None,
}
}
pub fn id(mut self, id: impl Into<ElementId>) -> Self {
self.element_id = id.into();
self
}
pub fn key(self, key: impl Into<ElementId>) -> Self {
self.id(key)
}
pub fn checked(mut self, checked: bool) -> Self {
self.checked = checked;
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn tone(mut self, tone: impl Into<Hsla>) -> Self {
self.tone = Some(tone.into());
self
}
pub fn on_toggle<F>(mut self, handler: F) -> Self
where
F: 'static + Fn(bool, Option<&ClickEvent>, &mut gpui::Window, &mut gpui::App),
{
self.on_toggle = Some(Arc::new(handler));
self
}
}
impl ParentElement for Switch {
fn extend(&mut self, elements: impl IntoIterator<Item = gpui::AnyElement>) {
self.base.extend(elements);
}
}
impl Styled for Switch {
fn style(&mut self) -> &mut gpui::StyleRefinement {
self.base.style()
}
}
impl InteractiveElement for Switch {
fn interactivity(&mut self) -> &mut gpui::Interactivity {
self.base.interactivity()
}
}
impl StatefulInteractiveElement for Switch {}
impl RenderOnce for Switch {
fn render(self, window: &mut gpui::Window, cx: &mut gpui::App) -> impl IntoElement {
let disabled = self.disabled;
let explicit_checked = self.checked;
let on_toggle = self.on_toggle;
let tone = self.tone;
let id = self.element_id;
let use_internal = use_internal_state_simple(on_toggle.is_some());
let internal_checked = create_internal_state(
window,
cx,
&id,
"ui:switch:checked".to_string(),
explicit_checked,
use_internal,
);
let checked =
resolve_state_value_simple(explicit_checked, &internal_checked, cx, use_internal);
let theme = cx.theme();
let toggle_style = compute_toggle_style(theme, checked, disabled, tone);
let knob_bg = if disabled {
theme.content.disabled
} else if checked {
theme.action.primary.fg
} else {
theme.content.primary
};
let mut base = self
.base
.id(id.clone())
.rounded_full()
.border_1()
.border_color(toggle_style.border)
.bg(toggle_style.bg)
.p(px(2.))
.relative() .h(px(18.)) .focusable()
.focus_visible(|style| style.border_2().border_color(theme.border.focus));
if disabled {
base = base
.opacity(toggle_style.disabled_opacity)
.cursor_not_allowed();
} else {
base = base
.cursor_pointer()
.hover(move |this| this.bg(toggle_style.hover_bg));
}
let knob = div()
.w(px(14.))
.h(px(14.))
.rounded_full()
.bg(knob_bg)
.absolute()
.top(px(2.)) .left(px(2.));
let animated_knob = knob.with_animation(
format!("ui:switch:knob:{}", checked),
Animation::new(animation::duration::FAST).with_easing(ease_in_out_clamped),
move |this, value| {
let position = if checked { value } else { 1.0 - value };
this.left(px(2. + position * 16.0))
},
);
base.child(animated_knob).on_click(move |ev, window, cx| {
if disabled {
return;
}
if use_internal {
if let Some(internal_checked) = &internal_checked {
internal_checked.update(cx, |value, _cx| *value = !*value);
}
} else if let Some(handler) = &on_toggle {
handler(!explicit_checked, Some(ev), window, cx);
}
})
}
}