From 479ab8f0d67dd11fa1c5b3fdd1a1d35f99e4cb1b Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Thu, 24 Jul 2025 19:22:55 +0800 Subject: [PATCH] switch: Apply `use_keyed_state` to store toggle state for animation. (#1091) Ref https://github.com/zed-industries/zed/pull/34741 Now `Switch` can use impl RenderOnce. --- crates/ui/src/switch.rs | 311 ++++++++++++++++------------------------ 1 file changed, 121 insertions(+), 190 deletions(-) diff --git a/crates/ui/src/switch.rs b/crates/ui/src/switch.rs index a0d555e6..cbf30cff 100644 --- a/crates/ui/src/switch.rs +++ b/crates/ui/src/switch.rs @@ -1,15 +1,15 @@ use crate::{ - h_flex, text::Text, tooltip::Tooltip, ActiveTheme, Colorize, Disableable, Side, Sizable, Size, - StyledExt, + h_flex, text::Text, tooltip::Tooltip, ActiveTheme, Disableable, Side, Sizable, Size, StyledExt, }; use gpui::{ - div, prelude::FluentBuilder as _, px, Animation, AnimationExt as _, AnyElement, App, Element, - ElementId, GlobalElementId, InteractiveElement, IntoElement, LayoutId, ParentElement as _, - SharedString, StatefulInteractiveElement, StyleRefinement, Styled, Window, + div, prelude::FluentBuilder as _, px, Animation, AnimationExt as _, App, ElementId, + InteractiveElement, IntoElement, ParentElement as _, RenderOnce, SharedString, + StatefulInteractiveElement, StyleRefinement, Styled, Window, }; -use std::{cell::RefCell, rc::Rc, time::Duration}; +use std::{rc::Rc, time::Duration}; /// A Switch element that can be toggled on or off. +#[derive(IntoElement)] pub struct Switch { id: ElementId, style: StyleRefinement, @@ -87,198 +87,129 @@ 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(); + let toggle_state = window.use_keyed_state(self.id.clone(), cx, |_, _| checked); - fn into_element(self) -> Self::Element { - self - } -} + let (bg, toggle_bg) = match checked { + true => (cx.theme().primary, cx.theme().background), + false => (cx.theme().switch, cx.theme().background), + }; -#[derive(Default)] -pub struct SwitchState { - prev_checked: Rc>>, -} + let (bg, toggle_bg) = if self.disabled { + ( + if checked { bg.alpha(0.5) } else { bg }, + toggle_bg.alpha(0.35), + ) + } else { + (bg, toggle_bg) + }; -impl Element for Switch { - type RequestLayoutState = AnyElement; + 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 radius = if cx.theme().radius >= px(4.) { + bg_height + } else { + cx.theme().radius + }; - type PrepaintState = (); - - fn id(&self) -> Option { - Some(self.id.clone()) - } - - fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { - None - } - - 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 checked = self.checked; - let on_click = self.on_click.clone(); - - let (bg, toggle_bg) = match self.checked { - true => (cx.theme().primary, cx.theme().background), - false => (cx.theme().switch, cx.theme().background), - }; - - let (bg, toggle_bg) = match self.disabled { - true => { - if self.checked { - (cx.theme().muted.darken(0.05), toggle_bg.opacity(0.8)) - } else { - (cx.theme().muted, 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 radius = if cx.theme().radius >= px(4.) { - bg_height - } else { - cx.theme().radius - }; - - let mut element = div() - .refine_style(&self.style) + div().refine_style(&self.style).child( + h_flex() + .id(self.id.clone()) + .gap_2() + .items_start() + .when(self.label_side.is_left(), |this| this.flex_row_reverse()) .child( - h_flex() + // Switch Bar + div() .id(self.id.clone()) - .gap_2() - .items_start() - .when(self.label_side.is_left(), |this| this.flex_row_reverse()) - .child( - // Switch Bar - div() - .id(self.id.clone()) - .w(bg_width) - .h(bg_height) - .rounded(radius) - .flex() - .items_center() - .border(inset) - .border_color(cx.theme().transparent) - .bg(bg) - .when_some(self.tooltip.clone(), |this, tooltip| { - this.tooltip(move |window, cx| { - Tooltip::new(tooltip.clone()).build(window, cx) - }) - }) - .child( - // Switch Toggle - div() - .rounded(radius) - .bg(toggle_bg) - .shadow_md() - .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(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.label.take(), |this, label| { - this.child(div().line_height(bg_height).child(label).map(|this| { - match self.size { - Size::XSmall | Size::Small => this.text_sm(), - _ => this.text_base(), - } - })) + .w(bg_width) + .h(bg_height) + .rounded(radius) + .flex() + .items_center() + .border(inset) + .border_color(cx.theme().transparent) + .bg(bg) + .when_some(self.tooltip.clone(), |this, tooltip| { + this.tooltip(move |window, cx| { + Tooltip::new(tooltip.clone()).build(window, cx) + }) }) - .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 Toggle + div() + .rounded(radius) + .bg(toggle_bg) + .shadow_md() + .size(bar_width) + .map(|this| { + let prev_checked = toggle_state.read(cx); + if !self.disabled && *prev_checked != checked { + let duration = Duration::from_secs_f64(0.15); + cx.spawn({ + let toggle_state = toggle_state.clone(); + async move |cx| { + cx.background_executor().timer(duration).await; + _ = toggle_state + .update(cx, |this, _| *this = checked); + } + }) + .detach(); + + this.with_animation( + ElementId::NamedInteger("move".into(), checked as u64), + Animation::new(duration), + 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() + } + }), ), ) - .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.label, |this, label| { + this.child(div().line_height(bg_height).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 toggle_state = toggle_state.clone(); + this.on_mouse_down(gpui::MouseButton::Left, move |_, window, cx| { + cx.stop_propagation(); + _ = toggle_state.update(cx, |this, _| *this = checked); + on_click(&!checked, window, cx); + }) + }, + ), + ) } }