From 39496445977d9ece72393967b8b8d2ca997377a5 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Thu, 17 Sep 2026 16:53:43 +0700 Subject: [PATCH] migrate switch onto gpui-base --- crates/ui/src/switch.rs | 283 +++++++++++++--------------------------- 1 file changed, 92 insertions(+), 191 deletions(-) diff --git a/crates/ui/src/switch.rs b/crates/ui/src/switch.rs index 375da353..cdf6ab99 100644 --- a/crates/ui/src/switch.rs +++ b/crates/ui/src/switch.rs @@ -1,19 +1,19 @@ -use std::cell::RefCell; use std::rc::Rc; -use instant::Duration; +use std::time::Duration; use gpui::prelude::FluentBuilder as _; use gpui::{ - Animation, AnimationExt as _, AnyElement, App, Element, ElementId, GlobalElementId, - InteractiveElement, IntoElement, LayoutId, ParentElement as _, SharedString, Styled as _, - Window, div, px, white, + App, ElementId, IntoElement, ParentElement as _, RenderOnce, SharedString, Styled as _, Window, + div, px, white, }; +use gpui_base::{Spring, Switch as BaseSwitch, SwitchThumb, SwitchTrack, spring}; use theme::{ActiveTheme, Side}; use crate::{Disableable, Sizable, Size}; type OnClick = Option>; +#[derive(IntoElement)] pub struct Switch { id: ElementId, checked: bool, @@ -84,204 +84,105 @@ impl Disableable for Switch { } } -impl IntoElement for Switch { - type Element = Self; +impl RenderOnce for Switch { + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + let checked = self.checked; + let on_click = self.on_click.clone(); - fn into_element(self) -> Self::Element { - self - } -} + let (bg, toggle_bg) = match checked { + true => (cx.theme().element_background, white()), + false => (cx.theme().elevated_surface_background, white()), + }; -#[derive(Default)] -pub struct SwitchState { - prev_checked: Rc>>, -} + let (bg, toggle_bg) = match self.disabled { + true => (bg.opacity(0.3), toggle_bg.opacity(0.8)), + false => (bg, toggle_bg), + }; -impl Element for Switch { - type PrepaintState = (); - type RequestLayoutState = AnyElement; + let (bg_width, bg_height) = match self.size { + Size::XSmall | Size::Small => (px(28.), px(16.)), + _ => (px(36.), px(20.)), + }; - fn id(&self) -> Option { - Some(self.id.clone()) - } + let bar_width = match self.size { + Size::XSmall | Size::Small => px(12.), + _ => px(16.), + }; - fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { - None - } + let inset = px(2.); - fn request_layout( - &mut self, - global_id: Option<&GlobalElementId>, - _: Option<&gpui::InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (LayoutId, Self::RequestLayoutState) { - window.with_element_state::(global_id.unwrap(), move |state, window| { - let state = state.unwrap_or_default(); + let thumb_left = spring( + (self.id.clone(), "thumb"), + if checked { + bg_width - bar_width - inset * 2. + } else { + px(0.) + }, + Spring::new(Duration::from_secs_f64(0.15)), + window, + cx, + ); - let theme = cx.theme(); - let checked = self.checked; - let on_click = self.on_click.clone(); + let accessibility_label = self.label.clone(); + let label = self.label; - let (bg, toggle_bg) = match self.checked { - true => (theme.element_background, white()), - false => (theme.elevated_surface_background, white()), - }; - - 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 = div() + div().child( + BaseSwitch::new(self.id.clone()) + .checked(checked) + .disabled(self.disabled) + .when_some(accessibility_label, |this, label| { + this.accessibility_label(label) + }) + .when_some(on_click, |this, on_click| { + this.on_change(move |next, _event, window, cx| on_click(&next, window, cx)) + }) + .when(self.label_side.is_left(), |this| this.flex_row_reverse()) .child( div() - .id(self.id.clone()) - .when(self.label_side.is_left(), |this| this.flex_row_reverse()) - .child( - div() - .w_full() - .flex() - .justify_between() - .items_center() - .gap_4() - .when_some(self.label.clone(), |this, label| { - // Label - this.child( - div().text_sm().text_color(cx.theme().text).child(label), - ) - }) - .child( - // Switch Bar - div() - .id(self.id.clone()) - .flex_shrink_0() - .w(bg_width) - .h(bg_height) - .rounded(bg_height / 2.) - .flex() - .items_center() - .border(inset) - .border_color(theme.border_transparent) - .bg(bg) - .when(!self.disabled, |this| this.cursor_pointer()) - .child( - // Switch Toggle - div() - .rounded_full() - .when(cx.theme().shadow, |this| this.shadow_sm()) - .bg(toggle_bg) - .size(bar_width) - .map(|this| { - let prev_checked = state.prev_checked.clone(); - if !self.disabled - && prev_checked - .borrow() - .is_some_and(|prev| prev != checked) - { - let dur = Duration::from_secs_f64(0.15); - cx.spawn(async move |cx| { - cx.background_executor() - .timer(dur) - .await; - *prev_checked.borrow_mut() = - Some(checked); - }) - .detach(); - this.with_animation( - ElementId::NamedInteger( - "move".into(), - checked as u64, - ), - 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 { - 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.description.clone(), |this, description| { - this.child( - div() - .pr_3() - .text_xs() - .text_color(cx.theme().text_muted) - .child(description), - ) + .w_full() + .flex() + .justify_between() + .items_center() + .gap_4() + .when_some(label, |this, label| { + // Label + this.child(div().text_sm().text_color(cx.theme().text).child(label)) }) - .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 |_, window, cx| { - cx.stop_propagation(); - *prev_checked.borrow_mut() = Some(checked); - on_click(&!checked, window, cx); - }) - }, + .child( + // Switch Bar + SwitchTrack::new((self.id.clone(), "track")) + .checked(checked) + .disabled(self.disabled) + .flex_shrink_0() + .w(bg_width) + .h(bg_height) + .rounded(bg_height / 2.) + .flex() + .items_center() + .border(inset) + .border_color(cx.theme().border_transparent) + .bg(bg) + .when(!self.disabled, |this| this.cursor_pointer()) + .child( + // Switch Toggle + SwitchThumb::new(checked) + .rounded_full() + .when(cx.theme().shadow, |this| this.shadow_sm()) + .bg(toggle_bg) + .size(bar_width) + .left(thumb_left), + ), ), ) - .into_any_element(); - - ((element.request_layout(window, cx), element), state) - }) - } - - fn prepaint( - &mut self, - _: Option<&gpui::GlobalElementId>, - _: Option<&gpui::InspectorElementId>, - _: gpui::Bounds, - element: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) { - element.prepaint(window, cx); - } - - fn paint( - &mut self, - _: Option<&gpui::GlobalElementId>, - _: Option<&gpui::InspectorElementId>, - _: gpui::Bounds, - element: &mut Self::RequestLayoutState, - _: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ) { - element.paint(window, cx) + .when_some(self.description.clone(), |this, description| { + this.child( + div() + .pr_3() + .text_xs() + .text_color(cx.theme().text_muted) + .child(description), + ) + }), + ) } }