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::{
text::Text, v_flex, ActiveTheme, Disableable, IconName, Selectable, Sizable, Size,
StyledExt as _,
};
use gpui::{
div, prelude::FluentBuilder as _, px, relative, rems, svg, AnyElement, App, Div, ElementId,
InteractiveElement, IntoElement, ParentElement, RenderOnce, StatefulInteractiveElement,
StyleRefinement, Styled, Window,
div, prelude::FluentBuilder as _, px, relative, rems, svg, Animation, AnimationExt, AnyElement,
App, Div, ElementId, InteractiveElement, IntoElement, ParentElement, RenderOnce,
StatefulInteractiveElement, StyleRefinement, Styled, Window,
};
/// 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 {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let border_color = if self.checked {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let checked = self.checked;
let border_color = if checked {
cx.theme().primary
} else {
cx.theme().input
};
let (color, icon_color) = if self.disabled {
(
border_color.opacity(0.5),
cx.theme().primary_foreground.opacity(0.5),
)
let color = if self.disabled {
border_color.opacity(0.5)
} else {
(border_color, cx.theme().primary_foreground)
border_color
};
let radius = cx.theme().radius.min(px(4.));
div().child(
self.base
.id(self.id)
.id(self.id.clone())
.h_flex()
.gap_2()
.items_start()
@ -155,24 +212,14 @@ impl RenderOnce for Checkbox {
false => this.bg(cx.theme().background),
_ => this.bg(color),
})
.child(
svg()
.absolute()
.top_px()
.left_px()
.map(|this| match self.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(icon_color)
.map(|this| match self.checked {
true => this.path(IconName::Check.path()),
_ => this,
}),
),
.child(checkbox_check_icon(
self.id,
self.size,
checked,
self.disabled,
window,
cx,
)),
)
.when(self.label.is_some() || !self.children.is_empty(), |this| {
this.child(

View file

@ -1,8 +1,11 @@
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::{
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,
StatefulInteractiveElement, StyleRefinement, Styled, Window,
};
@ -19,6 +22,7 @@ pub struct Radio {
children: Vec<AnyElement>,
checked: bool,
disabled: bool,
size: Size,
on_click: Option<Box<dyn Fn(&bool, &mut Window, &mut App) + 'static>>,
}
@ -32,6 +36,7 @@ impl Radio {
children: Vec::new(),
checked: false,
disabled: false,
size: Size::default(),
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 {
fn style(&mut self) -> &mut gpui::StyleRefinement {
&mut self.style
@ -76,13 +88,16 @@ impl ParentElement for Radio {
}
impl RenderOnce for Radio {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let (border_color, bg) = if self.checked {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let checked = self.checked;
let disabled = self.disabled;
let (border_color, bg) = if checked {
(cx.theme().primary, cx.theme().primary)
} else {
(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))
} else {
(border_color, bg)
@ -92,7 +107,7 @@ impl RenderOnce for Radio {
div().child(
self.base
.h_flex()
.id(self.id)
.id(self.id.clone())
.gap_x_2()
.text_color(cx.theme().foreground)
.items_start()
@ -101,31 +116,25 @@ impl RenderOnce for Radio {
.child(
div()
.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()
.rounded_full()
.border_1()
.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 {
false => this.bg(cx.theme().background),
_ => this.bg(bg),
})
.child(
svg()
.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(checkbox_check_icon(
self.id, self.size, checked, disabled, window, cx,
)),
)
.child(
v_flex()