check, radio: Add fade in animation to Checkbox and Radio. (#1092)

This commit is contained in:
Jason Lee 2025-07-24 19:41:34 +08:00 committed by GitHub
parent 479ab8f0d6
commit 37aa26d655
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 109 additions and 53 deletions

View file

@ -1,11 +1,13 @@
use std::time::Duration;
use crate::{ use crate::{
text::Text, v_flex, ActiveTheme, Disableable, IconName, Selectable, Sizable, Size, text::Text, v_flex, ActiveTheme, Disableable, IconName, Selectable, Sizable, Size,
StyledExt as _, StyledExt as _,
}; };
use gpui::{ use gpui::{
div, prelude::FluentBuilder as _, px, relative, rems, svg, AnyElement, App, Div, ElementId, div, prelude::FluentBuilder as _, px, relative, rems, svg, Animation, AnimationExt, AnyElement,
InteractiveElement, IntoElement, ParentElement, RenderOnce, StatefulInteractiveElement, App, Div, ElementId, InteractiveElement, IntoElement, ParentElement, RenderOnce,
StyleRefinement, Styled, Window, StatefulInteractiveElement, StyleRefinement, Styled, Window,
}; };
/// A Checkbox element. /// A Checkbox element.
@ -100,26 +102,81 @@ impl Sizable for Checkbox {
} }
} }
pub(crate) fn checkbox_check_icon(
id: ElementId,
size: Size,
checked: bool,
disabled: bool,
window: &mut Window,
cx: &mut App,
) -> impl IntoElement {
let toggle_state = window.use_keyed_state(id, cx, |_, _| checked);
let color = if disabled {
cx.theme().primary_foreground.opacity(0.5)
} else {
cx.theme().primary_foreground
};
svg()
.absolute()
.top_px()
.left_px()
.map(|this| match size {
Size::XSmall => this.size_2(),
Size::Small => this.size_2p5(),
Size::Medium => this.size_3(),
Size::Large => this.size_3p5(),
_ => this.size_3(),
})
.text_color(color)
.map(|this| match checked {
true => this.path(IconName::Check.path()),
_ => this,
})
.map(|this| {
if !disabled && checked != *toggle_state.read(cx) {
let duration = Duration::from_secs_f64(0.25);
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("toggle".into(), checked as u64),
Animation::new(Duration::from_secs_f64(0.25)),
move |this, delta| {
this.opacity(if checked { 1.0 * delta } else { 1.0 - delta })
},
)
.into_any_element()
} else {
this.into_any_element()
}
})
}
impl RenderOnce for Checkbox { impl RenderOnce for Checkbox {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let border_color = if self.checked { let checked = self.checked;
let border_color = if checked {
cx.theme().primary cx.theme().primary
} else { } else {
cx.theme().input cx.theme().input
}; };
let (color, icon_color) = if self.disabled { let color = if self.disabled {
( border_color.opacity(0.5)
border_color.opacity(0.5),
cx.theme().primary_foreground.opacity(0.5),
)
} else { } else {
(border_color, cx.theme().primary_foreground) border_color
}; };
let radius = cx.theme().radius.min(px(4.)); let radius = cx.theme().radius.min(px(4.));
div().child( div().child(
self.base self.base
.id(self.id) .id(self.id.clone())
.h_flex() .h_flex()
.gap_2() .gap_2()
.items_start() .items_start()
@ -155,24 +212,14 @@ impl RenderOnce for Checkbox {
false => this.bg(cx.theme().background), false => this.bg(cx.theme().background),
_ => this.bg(color), _ => this.bg(color),
}) })
.child( .child(checkbox_check_icon(
svg() self.id,
.absolute() self.size,
.top_px() checked,
.left_px() self.disabled,
.map(|this| match self.size { window,
Size::XSmall => this.size_2(), cx,
Size::Small => this.size_2p5(), )),
Size::Medium => this.size_3(),
Size::Large => this.size_3p5(),
_ => this.size_3(),
})
.text_color(icon_color)
.map(|this| match self.checked {
true => this.path(IconName::Check.path()),
_ => this,
}),
),
) )
.when(self.label.is_some() || !self.children.is_empty(), |this| { .when(self.label.is_some() || !self.children.is_empty(), |this| {
this.child( this.child(

View file

@ -1,8 +1,11 @@
use std::rc::Rc; use std::rc::Rc;
use crate::{h_flex, text::Text, v_flex, ActiveTheme, AxisExt, IconName, StyledExt}; use crate::{
checkbox::checkbox_check_icon, h_flex, text::Text, v_flex, ActiveTheme, AxisExt, Sizable, Size,
StyledExt,
};
use gpui::{ use gpui::{
div, prelude::FluentBuilder, relative, svg, AnyElement, App, Axis, Div, ElementId, div, prelude::FluentBuilder, relative, rems, AnyElement, App, Axis, Div, ElementId,
InteractiveElement, IntoElement, ParentElement, RenderOnce, SharedString, InteractiveElement, IntoElement, ParentElement, RenderOnce, SharedString,
StatefulInteractiveElement, StyleRefinement, Styled, Window, StatefulInteractiveElement, StyleRefinement, Styled, Window,
}; };
@ -19,6 +22,7 @@ pub struct Radio {
children: Vec<AnyElement>, children: Vec<AnyElement>,
checked: bool, checked: bool,
disabled: bool, disabled: bool,
size: Size,
on_click: Option<Box<dyn Fn(&bool, &mut Window, &mut App) + 'static>>, on_click: Option<Box<dyn Fn(&bool, &mut Window, &mut App) + 'static>>,
} }
@ -32,6 +36,7 @@ impl Radio {
children: Vec::new(), children: Vec::new(),
checked: false, checked: false,
disabled: false, disabled: false,
size: Size::default(),
on_click: None, on_click: None,
} }
} }
@ -57,6 +62,13 @@ impl Radio {
} }
} }
impl Sizable for Radio {
fn with_size(mut self, size: impl Into<Size>) -> Self {
self.size = size.into();
self
}
}
impl Styled for Radio { impl Styled for Radio {
fn style(&mut self) -> &mut gpui::StyleRefinement { fn style(&mut self) -> &mut gpui::StyleRefinement {
&mut self.style &mut self.style
@ -76,13 +88,16 @@ impl ParentElement for Radio {
} }
impl RenderOnce for Radio { impl RenderOnce for Radio {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let (border_color, bg) = if self.checked { let checked = self.checked;
let disabled = self.disabled;
let (border_color, bg) = if checked {
(cx.theme().primary, cx.theme().primary) (cx.theme().primary, cx.theme().primary)
} else { } else {
(cx.theme().input, cx.theme().input.opacity(0.3)) (cx.theme().input, cx.theme().input.opacity(0.3))
}; };
let (border_color, bg) = if self.disabled { let (border_color, bg) = if disabled {
(border_color.opacity(0.5), bg.opacity(0.5)) (border_color.opacity(0.5), bg.opacity(0.5))
} else { } else {
(border_color, bg) (border_color, bg)
@ -92,7 +107,7 @@ impl RenderOnce for Radio {
div().child( div().child(
self.base self.base
.h_flex() .h_flex()
.id(self.id) .id(self.id.clone())
.gap_x_2() .gap_x_2()
.text_color(cx.theme().foreground) .text_color(cx.theme().foreground)
.items_start() .items_start()
@ -101,31 +116,25 @@ impl RenderOnce for Radio {
.child( .child(
div() div()
.relative() .relative()
.size_4() .map(|this| match self.size {
Size::XSmall => this.size_3(),
Size::Small => this.size_3p5(),
Size::Medium => this.size_4(),
Size::Large => this.size(rems(1.125)),
_ => this.size_4(),
})
.flex_shrink_0() .flex_shrink_0()
.rounded_full() .rounded_full()
.border_1() .border_1()
.border_color(border_color) .border_color(border_color)
.when(cx.theme().shadow && !self.disabled, |this| this.shadow_xs()) .when(cx.theme().shadow && !disabled, |this| this.shadow_xs())
.map(|this| match self.checked { .map(|this| match self.checked {
false => this.bg(cx.theme().background), false => this.bg(cx.theme().background),
_ => this.bg(bg), _ => this.bg(bg),
}) })
.child( .child(checkbox_check_icon(
svg() self.id, self.size, checked, disabled, window, cx,
.absolute() )),
.top_px()
.left_px()
.size_3()
.text_color(bg)
.when(self.checked, |this| {
this.text_color(cx.theme().primary_foreground)
})
.map(|this| match self.checked {
true => this.path(IconName::Check.path()),
false => this,
}),
),
) )
.child( .child(
v_flex() v_flex()