diff --git a/README.md b/README.md index cdf64a04..3f027352 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ A UI components for building desktop application by using [GPUI](https://gpui.rs - [ ] RadioGroup - [x] Switch - [x] With Label (Left, Right side) - - [ ] Toggle Animation + - [x] Toggle Animation - [x] Dropdown - [x] Tabs - [x] Tab diff --git a/crates/story/src/picker_story.rs b/crates/story/src/picker_story.rs index b22196f7..9ce60bb5 100644 --- a/crates/story/src/picker_story.rs +++ b/crates/story/src/picker_story.rs @@ -2,9 +2,9 @@ use std::{sync::Arc, time::Duration}; use fake::Fake; use gpui::{ - deferred, div, prelude::FluentBuilder as _, px, FocusHandle, FocusableView, - InteractiveElement as _, IntoElement, ParentElement, Render, Styled, Task, Timer, View, - ViewContext, VisualContext as _, WeakView, WindowContext, + deferred, div, prelude::FluentBuilder as _, px, Animation, AnimationExt as _, FocusHandle, + FocusableView, InteractiveElement as _, IntoElement, ParentElement, Render, Styled, Task, + Timer, View, ViewContext, VisualContext as _, WeakView, WindowContext, }; use ui::{ @@ -261,20 +261,33 @@ impl Render for PickerStory { }) .when(self.open, |this| { this.child(deferred( - div().absolute().size_full().top_0().left_0().child( - v_flex().flex().flex_col().items_center().child( - v_flex() - .occlude() - .w(px(450.)) - .h(px(350.)) - .elevation_3(cx) - .child(self.list.clone()) - .on_mouse_down_out(cx.listener(|this, _, cx| { - this.open = false; - cx.notify(); - })), + div() + .absolute() + .size_full() + .top_0() + .left_0() + .child( + v_flex().flex().flex_col().items_center().child( + v_flex() + .occlude() + .w(px(450.)) + .h(px(350.)) + .elevation_3(cx) + .child(self.list.clone()) + .on_mouse_down_out(cx.listener(|this, _, cx| { + this.open = false; + cx.notify(); + })), + ), + ) + .with_animation( + "slide-down", + Animation::new(Duration::from_secs_f64(0.15)), + move |this, delta| { + let y = px(-10.) + delta * px(10.); + this.top(y).opacity((1.0 * delta + 0.3).min(1.0)) + }, ), - ), )) }) } diff --git a/crates/story/src/switch_story.rs b/crates/story/src/switch_story.rs index 466541c4..bd1b5395 100644 --- a/crates/story/src/switch_story.rs +++ b/crates/story/src/switch_story.rs @@ -112,7 +112,7 @@ impl Render for SwitchStory { ) .child( card(cx).v_flex() - .items_start().child(title("Disabled Switchs")).child( + .items_start().child(title("Small Switchs")).child( h_flex().items_center() .gap_6() .child(Switch::new("switch3").checked(self.switch3).label("Small Size").small().on_click(cx.listener(move |view, checked, cx| { diff --git a/crates/ui/src/popover.rs b/crates/ui/src/popover.rs index 6f4e9949..0b0ada2d 100644 --- a/crates/ui/src/popover.rs +++ b/crates/ui/src/popover.rs @@ -240,16 +240,18 @@ impl Element for Popover { .into_any() } else { let content_view_mut = element_state.content_view.clone(); + let bg_color = cx.theme().popover; + let anchor = view.anchor; deferred( anchored.child( div() .size_full() .occlude() .elevation_2(cx) - .bg(cx.theme().popover) + .bg(bg_color) .border_1() .border_color(cx.theme().border) - .map(|this| match view.anchor { + .map(|this| match anchor { AnchorCorner::TopLeft | AnchorCorner::TopRight => this.top_2(), AnchorCorner::BottomLeft | AnchorCorner::BottomRight => { this.bottom_2() diff --git a/crates/ui/src/skeleton.rs b/crates/ui/src/skeleton.rs index d8f66fa1..f1eaea05 100644 --- a/crates/ui/src/skeleton.rs +++ b/crates/ui/src/skeleton.rs @@ -1,11 +1,11 @@ use std::time::Duration; use gpui::{ - bounce, div, ease_in_out, hsla, px, Animation, AnimationExt, Div, IntoElement, - ParentElement as _, RenderOnce, Styled, + bounce, div, ease_in_out, Animation, AnimationExt, Div, IntoElement, ParentElement as _, + RenderOnce, Styled, }; -use crate::theme::{ActiveTheme, Colorize}; +use crate::{theme::ActiveTheme, StyledExt}; #[derive(IntoElement)] pub struct Skeleton { @@ -28,18 +28,15 @@ impl Styled for Skeleton { impl RenderOnce for Skeleton { fn render(self, cx: &mut gpui::WindowContext) -> impl IntoElement { - let color = cx.theme().skeleton; - div().child( - self.base.with_animation( + self.base.bg(cx.theme().skeleton).with_animation( "skeleton", Animation::new(Duration::from_secs(2)) .repeat() .with_easing(bounce(ease_in_out)), move |this, delta| { let v = 1.0 - delta * 0.5; - let color = color.opacity(v); - this.bg(color) + this.opacity(v) }, ), ) diff --git a/crates/ui/src/styled_ext.rs b/crates/ui/src/styled_ext.rs index 323365b7..6706abca 100644 --- a/crates/ui/src/styled_ext.rs +++ b/crates/ui/src/styled_ext.rs @@ -1,9 +1,9 @@ use crate::{ scroll::{Scrollable, ScrollbarAxis}, - theme::ActiveTheme, + theme::{ActiveTheme, Colorize}, }; use gpui::{ - hsla, point, px, rems, AnyView, Axis, BoxShadow, Element, FocusHandle, Pixels, Styled, + hsla, point, px, rems, AnyView, Axis, BoxShadow, Element, Fill, FocusHandle, Pixels, Styled, WindowContext, }; use smallvec::{smallvec, SmallVec}; @@ -174,6 +174,38 @@ pub trait StyledExt: Styled + Sized { font_weight!(font_bold, BOLD); font_weight!(font_extrabold, EXTRA_BOLD); font_weight!(font_black, BLACK); + + /// Set the opacity of the element. + fn opacity(mut self, opacity: f32) -> Self { + let bg_color = self.style().background.clone(); + let border_color = self.style().border_color; + let box_shadow = self.style().box_shadow.clone(); + + let this = if let Some(bg) = bg_color { + match bg { + Fill::Color(color) => self.bg(color.opacity(opacity)), + } + } else { + self + }; + + let this = if let Some(color) = border_color { + this.border_color(color.opacity(opacity)) + } else { + this + }; + + let this = if let Some(mut shadow) = box_shadow { + for shadow in shadow.iter_mut() { + shadow.color = shadow.color.opacity(opacity); + } + this.shadow(shadow) + } else { + this + }; + + this + } } impl StyledExt for E {} diff --git a/crates/ui/src/switch.rs b/crates/ui/src/switch.rs index 00253036..e1f3af76 100644 --- a/crates/ui/src/switch.rs +++ b/crates/ui/src/switch.rs @@ -1,11 +1,14 @@ +use std::time::Duration; + use crate::{ stack::h_flex, theme::{ActiveTheme, Colorize}, Disableable, Sizable, Size, }; use gpui::{ - div, prelude::FluentBuilder as _, Div, InteractiveElement, IntoElement, ParentElement as _, - RenderOnce, SharedString, Stateful, Styled as _, WindowContext, + div, prelude::FluentBuilder as _, px, Animation, AnimationExt as _, Div, ElementId, + InteractiveElement, IntoElement, ParentElement as _, RenderOnce, SharedString, Stateful, + Styled as _, WindowContext, }; type OnClick = Box; @@ -88,6 +91,7 @@ impl RenderOnce for Switch { fn render(self, cx: &mut gpui::WindowContext) -> impl IntoElement { let theme = cx.theme(); let group_id = format!("switch_group_{:?}", self.id); + let checked = self.checked; let (bg, toggle_bg) = match self.checked { true => (theme.primary, theme.background), @@ -99,6 +103,16 @@ impl RenderOnce for Switch { 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) .group(group_id) @@ -106,29 +120,36 @@ impl RenderOnce for Switch { .gap_2() .when(self.label_side.left(), |this| this.flex_row_reverse()) .child( + // Switch Bar self.base - .map(|this| match self.size { - Size::XSmall | Size::Small => this.w_8().h_4().rounded_lg(), - _ => this.w_11().h_6().rounded_xl(), - }) + .w(bg_width) + .h(bg_height) + .rounded(bg_height / 2.) .flex() .items_center() - .border_2() + .border(inset) .border_color(theme.transparent) .bg(bg) .when(!self.disabled, |this| this.cursor_pointer()) - .map(|this| match self.checked { - true => this.flex_row_reverse(), - false => this, - }) .child( + // Switch Toggle div() .rounded_full() .bg(toggle_bg) - .map(|this| match self.size { - Size::XSmall | Size::Small => this.w_3().h_3(), - _ => this.w_5().h_5(), - }), + .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 + } else { + max_x - max_x * delta + }; + this.left(x) + }, + ), ), ) .when_some(self.label, |this, label| {