check, radio: Add fade in animation to Checkbox and Radio. (#1092)
This commit is contained in:
parent
479ab8f0d6
commit
37aa26d655
2 changed files with 109 additions and 53 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in a new issue