@@ -1,54 +1,117 @@
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
use std::{rc::Rc, time::Duration};
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
actions, anchored, div, point, px, relative, Animation, AnimationExt as _, AnyElement, App,
|
||||
Bounds, ClickEvent, Div, FocusHandle, InteractiveElement, IntoElement, KeyBinding, MouseButton,
|
||||
ParentElement, Pixels, Point, RenderOnce, SharedString, Styled, Window,
|
||||
anchored, div, point, prelude::FluentBuilder, px, relative, Animation, AnimationExt as _,
|
||||
AnyElement, App, Bounds, ClickEvent, Div, FocusHandle, InteractiveElement, IntoElement,
|
||||
KeyBinding, MouseButton, ParentElement, Pixels, Point, RenderOnce, SharedString, Styled,
|
||||
Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::animation::cubic_bezier;
|
||||
use crate::button::{Button, ButtonCustomVariant, ButtonVariants as _};
|
||||
use crate::{v_flex, ContextModal, IconName, StyledExt};
|
||||
|
||||
actions!(modal, [Escape]);
|
||||
use crate::{
|
||||
actions::{Cancel, Confirm},
|
||||
animation::cubic_bezier,
|
||||
button::{Button, ButtonCustomVariant, ButtonVariant, ButtonVariants as _},
|
||||
h_flex, v_flex, ContextModal, IconName, Root, StyledExt,
|
||||
};
|
||||
|
||||
const CONTEXT: &str = "Modal";
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
cx.bind_keys([KeyBinding::new("escape", Escape, Some(CONTEXT))])
|
||||
cx.bind_keys([
|
||||
KeyBinding::new("escape", Cancel, Some(CONTEXT)),
|
||||
KeyBinding::new("enter", Confirm { secondary: false }, Some(CONTEXT)),
|
||||
]);
|
||||
}
|
||||
|
||||
type OnClose = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;
|
||||
type OnOk = Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static>>;
|
||||
type OnCancel = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static>;
|
||||
type RenderButtonFn = Box<dyn FnOnce(&mut Window, &mut App) -> AnyElement>;
|
||||
type FooterFn =
|
||||
Box<dyn Fn(RenderButtonFn, RenderButtonFn, &mut Window, &mut App) -> Vec<AnyElement>>;
|
||||
|
||||
/// Modal button props.
|
||||
pub struct ModalButtonProps {
|
||||
ok_text: Option<SharedString>,
|
||||
ok_variant: ButtonVariant,
|
||||
cancel_text: Option<SharedString>,
|
||||
cancel_variant: ButtonVariant,
|
||||
}
|
||||
|
||||
impl Default for ModalButtonProps {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
ok_text: None,
|
||||
ok_variant: ButtonVariant::Primary,
|
||||
cancel_text: None,
|
||||
cancel_variant: ButtonVariant::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ModalButtonProps {
|
||||
/// Sets the text of the OK button. Default is `OK`.
|
||||
pub fn ok_text(mut self, ok_text: impl Into<SharedString>) -> Self {
|
||||
self.ok_text = Some(ok_text.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the variant of the OK button. Default is `ButtonVariant::Primary`.
|
||||
pub fn ok_variant(mut self, ok_variant: ButtonVariant) -> Self {
|
||||
self.ok_variant = ok_variant;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the text of the Cancel button. Default is `Cancel`.
|
||||
pub fn cancel_text(mut self, cancel_text: impl Into<SharedString>) -> Self {
|
||||
self.cancel_text = Some(cancel_text.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the variant of the Cancel button. Default is `ButtonVariant::default()`.
|
||||
pub fn cancel_variant(mut self, cancel_variant: ButtonVariant) -> Self {
|
||||
self.cancel_variant = cancel_variant;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Modal {
|
||||
base: Div,
|
||||
title: Option<AnyElement>,
|
||||
footer: Option<AnyElement>,
|
||||
footer: Option<FooterFn>,
|
||||
content: Div,
|
||||
width: Pixels,
|
||||
max_width: Option<Pixels>,
|
||||
margin_top: Option<Pixels>,
|
||||
|
||||
on_close: OnClose,
|
||||
closable: bool,
|
||||
on_ok: OnOk,
|
||||
on_cancel: OnCancel,
|
||||
button_props: ModalButtonProps,
|
||||
show_close: bool,
|
||||
overlay: bool,
|
||||
overlay_closable: bool,
|
||||
keyboard: bool,
|
||||
|
||||
/// This will be change when open the modal, the focus handle is create when open the modal.
|
||||
pub(crate) focus_handle: FocusHandle,
|
||||
pub(crate) layer_ix: usize,
|
||||
pub(crate) overlay: bool,
|
||||
pub(crate) overlay_visible: bool,
|
||||
}
|
||||
|
||||
impl Modal {
|
||||
pub fn new(_window: &mut Window, cx: &mut App) -> Self {
|
||||
let radius = (cx.theme().radius * 2.).min(px(20.));
|
||||
|
||||
let base = v_flex()
|
||||
.bg(cx.theme().background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().border)
|
||||
.rounded_xl()
|
||||
.shadow_md();
|
||||
.rounded(radius)
|
||||
.shadow_xl()
|
||||
.min_h_24();
|
||||
|
||||
Self {
|
||||
base,
|
||||
@@ -61,9 +124,14 @@ impl Modal {
|
||||
max_width: None,
|
||||
overlay: true,
|
||||
keyboard: true,
|
||||
closable: true,
|
||||
layer_ix: 0,
|
||||
overlay_visible: false,
|
||||
on_close: Rc::new(|_, _, _| {}),
|
||||
on_ok: None,
|
||||
on_cancel: Rc::new(|_, _, _| true),
|
||||
button_props: ModalButtonProps::default(),
|
||||
show_close: true,
|
||||
overlay_closable: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,12 +142,54 @@ impl Modal {
|
||||
}
|
||||
|
||||
/// Set the footer of the modal.
|
||||
pub fn footer(mut self, footer: impl IntoElement) -> Self {
|
||||
self.footer = Some(footer.into_any_element());
|
||||
///
|
||||
/// The `footer` is a function that takes two `RenderButtonFn` and a `WindowContext` and returns a list of `AnyElement`.
|
||||
///
|
||||
/// - First `RenderButtonFn` is the render function for the OK button.
|
||||
/// - Second `RenderButtonFn` is the render function for the CANCEL button.
|
||||
///
|
||||
/// When you set the footer, the footer will be placed default footer buttons.
|
||||
pub fn footer<E, F>(mut self, footer: F) -> Self
|
||||
where
|
||||
E: IntoElement,
|
||||
F: Fn(RenderButtonFn, RenderButtonFn, &mut Window, &mut App) -> Vec<E> + 'static,
|
||||
{
|
||||
self.footer = Some(Box::new(move |ok, cancel, window, cx| {
|
||||
footer(ok, cancel, window, cx)
|
||||
.into_iter()
|
||||
.map(|e| e.into_any_element())
|
||||
.collect()
|
||||
}));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set to use confirm modal, with OK and Cancel buttons.
|
||||
///
|
||||
/// See also [`Self::alert`]
|
||||
pub fn confirm(self) -> Self {
|
||||
self.footer(|ok, cancel, window, cx| vec![cancel(window, cx), ok(window, cx)])
|
||||
.overlay_closable(false)
|
||||
.show_close(false)
|
||||
}
|
||||
|
||||
/// Set to as a alter modal, with OK button.
|
||||
///
|
||||
/// See also [`Self::confirm`]
|
||||
pub fn alert(self) -> Self {
|
||||
self.footer(|ok, _, window, cx| vec![ok(window, cx)])
|
||||
.overlay_closable(false)
|
||||
.show_close(false)
|
||||
}
|
||||
|
||||
/// Set the button props of the modal.
|
||||
pub fn button_props(mut self, button_props: ModalButtonProps) -> Self {
|
||||
self.button_props = button_props;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the callback for when the modal is closed.
|
||||
///
|
||||
/// Called after [`Self::on_ok`] or [`Self::on_cancel`] callback.
|
||||
pub fn on_close(
|
||||
mut self,
|
||||
on_close: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
@@ -88,9 +198,31 @@ impl Modal {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the false to make modal unclosable, default: true
|
||||
pub fn closable(mut self, closable: bool) -> Self {
|
||||
self.closable = closable;
|
||||
/// Sets the callback for when the modal is has been confirmed.
|
||||
///
|
||||
/// The callback should return `true` to close the modal, if return `false` the modal will not be closed.
|
||||
pub fn on_ok(
|
||||
mut self,
|
||||
on_ok: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
|
||||
) -> Self {
|
||||
self.on_ok = Some(Rc::new(on_ok));
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the callback for when the modal is has been canceled.
|
||||
///
|
||||
/// The callback should return `true` to close the modal, if return `false` the modal will not be closed.
|
||||
pub fn on_cancel(
|
||||
mut self,
|
||||
on_cancel: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
|
||||
) -> Self {
|
||||
self.on_cancel = Rc::new(on_cancel);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the false to hide close icon, default: true
|
||||
pub fn show_close(mut self, show_close: bool) -> Self {
|
||||
self.show_close = show_close;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -118,6 +250,14 @@ impl Modal {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the overlay closable of the modal, defaults to `true`.
|
||||
///
|
||||
/// When the overlay is clicked, the modal will be closed.
|
||||
pub fn overlay_closable(mut self, overlay_closable: bool) -> Self {
|
||||
self.overlay_closable = overlay_closable;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set whether to support keyboard esc to close the modal, defaults to `true`.
|
||||
pub fn keyboard(mut self, keyboard: bool) -> Self {
|
||||
self.keyboard = keyboard;
|
||||
@@ -145,6 +285,64 @@ impl RenderOnce for Modal {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl gpui::IntoElement {
|
||||
let layer_ix = self.layer_ix;
|
||||
let on_close = self.on_close.clone();
|
||||
let on_ok = self.on_ok.clone();
|
||||
let on_cancel = self.on_cancel.clone();
|
||||
|
||||
let render_ok: RenderButtonFn = Box::new({
|
||||
let on_ok = on_ok.clone();
|
||||
let on_close = on_close.clone();
|
||||
let ok_text = self.button_props.ok_text.unwrap_or_else(|| "Ok".into());
|
||||
let ok_variant = self.button_props.ok_variant;
|
||||
move |_, _| {
|
||||
Button::new("ok")
|
||||
.label(ok_text)
|
||||
.with_variant(ok_variant)
|
||||
.on_click({
|
||||
let on_ok = on_ok.clone();
|
||||
let on_close = on_close.clone();
|
||||
|
||||
move |_, window, cx| {
|
||||
if let Some(on_ok) = &on_ok {
|
||||
if !on_ok(&ClickEvent::default(), window, cx) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
on_close(&ClickEvent::default(), window, cx);
|
||||
window.close_modal(cx);
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
});
|
||||
let render_cancel: RenderButtonFn = Box::new({
|
||||
let on_cancel = on_cancel.clone();
|
||||
let on_close = on_close.clone();
|
||||
let cancel_text = self
|
||||
.button_props
|
||||
.cancel_text
|
||||
.unwrap_or_else(|| "Cancel".into());
|
||||
let cancel_variant = self.button_props.cancel_variant;
|
||||
move |_, _| {
|
||||
Button::new("cancel")
|
||||
.label(cancel_text)
|
||||
.with_variant(cancel_variant)
|
||||
.on_click({
|
||||
let on_cancel = on_cancel.clone();
|
||||
let on_close = on_close.clone();
|
||||
move |_, window, cx| {
|
||||
if !on_cancel(&ClickEvent::default(), window, cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
on_close(&ClickEvent::default(), window, cx);
|
||||
window.close_modal(cx);
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
});
|
||||
|
||||
let window_paddings = crate::window_border::window_paddings(window, cx);
|
||||
let view_size = window.viewport_size()
|
||||
- gpui::size(
|
||||
@@ -155,8 +353,8 @@ impl RenderOnce for Modal {
|
||||
origin: Point::default(),
|
||||
size: view_size,
|
||||
};
|
||||
let offset_top = px(layer_ix as f32 * 2.);
|
||||
let y = self.margin_top.unwrap_or(view_size.height / 16.) + offset_top;
|
||||
let offset_top = px(layer_ix as f32 * 16.);
|
||||
let y = self.margin_top.unwrap_or(view_size.height / 10.) + offset_top;
|
||||
let x = bounds.center().x - self.width / 2.;
|
||||
|
||||
anchored()
|
||||
@@ -164,14 +362,22 @@ impl RenderOnce for Modal {
|
||||
.snap_to_window()
|
||||
.child(
|
||||
div()
|
||||
.occlude()
|
||||
.w(view_size.width)
|
||||
.h(view_size.height)
|
||||
.when(self.overlay, |this| this.bg(cx.theme().overlay))
|
||||
.when(self.keyboard, |this| {
|
||||
.when(self.overlay_visible, |this| {
|
||||
this.occlude().bg(cx.theme().overlay)
|
||||
})
|
||||
.when(self.overlay_closable, |this| {
|
||||
// Only the last modal owns the `mouse down - close modal` event.
|
||||
if (self.layer_ix + 1) != Root::read(window, cx).active_modals.len() {
|
||||
return this;
|
||||
}
|
||||
|
||||
this.on_mouse_down(MouseButton::Left, {
|
||||
let on_close = self.on_close.clone();
|
||||
let on_cancel = on_cancel.clone();
|
||||
let on_close = on_close.clone();
|
||||
move |_, window, cx| {
|
||||
on_cancel(&ClickEvent::default(), window, cx);
|
||||
on_close(&ClickEvent::default(), window, cx);
|
||||
window.close_modal(cx);
|
||||
}
|
||||
@@ -182,8 +388,39 @@ impl RenderOnce for Modal {
|
||||
.id(SharedString::from(format!("modal-{layer_ix}")))
|
||||
.key_context(CONTEXT)
|
||||
.track_focus(&self.focus_handle)
|
||||
.when(self.keyboard, |this| {
|
||||
this.on_action({
|
||||
let on_cancel = on_cancel.clone();
|
||||
let on_close = on_close.clone();
|
||||
move |_: &Cancel, window, cx| {
|
||||
// FIXME:
|
||||
//
|
||||
// Here some Modal have no focus_handle, so it will not work will Escape key.
|
||||
// But by now, we `cx.close_modal()` going to close the last active model, so the Escape is unexpected to work.
|
||||
on_cancel(&ClickEvent::default(), window, cx);
|
||||
on_close(&ClickEvent::default(), window, cx);
|
||||
window.close_modal(cx);
|
||||
}
|
||||
})
|
||||
.on_action({
|
||||
let on_ok = on_ok.clone();
|
||||
let on_close = on_close.clone();
|
||||
let has_footer = self.footer.is_some();
|
||||
move |_: &Confirm, window, cx| {
|
||||
if let Some(on_ok) = &on_ok {
|
||||
if on_ok(&ClickEvent::default(), window, cx) {
|
||||
on_close(&ClickEvent::default(), window, cx);
|
||||
window.close_modal(cx);
|
||||
}
|
||||
} else if has_footer {
|
||||
window.close_modal(cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
.absolute()
|
||||
.occlude()
|
||||
.relative()
|
||||
.left(x)
|
||||
.top(y)
|
||||
.w(self.width)
|
||||
@@ -203,7 +440,7 @@ impl RenderOnce for Modal {
|
||||
.child(title),
|
||||
)
|
||||
})
|
||||
.when(self.closable, |this| {
|
||||
.when(self.show_close, |this| {
|
||||
this.child(
|
||||
Button::new(SharedString::from(format!(
|
||||
"modal-close-{layer_ix}"
|
||||
@@ -221,26 +458,23 @@ impl RenderOnce for Modal {
|
||||
)
|
||||
.on_click(
|
||||
move |_, window, cx| {
|
||||
on_cancel(&ClickEvent::default(), window, cx);
|
||||
on_close(&ClickEvent::default(), window, cx);
|
||||
window.close_modal(cx);
|
||||
},
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(self.content)
|
||||
.children(self.footer)
|
||||
.when(self.keyboard, |this| {
|
||||
this.on_action({
|
||||
let on_close = self.on_close.clone();
|
||||
move |_: &Escape, window, cx| {
|
||||
// FIXME:
|
||||
//
|
||||
// Here some Modal have no focus_handle, so it will not work will Escape key.
|
||||
// But by now, we `cx.close_modal()` going to close the last active model, so the Escape is unexpected to work.
|
||||
on_close(&ClickEvent::default(), window, cx);
|
||||
window.close_modal(cx);
|
||||
}
|
||||
})
|
||||
.child(div().w_full().flex_1().child(self.content))
|
||||
.when(self.footer.is_some(), |this| {
|
||||
let footer = self.footer.unwrap();
|
||||
|
||||
this.child(h_flex().gap_2().justify_end().children(footer(
|
||||
render_ok,
|
||||
render_cancel,
|
||||
window,
|
||||
cx,
|
||||
)))
|
||||
})
|
||||
.with_animation(
|
||||
"slide-down",
|
||||
|
||||
@@ -128,7 +128,7 @@ impl ContextModal for Window {
|
||||
type Builder = Rc<dyn Fn(Modal, &mut Window, &mut App) -> Modal + 'static>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ActiveModal {
|
||||
pub(crate) struct ActiveModal {
|
||||
focus_handle: FocusHandle,
|
||||
builder: Builder,
|
||||
}
|
||||
@@ -137,7 +137,7 @@ pub struct ActiveModal {
|
||||
///
|
||||
/// It is used to manage the Modal, and Notification.
|
||||
pub struct Root {
|
||||
pub active_modals: Vec<ActiveModal>,
|
||||
pub(crate) active_modals: Vec<ActiveModal>,
|
||||
pub notification: Entity<NotificationList>,
|
||||
pub focused_input: Option<Entity<InputState>>,
|
||||
/// Used to store the focus handle of the previous view.
|
||||
@@ -194,36 +194,46 @@ impl Root {
|
||||
/// Render the Modal layer.
|
||||
pub fn render_modal_layer(window: &mut Window, cx: &mut App) -> Option<impl IntoElement> {
|
||||
let root = window.root::<Root>()??;
|
||||
|
||||
let active_modals = root.read(cx).active_modals.clone();
|
||||
let mut has_overlay = false;
|
||||
|
||||
if active_modals.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(
|
||||
div().children(active_modals.iter().enumerate().map(|(i, active_modal)| {
|
||||
let mut show_overlay_ix = None;
|
||||
|
||||
let mut modals = active_modals
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, active_modal)| {
|
||||
let mut modal = Modal::new(window, cx);
|
||||
|
||||
modal = (active_modal.builder)(modal, window, cx);
|
||||
modal.layer_ix = i;
|
||||
|
||||
// Give the modal the focus handle, because `modal` is a temporary value, is not possible to
|
||||
// keep the focus handle in the modal.
|
||||
//
|
||||
// So we keep the focus handle in the `active_modal`, this is owned by the `Root`.
|
||||
modal.focus_handle = active_modal.focus_handle.clone();
|
||||
|
||||
// Keep only have one overlay, we only render the first modal with overlay.
|
||||
if has_overlay {
|
||||
modal.overlay = false;
|
||||
}
|
||||
modal.layer_ix = i;
|
||||
// Find the modal which one needs to show overlay.
|
||||
if modal.has_overlay() {
|
||||
has_overlay = true;
|
||||
show_overlay_ix = Some(i);
|
||||
}
|
||||
|
||||
modal
|
||||
})),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if let Some(ix) = show_overlay_ix {
|
||||
if let Some(modal) = modals.get_mut(ix) {
|
||||
modal.overlay_visible = true;
|
||||
}
|
||||
}
|
||||
|
||||
Some(div().children(modals))
|
||||
}
|
||||
|
||||
/// Return the root view of the Root.
|
||||
|
||||
@@ -4,13 +4,13 @@ use std::time::Duration;
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
div, px, Animation, AnimationExt as _, AnyElement, App, Element, ElementId, GlobalElementId,
|
||||
InteractiveElement, IntoElement, LayoutId, ParentElement as _, SharedString, Styled as _,
|
||||
Window,
|
||||
div, px, white, Animation, AnimationExt as _, AnyElement, App, Element, ElementId,
|
||||
GlobalElementId, InteractiveElement, IntoElement, LayoutId, ParentElement as _, SharedString,
|
||||
Styled as _, Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::{h_flex, Disableable, Side, Sizable, Size};
|
||||
use crate::{Disableable, Side, Sizable, Size};
|
||||
|
||||
type OnClick = Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>;
|
||||
|
||||
@@ -19,6 +19,7 @@ pub struct Switch {
|
||||
checked: bool,
|
||||
disabled: bool,
|
||||
label: Option<SharedString>,
|
||||
description: Option<SharedString>,
|
||||
label_side: Side,
|
||||
on_click: OnClick,
|
||||
size: Size,
|
||||
@@ -27,13 +28,15 @@ pub struct Switch {
|
||||
impl Switch {
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
let id: ElementId = id.into();
|
||||
|
||||
Self {
|
||||
id: id.clone(),
|
||||
checked: false,
|
||||
disabled: false,
|
||||
label: None,
|
||||
description: None,
|
||||
on_click: None,
|
||||
label_side: Side::Right,
|
||||
label_side: Side::Left,
|
||||
size: Size::Medium,
|
||||
}
|
||||
}
|
||||
@@ -48,6 +51,11 @@ impl Switch {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn description(mut self, description: impl Into<SharedString>) -> Self {
|
||||
self.description = Some(description.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_click<F>(mut self, handler: F) -> Self
|
||||
where
|
||||
F: Fn(&bool, &mut Window, &mut App) + 'static,
|
||||
@@ -116,8 +124,8 @@ impl Element for Switch {
|
||||
let on_click = self.on_click.clone();
|
||||
|
||||
let (bg, toggle_bg) = match self.checked {
|
||||
true => (theme.icon_accent, theme.background),
|
||||
false => (theme.element_background, theme.background),
|
||||
true => (theme.element_background, white()),
|
||||
false => (theme.elevated_surface_background, white()),
|
||||
};
|
||||
|
||||
let (bg, toggle_bg) = match self.disabled {
|
||||
@@ -138,74 +146,98 @@ impl Element for Switch {
|
||||
let inset = px(2.);
|
||||
|
||||
let mut element = div()
|
||||
.flex()
|
||||
.child(
|
||||
h_flex()
|
||||
div()
|
||||
.id(self.id.clone())
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.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(bg_height / 2.)
|
||||
.w_full()
|
||||
.flex()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.border(inset)
|
||||
.border_color(theme.border_transparent)
|
||||
.bg(bg)
|
||||
.when(!self.disabled, |this| this.cursor_pointer())
|
||||
.gap_4()
|
||||
.when_some(self.label.clone(), |this, label| {
|
||||
// Label
|
||||
this.child(
|
||||
div().text_sm().text_color(cx.theme().text).child(label),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
// Switch Toggle
|
||||
div().rounded_full().bg(toggle_bg).size(bar_width).map(
|
||||
|this| {
|
||||
let prev_checked = state.prev_checked.clone();
|
||||
if !self.disabled
|
||||
&& prev_checked
|
||||
.borrow()
|
||||
.is_some_and(|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| {
|
||||
// Switch Bar
|
||||
div()
|
||||
.id(self.id.clone())
|
||||
.flex_shrink_0()
|
||||
.w(bg_width)
|
||||
.h(bg_height)
|
||||
.rounded(bg_height / 2.)
|
||||
.flex()
|
||||
.items_center()
|
||||
.border(inset)
|
||||
.border_color(theme.border_transparent)
|
||||
.bg(bg)
|
||||
.when(!self.disabled, |this| this.cursor_pointer())
|
||||
.child(
|
||||
// Switch Toggle
|
||||
div()
|
||||
.rounded_full()
|
||||
.shadow_sm()
|
||||
.bg(toggle_bg)
|
||||
.size(bar_width)
|
||||
.map(|this| {
|
||||
let prev_checked = state.prev_checked.clone();
|
||||
if !self.disabled
|
||||
&& prev_checked
|
||||
.borrow()
|
||||
.is_some_and(|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 * 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()
|
||||
}
|
||||
},
|
||||
),
|
||||
let x =
|
||||
if checked { max_x } else { px(0.) };
|
||||
this.left(x).into_any_element()
|
||||
}
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.when_some(self.label.clone(), |this, label| {
|
||||
this.child(div().child(label).map(|this| match self.size {
|
||||
Size::XSmall | Size::Small => this.text_sm(),
|
||||
_ => this.text_base(),
|
||||
}))
|
||||
.when_some(self.description.clone(), |this, description| {
|
||||
this.child(
|
||||
div()
|
||||
.w_3_4()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(description),
|
||||
)
|
||||
})
|
||||
.when_some(
|
||||
on_click
|
||||
|
||||
Reference in New Issue
Block a user