Add StatusToast & the ToastLayer (#26232)
https://github.com/user-attachments/assets/b16e32e6-46c6-41dc-ab68-1824d288c8c2 This PR adds the first part of our planned extended notification system: StatusToasts. It also makes various updates to ComponentPreview and adds a `Styled` extension in `ui::style::animation` to make it easier to animate styled elements. _**Note**: We will be very, very selective with what elements are allowed to be animated in Zed. Assume PRs adding animation to elements will all need to be manually signed off on by a designer._ ## Status Toast  These are designed to be used for notifying about things that don't require an action to be taken or don't need to be triaged. They are designed to be ignorable, and dismiss themselves automatically after a set time. They can optionally include a single action. Example: When the user enables Vim Mode, that action might let them undo enabling it.  Status Toasts should _not_ be used when an action is required, or for any binary choice. If the user must provide some input, this isn't the right component! ### Out of scope - Toasts should fade over a short time (like AnimationDuration::Fast or Instant) when dismissed - We should visually show when the toast will dismiss. We'll need to pipe the `duration_remaining` from the toast layer -> ActiveToast to do this. - Dismiss any active toast if another notification kind is created, like a Notification or Alert. Release Notes: - N/A --------- Co-authored-by: Cole Miller <m@cole-miller.net>
This commit is contained in:
parent
b8a8b9c699
commit
7e964290bf
24 changed files with 1308 additions and 75 deletions
|
@ -17,6 +17,7 @@ mod label;
|
|||
mod list;
|
||||
mod modal;
|
||||
mod navigable;
|
||||
mod notification;
|
||||
mod numeric_stepper;
|
||||
mod popover;
|
||||
mod popover_menu;
|
||||
|
@ -54,6 +55,7 @@ pub use label::*;
|
|||
pub use list::*;
|
||||
pub use modal::*;
|
||||
pub use navigable::*;
|
||||
pub use notification::*;
|
||||
pub use numeric_stepper::*;
|
||||
pub use popover::*;
|
||||
pub use popover_menu::*;
|
||||
|
|
|
@ -80,7 +80,7 @@ use super::button_icon::ButtonIcon;
|
|||
/// ```
|
||||
///
|
||||
#[derive(IntoElement, IntoComponent)]
|
||||
#[component(scope = "input")]
|
||||
#[component(scope = "Input")]
|
||||
pub struct Button {
|
||||
base: ButtonLike,
|
||||
label: SharedString,
|
||||
|
|
|
@ -14,7 +14,7 @@ pub enum IconButtonShape {
|
|||
}
|
||||
|
||||
#[derive(IntoElement, IntoComponent)]
|
||||
#[component(scope = "input")]
|
||||
#[component(scope = "Input")]
|
||||
pub struct IconButton {
|
||||
base: ButtonLike,
|
||||
shape: IconButtonShape,
|
||||
|
|
|
@ -16,7 +16,7 @@ pub enum ToggleButtonPosition {
|
|||
}
|
||||
|
||||
#[derive(IntoElement, IntoComponent)]
|
||||
#[component(scope = "input")]
|
||||
#[component(scope = "Input")]
|
||||
pub struct ToggleButton {
|
||||
base: ButtonLike,
|
||||
position_in_group: Option<ToggleButtonPosition>,
|
||||
|
|
|
@ -24,7 +24,7 @@ pub fn h_container() -> ContentGroup {
|
|||
|
||||
/// A flexible container component that can hold other elements.
|
||||
#[derive(IntoElement, IntoComponent)]
|
||||
#[component(scope = "layout")]
|
||||
#[component(scope = "Layout")]
|
||||
pub struct ContentGroup {
|
||||
base: Div,
|
||||
border: bool,
|
||||
|
|
3
crates/ui/src/components/notification.rs
Normal file
3
crates/ui/src/components/notification.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
mod alert_modal;
|
||||
|
||||
pub use alert_modal::*;
|
99
crates/ui/src/components/notification/alert_modal.rs
Normal file
99
crates/ui/src/components/notification/alert_modal.rs
Normal file
|
@ -0,0 +1,99 @@
|
|||
use crate::prelude::*;
|
||||
use gpui::IntoElement;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
|
||||
#[derive(IntoElement, IntoComponent)]
|
||||
#[component(scope = "Notification")]
|
||||
pub struct AlertModal {
|
||||
id: ElementId,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
title: SharedString,
|
||||
primary_action: SharedString,
|
||||
dismiss_label: SharedString,
|
||||
}
|
||||
|
||||
impl AlertModal {
|
||||
pub fn new(id: impl Into<ElementId>, title: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
children: smallvec![],
|
||||
title: title.into(),
|
||||
primary_action: "Ok".into(),
|
||||
dismiss_label: "Cancel".into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn primary_action(mut self, primary_action: impl Into<SharedString>) -> Self {
|
||||
self.primary_action = primary_action.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn dismiss_label(mut self, dismiss_label: impl Into<SharedString>) -> Self {
|
||||
self.dismiss_label = dismiss_label.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for AlertModal {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
v_flex()
|
||||
.id(self.id)
|
||||
.elevation_3(cx)
|
||||
.w(px(440.))
|
||||
.p_5()
|
||||
.child(
|
||||
v_flex()
|
||||
.text_ui(cx)
|
||||
.text_color(Color::Muted.color(cx))
|
||||
.gap_1()
|
||||
.child(Headline::new(self.title).size(HeadlineSize::Small))
|
||||
.children(self.children),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.h(rems(1.75))
|
||||
.items_center()
|
||||
.child(div().flex_1())
|
||||
.child(
|
||||
h_flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.child(
|
||||
Button::new(self.dismiss_label.clone(), self.dismiss_label.clone())
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Button::new(
|
||||
self.primary_action.clone(),
|
||||
self.primary_action.clone(),
|
||||
)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for AlertModal {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
|
||||
impl ComponentPreview for AlertModal {
|
||||
fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
|
||||
v_flex()
|
||||
.gap_6()
|
||||
.p_4()
|
||||
.children(vec![example_group(
|
||||
vec![
|
||||
single_example(
|
||||
"Basic Alert",
|
||||
AlertModal::new("simple-modal", "Do you want to leave the current call?")
|
||||
.child("The current window will be closed, and connections to any shared projects will be terminated."
|
||||
)
|
||||
.primary_action("Leave Call")
|
||||
.into_any_element(),
|
||||
)
|
||||
],
|
||||
)])
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
|
@ -40,7 +40,7 @@ pub enum ToggleStyle {
|
|||
/// Each checkbox works independently from other checkboxes in the list,
|
||||
/// therefore checking an additional box does not affect any other selections.
|
||||
#[derive(IntoElement, IntoComponent)]
|
||||
#[component(scope = "input")]
|
||||
#[component(scope = "Input")]
|
||||
pub struct Checkbox {
|
||||
id: ElementId,
|
||||
toggle_state: ToggleState,
|
||||
|
@ -240,7 +240,7 @@ impl RenderOnce for Checkbox {
|
|||
|
||||
/// A [`Checkbox`] that has a [`Label`].
|
||||
#[derive(IntoElement, IntoComponent)]
|
||||
#[component(scope = "input")]
|
||||
#[component(scope = "Input")]
|
||||
pub struct CheckboxWithLabel {
|
||||
id: ElementId,
|
||||
label: Label,
|
||||
|
@ -318,7 +318,7 @@ impl RenderOnce for CheckboxWithLabel {
|
|||
///
|
||||
/// Switches are used to represent opposite states, such as enabled or disabled.
|
||||
#[derive(IntoElement, IntoComponent)]
|
||||
#[component(scope = "input")]
|
||||
#[component(scope = "Input")]
|
||||
pub struct Switch {
|
||||
id: ElementId,
|
||||
toggle_state: ToggleState,
|
||||
|
|
|
@ -7,9 +7,12 @@ pub use gpui::{
|
|||
Styled, Window,
|
||||
};
|
||||
|
||||
pub use component::{example_group, example_group_with_title, single_example, ComponentPreview};
|
||||
pub use component::{
|
||||
example_group, example_group_with_title, single_example, ComponentPreview, ComponentScope,
|
||||
};
|
||||
pub use ui_macros::IntoComponent;
|
||||
|
||||
pub use crate::animation::{AnimationDirection, AnimationDuration, DefaultAnimations};
|
||||
pub use crate::styles::{rems_from_px, vh, vw, PlatformStyle, StyledTypography, TextSize};
|
||||
pub use crate::traits::clickable::*;
|
||||
pub use crate::traits::disableable::*;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
pub mod animation;
|
||||
mod appearance;
|
||||
mod color;
|
||||
mod elevation;
|
||||
|
|
276
crates/ui/src/styles/animation.rs
Normal file
276
crates/ui/src/styles/animation.rs
Normal file
|
@ -0,0 +1,276 @@
|
|||
use crate::{prelude::*, ContentGroup};
|
||||
use gpui::{AnimationElement, AnimationExt, Styled};
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::ease_out_quint;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum AnimationDuration {
|
||||
Instant = 50,
|
||||
Fast = 150,
|
||||
Slow = 300,
|
||||
}
|
||||
|
||||
impl AnimationDuration {
|
||||
pub fn duration(&self) -> Duration {
|
||||
Duration::from_millis(*self as u64)
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<std::time::Duration> for AnimationDuration {
|
||||
fn into(self) -> Duration {
|
||||
self.duration()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum AnimationDirection {
|
||||
FromBottom,
|
||||
FromLeft,
|
||||
FromRight,
|
||||
FromTop,
|
||||
}
|
||||
|
||||
pub trait DefaultAnimations: Styled + Sized {
|
||||
fn animate_in(
|
||||
self,
|
||||
animation_type: AnimationDirection,
|
||||
fade_in: bool,
|
||||
) -> AnimationElement<Self> {
|
||||
let animation_name = match animation_type {
|
||||
AnimationDirection::FromBottom => "animate_from_bottom",
|
||||
AnimationDirection::FromLeft => "animate_from_left",
|
||||
AnimationDirection::FromRight => "animate_from_right",
|
||||
AnimationDirection::FromTop => "animate_from_top",
|
||||
};
|
||||
|
||||
self.with_animation(
|
||||
animation_name,
|
||||
gpui::Animation::new(AnimationDuration::Fast.into()).with_easing(ease_out_quint()),
|
||||
move |mut this, delta| {
|
||||
let start_opacity = 0.4;
|
||||
let start_pos = 0.0;
|
||||
let end_pos = 40.0;
|
||||
|
||||
if fade_in {
|
||||
this = this.opacity(start_opacity + delta * (1.0 - start_opacity));
|
||||
}
|
||||
|
||||
match animation_type {
|
||||
AnimationDirection::FromBottom => {
|
||||
this.bottom(px(start_pos + delta * (end_pos - start_pos)))
|
||||
}
|
||||
AnimationDirection::FromLeft => {
|
||||
this.left(px(start_pos + delta * (end_pos - start_pos)))
|
||||
}
|
||||
AnimationDirection::FromRight => {
|
||||
this.right(px(start_pos + delta * (end_pos - start_pos)))
|
||||
}
|
||||
AnimationDirection::FromTop => {
|
||||
this.top(px(start_pos + delta * (end_pos - start_pos)))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn animate_in_from_bottom(self, fade: bool) -> AnimationElement<Self> {
|
||||
self.animate_in(AnimationDirection::FromBottom, fade)
|
||||
}
|
||||
|
||||
fn animate_in_from_left(self, fade: bool) -> AnimationElement<Self> {
|
||||
self.animate_in(AnimationDirection::FromLeft, fade)
|
||||
}
|
||||
|
||||
fn animate_in_from_right(self, fade: bool) -> AnimationElement<Self> {
|
||||
self.animate_in(AnimationDirection::FromRight, fade)
|
||||
}
|
||||
|
||||
fn animate_in_from_top(self, fade: bool) -> AnimationElement<Self> {
|
||||
self.animate_in(AnimationDirection::FromTop, fade)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Styled> DefaultAnimations for E {}
|
||||
|
||||
// Don't use this directly, it only exists to show animation previews
|
||||
#[derive(IntoComponent)]
|
||||
struct Animation {}
|
||||
|
||||
// View this component preview using `workspace: open component-preview`
|
||||
impl ComponentPreview for Animation {
|
||||
fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
|
||||
let container_size = 128.0;
|
||||
let element_size = 32.0;
|
||||
let left_offset = element_size - container_size / 2.0;
|
||||
v_flex()
|
||||
.gap_6()
|
||||
.children(vec![
|
||||
example_group_with_title(
|
||||
"Animate In",
|
||||
vec![
|
||||
single_example(
|
||||
"From Bottom",
|
||||
ContentGroup::new()
|
||||
.relative()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.size(px(container_size))
|
||||
.child(
|
||||
div()
|
||||
.id("animate-in-from-bottom")
|
||||
.absolute()
|
||||
.size(px(element_size))
|
||||
.left(px(left_offset))
|
||||
.rounded_md()
|
||||
.bg(gpui::red())
|
||||
.animate_in(AnimationDirection::FromBottom, false),
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"From Top",
|
||||
ContentGroup::new()
|
||||
.relative()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.size(px(container_size))
|
||||
.child(
|
||||
div()
|
||||
.id("animate-in-from-top")
|
||||
.absolute()
|
||||
.size(px(element_size))
|
||||
.left(px(left_offset))
|
||||
.rounded_md()
|
||||
.bg(gpui::blue())
|
||||
.animate_in(AnimationDirection::FromTop, false),
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"From Left",
|
||||
ContentGroup::new()
|
||||
.relative()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.size(px(container_size))
|
||||
.child(
|
||||
div()
|
||||
.id("animate-in-from-left")
|
||||
.absolute()
|
||||
.size(px(element_size))
|
||||
.left(px(left_offset))
|
||||
.rounded_md()
|
||||
.bg(gpui::green())
|
||||
.animate_in(AnimationDirection::FromLeft, false),
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"From Right",
|
||||
ContentGroup::new()
|
||||
.relative()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.size(px(container_size))
|
||||
.child(
|
||||
div()
|
||||
.id("animate-in-from-right")
|
||||
.absolute()
|
||||
.size(px(element_size))
|
||||
.left(px(left_offset))
|
||||
.rounded_md()
|
||||
.bg(gpui::yellow())
|
||||
.animate_in(AnimationDirection::FromRight, false),
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
],
|
||||
)
|
||||
.grow(),
|
||||
example_group_with_title(
|
||||
"Fade and Animate In",
|
||||
vec![
|
||||
single_example(
|
||||
"From Bottom",
|
||||
ContentGroup::new()
|
||||
.relative()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.size(px(container_size))
|
||||
.child(
|
||||
div()
|
||||
.id("fade-animate-in-from-bottom")
|
||||
.absolute()
|
||||
.size(px(element_size))
|
||||
.left(px(left_offset))
|
||||
.rounded_md()
|
||||
.bg(gpui::red())
|
||||
.animate_in(AnimationDirection::FromBottom, true),
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"From Top",
|
||||
ContentGroup::new()
|
||||
.relative()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.size(px(container_size))
|
||||
.child(
|
||||
div()
|
||||
.id("fade-animate-in-from-top")
|
||||
.absolute()
|
||||
.size(px(element_size))
|
||||
.left(px(left_offset))
|
||||
.rounded_md()
|
||||
.bg(gpui::blue())
|
||||
.animate_in(AnimationDirection::FromTop, true),
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"From Left",
|
||||
ContentGroup::new()
|
||||
.relative()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.size(px(container_size))
|
||||
.child(
|
||||
div()
|
||||
.id("fade-animate-in-from-left")
|
||||
.absolute()
|
||||
.size(px(element_size))
|
||||
.left(px(left_offset))
|
||||
.rounded_md()
|
||||
.bg(gpui::green())
|
||||
.animate_in(AnimationDirection::FromLeft, true),
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"From Right",
|
||||
ContentGroup::new()
|
||||
.relative()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.size(px(container_size))
|
||||
.child(
|
||||
div()
|
||||
.id("fade-animate-in-from-right")
|
||||
.absolute()
|
||||
.size(px(element_size))
|
||||
.left(px(left_offset))
|
||||
.rounded_md()
|
||||
.bg(gpui::yellow())
|
||||
.animate_in(AnimationDirection::FromRight, true),
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
],
|
||||
)
|
||||
.grow(),
|
||||
])
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue