diff --git a/crates/ui/src/switch.rs b/crates/ui/src/switch.rs index b3f74c8..c610745 100644 --- a/crates/ui/src/switch.rs +++ b/crates/ui/src/switch.rs @@ -1,13 +1,13 @@ -use std::time::Duration; +use std::{cell::RefCell, rc::Rc, time::Duration}; use crate::{h_flex, theme::ActiveTheme, Disableable, Sizable, Size}; use gpui::{ - div, prelude::FluentBuilder as _, px, Animation, AnimationExt as _, Div, ElementId, - InteractiveElement, IntoElement, ParentElement as _, RenderOnce, SharedString, Stateful, - Styled as _, WindowContext, + div, prelude::FluentBuilder as _, px, Animation, AnimationExt as _, AnyElement, Element, + ElementId, GlobalElementId, InteractiveElement, IntoElement, LayoutId, ParentElement as _, + SharedString, Styled as _, WindowContext, }; -type OnClick = Box; +type OnClick = Rc; pub enum LabelSide { Left, @@ -20,10 +20,8 @@ impl LabelSide { } } -#[derive(IntoElement)] pub struct Switch { id: ElementId, - base: Stateful
, checked: bool, disabled: bool, label: Option, @@ -37,7 +35,6 @@ impl Switch { let id: ElementId = id.into(); Self { id: id.clone(), - base: div().id(id), checked: false, disabled: false, label: None, @@ -57,8 +54,11 @@ impl Switch { self } - pub fn on_click(mut self, handler: impl Fn(&bool, &mut WindowContext) + 'static) -> Self { - self.on_click = Some(Box::new(handler)); + pub fn on_click(mut self, handler: F) -> Self + where + F: Fn(&bool, &mut WindowContext) + 'static, + { + self.on_click = Some(Rc::new(handler)); self } @@ -82,83 +82,167 @@ impl Disableable for Switch { } } -impl RenderOnce for Switch { - fn render(self, cx: &mut gpui::WindowContext) -> impl IntoElement { - let theme = cx.theme(); - let checked = self.checked; - - let (bg, toggle_bg) = match self.checked { - true => (theme.primary, theme.background), - false => (theme.input, theme.background), - }; - - let (bg, toggle_bg) = match self.disabled { - true => (bg.opacity(0.3), toggle_bg.opacity(0.8)), - false => (bg, toggle_bg), - }; - - let (bg_width, bg_height) = match self.size { - Size::XSmall | Size::Small => (px(28.), px(16.)), - _ => (px(36.), px(20.)), - }; - let bar_width = match self.size { - Size::XSmall | Size::Small => px(12.), - _ => px(16.), - }; - let inset = px(2.); - - h_flex() - .id(self.id) - .items_center() - .gap_2() - .when(self.label_side.left(), |this| this.flex_row_reverse()) - .child( - // Switch Bar - self.base - .w(bg_width) - .h(bg_height) - .rounded(bg_height / 2.) - .flex() - .items_center() - .border(inset) - .border_color(theme.transparent) - .bg(bg) - .when(!self.disabled, |this| this.cursor_pointer()) - .child( - // Switch Toggle - div() - .rounded_full() - .bg(toggle_bg) - .size(bar_width) - .with_animation( - ElementId::NamedInteger("move".into(), checked as usize), - Animation::new(Duration::from_secs_f64(0.15)), - move |this, delta| { - let max_x = bg_width - bar_width - inset * 2; - let x = if checked { - max_x * delta +impl IntoElement for Switch { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +#[derive(Default)] +pub struct SwitchState { + prev_checked: Rc>>, +} + +impl Element for Switch { + type RequestLayoutState = AnyElement; + + type PrepaintState = (); + + fn id(&self) -> Option { + Some(self.id.clone()) + } + + fn request_layout( + &mut self, + global_id: Option<&GlobalElementId>, + cx: &mut WindowContext, + ) -> (LayoutId, Self::RequestLayoutState) { + cx.with_element_state::(global_id.unwrap(), move |state, cx| { + let state = state.unwrap_or_default(); + + let theme = cx.theme(); + let checked = self.checked; + let on_click = self.on_click.clone(); + + let (bg, toggle_bg) = match self.checked { + true => (theme.primary, theme.background), + false => (theme.input, theme.background), + }; + + let (bg, toggle_bg) = match self.disabled { + true => (bg.opacity(0.3), toggle_bg.opacity(0.8)), + false => (bg, toggle_bg), + }; + + let (bg_width, bg_height) = match self.size { + Size::XSmall | Size::Small => (px(28.), px(16.)), + _ => (px(36.), px(20.)), + }; + let bar_width = match self.size { + Size::XSmall | Size::Small => px(12.), + _ => px(16.), + }; + let inset = px(2.); + + let mut element = h_flex() + .id(self.id.clone()) + .items_center() + .gap_2() + .when(self.label_side.left(), |this| this.flex_row_reverse()) + .child( + // Switch Bar + div() + .id(self.id.clone()) + .w(bg_width) + .h(bg_height) + .rounded(bg_height / 2.) + .flex() + .items_center() + .border(inset) + .border_color(theme.transparent) + .bg(bg) + .when(!self.disabled, |this| this.cursor_pointer()) + .child( + // Switch Toggle + div() + .rounded_full() + .bg(toggle_bg) + .size(bar_width) + .map(|this| { + let prev_checked = state.prev_checked.clone(); + if !self.disabled + && prev_checked + .borrow() + .map_or(false, |prev| prev != checked) + { + let dur = Duration::from_secs_f64(0.15); + cx.spawn(|cx| async move { + cx.background_executor().timer(dur).await; + + *prev_checked.borrow_mut() = Some(checked); + }) + .detach(); + this.with_animation( + ElementId::NamedInteger( + "move".into(), + checked as usize, + ), + Animation::new(dur), + move |this, delta| { + let max_x = bg_width - bar_width - inset * 2; + let x = if checked { + max_x * delta + } else { + max_x - max_x * delta + }; + this.left(x) + }, + ) + .into_any_element() } else { - max_x - max_x * delta - }; - this.left(x) - }, - ), - ), - ) - .when_some(self.label, |this, label| { - this.child(div().child(label).map(|this| match self.size { - Size::XSmall | Size::Small => this.text_sm(), - _ => this.text_base(), - })) - }) - .when_some( - self.on_click.filter(|_| !self.disabled), - |this, on_click| { - this.on_mouse_down(gpui::MouseButton::Left, move |_, cx| { - cx.stop_propagation(); - on_click(&!self.checked, cx); - }) - }, - ) + let max_x = bg_width - bar_width - inset * 2; + let x = if checked { max_x } else { px(0.) }; + this.left(x).into_any_element() + } + }), + ), + ) + .when_some(self.label.clone(), |this, label| { + this.child(div().child(label).map(|this| match self.size { + Size::XSmall | Size::Small => this.text_sm(), + _ => this.text_base(), + })) + }) + .when_some( + on_click + .as_ref() + .map(|c| c.clone()) + .filter(|_| !self.disabled), + |this, on_click| { + let prev_checked = state.prev_checked.clone(); + this.on_mouse_down(gpui::MouseButton::Left, move |_, cx| { + cx.stop_propagation(); + *prev_checked.borrow_mut() = Some(checked); + on_click(&!checked, cx); + }) + }, + ) + .into_any_element(); + + ((element.request_layout(cx), element), state) + }) + } + + fn prepaint( + &mut self, + _: Option<&gpui::GlobalElementId>, + _: gpui::Bounds, + element: &mut Self::RequestLayoutState, + cx: &mut WindowContext, + ) { + element.prepaint(cx); + } + + fn paint( + &mut self, + _: Option<&gpui::GlobalElementId>, + _: gpui::Bounds, + element: &mut Self::RequestLayoutState, + _: &mut Self::PrepaintState, + cx: &mut WindowContext, + ) { + element.paint(cx) } }