diff --git a/crates/story/src/alert_story.rs b/crates/story/src/alert_story.rs index b4c0bd1f..866d2580 100644 --- a/crates/story/src/alert_story.rs +++ b/crates/story/src/alert_story.rs @@ -13,6 +13,7 @@ use crate::section; pub struct AlertStory { size: Size, + banner_visible: bool, focus_handle: gpui::FocusHandle, } @@ -20,6 +21,7 @@ impl AlertStory { fn new(_: &mut Window, cx: &mut Context) -> Self { Self { size: Size::default(), + banner_visible: true, focus_handle: cx.focus_handle(), } } @@ -62,6 +64,20 @@ impl Render for AlertStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_4() + .child( + Alert::warning( + "banner-1", + "This is a banner alert, it will take the full width of the container.", + ) + .banner() + .on_close(cx.listener(|this, _, _, cx| { + this.banner_visible = !this.banner_visible; + cx.notify(); + })) + .visible(self.banner_visible) + .with_size(self.size) + .icon(IconName::Bell), + ) .child( ButtonGroup::new("toggle-size") .outline() @@ -99,14 +115,18 @@ impl Render for AlertStory { ) .child( section("Info").max_w_md().child( - Alert::info("This is an info alert.") + Alert::info("info1", "This is an info alert.") .with_size(self.size) - .title("Info message"), + .title("Info message") + .on_close(cx.listener(|_, _, _, _| { + println!("Info alert closed"); + })), ), ) .child( section("Success with Title").max_w_md().child( Alert::success( + "success-1", "You have successfully submitted your form.\n\ Thank you for your submission!", ) @@ -117,6 +137,7 @@ impl Render for AlertStory { .child( section("Warning").max_w_md().child( Alert::warning( + "warning-1", "This is a warning alert with icon and title.\n\ This is second line of text to test is the line-height is correct.", ) @@ -126,6 +147,7 @@ impl Render for AlertStory { .child( section("Error").max_w_md().child( Alert::error( + "error-1", "There was an error submitting your form.\n\ Please try again later, if you still have issues, please contact support.", ) @@ -135,7 +157,7 @@ impl Render for AlertStory { ) .child( section("Custom Icon").max_w_md().child( - Alert::info("Custom icon with info alert.") + Alert::info("other-1", "Custom icon with info alert.") .title("Custom Icon") .with_size(self.size) .icon(IconName::Bell), diff --git a/crates/ui/src/alert.rs b/crates/ui/src/alert.rs index 125221e4..6d2069fa 100644 --- a/crates/ui/src/alert.rs +++ b/crates/ui/src/alert.rs @@ -1,6 +1,9 @@ +use std::rc::Rc; + use gpui::{ - div, prelude::FluentBuilder as _, px, relative, App, Div, Hsla, IntoElement, - ParentElement as _, RenderOnce, SharedString, Styled, Window, + div, prelude::FluentBuilder as _, px, relative, App, ClickEvent, Div, ElementId, Empty, Hsla, + InteractiveElement, IntoElement, ParentElement as _, RenderOnce, SharedString, Stateful, + StatefulInteractiveElement, Styled, Window, }; use crate::{h_flex, text::Text, ActiveTheme as _, Icon, IconName, Sizable, Size, StyledExt}; @@ -37,51 +40,57 @@ impl AlertVariant { /// Alert used to display a message to the user. #[derive(IntoElement)] pub struct Alert { - base: Div, + base: Stateful
, variant: AlertVariant, icon: Option, title: Option, message: Text, size: Size, + banner: bool, + on_close: Option>, + visible: bool, } impl Alert { /// Create a new alert with the given message. - fn new(message: impl Into) -> Self { + fn new(id: impl Into, message: impl Into) -> Self { Self { - base: div(), + base: div().id(id), variant: AlertVariant::default(), icon: None, title: None, message: message.into(), size: Size::default(), + banner: false, + visible: true, + on_close: None, } } /// Create a new info [`AlertVariant::Info`] with the given message. - pub fn info(message: impl Into) -> Self { - Self::new(message) + pub fn info(id: impl Into, message: impl Into) -> Self { + Self::new(id, message) .with_variant(AlertVariant::Info) .icon(IconName::Info) } /// Create a new [`AlertVariant::Success`] alert with the given message. - pub fn success(message: impl Into) -> Self { - Self::new(message) + pub fn success(id: impl Into, message: impl Into) -> Self { + Self::new(id, message) .with_variant(AlertVariant::Success) .icon(IconName::CircleCheck) } /// Create a new [`AlertVariant::Warning`] alert with the given message. - pub fn warning(message: impl Into) -> Self { - Self::new(message) + pub fn warning(id: impl Into, message: impl Into) -> Self { + Self::new(id, message) .with_variant(AlertVariant::Warning) .icon(IconName::TriangleAlert) } /// Create a new [`AlertVariant::Error`] alert with the given message. - pub fn error(message: impl Into) -> Self { - Self::new(message) + pub fn error(id: impl Into, message: impl Into) -> Self { + Self::new(id, message) .with_variant(AlertVariant::Error) .icon(IconName::CircleX) } @@ -103,6 +112,30 @@ impl Alert { self.title = Some(title.into()); self } + + /// Set alert as banner style. + /// + /// The `banner` style will make the alert take the full width of the container and not border and radius. + /// This mode will not display `title`. + pub fn banner(mut self) -> Self { + self.banner = true; + self + } + + /// Set alert as closable, true will show Close icon. + pub fn on_close( + mut self, + on_close: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.on_close = Some(Rc::new(on_close)); + self + } + + /// Set the visibility of the alert. + pub fn visible(mut self, visible: bool) -> Self { + self.visible = visible; + self + } } impl Sizable for Alert { @@ -120,6 +153,10 @@ impl Styled for Alert { impl RenderOnce for Alert { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { + if !self.visible { + return Empty.into_any_element(); + } + let (radius, padding_x, padding_y, gap, line_height, icon_mt) = match self.size { Size::XSmall => (cx.theme().radius, px(12.), px(6.), px(6.), 1.2, px(2.5)), Size::Small => (cx.theme().radius, px(12.), px(8.), px(6.), 1.2, px(1.5)), @@ -143,47 +180,81 @@ impl RenderOnce for Alert { let color = self.variant.color(cx); - self.base.flex_1().child( - h_flex() - .w_full() - .rounded(radius) - .border_1() - .border_color(color) - .bg(color.opacity(0.1)) - .text_color(self.variant.fg(cx)) - .px(padding_x) - .py(padding_y) - .gap(gap) - .overflow_hidden() - .items_start() - .map(|this| match self.size { - Size::Large => this.text_base(), - _ => this.text_sm(), - }) - .line_height(relative(line_height)) - .child( - div().mt(icon_mt).child( - self.icon - .unwrap_or(IconName::Info.into()) - .with_size(self.size) - .flex_shrink_0(), - ), - ) - .child( - div() - .overflow_hidden() - .when_some(self.title, |this, title| { - this.child( - div() - .w_full() - .truncate() - .mb_1() - .font_semibold() - .child(title), + self.base + .flex_1() + .when(self.banner, |this| this.w_full()) + .child( + h_flex() + .w_full() + .items_center() + .when(!self.banner, |this| { + this.rounded(radius) + .border_1() + .border_color(color) + .items_start() + }) + .bg(color.opacity(0.06)) + .text_color(self.variant.fg(cx)) + .px(padding_x) + .py(padding_y) + .gap(gap) + .overflow_hidden() + .justify_between() + .map(|this| match self.size { + Size::Large => this.text_base(), + _ => this.text_sm(), + }) + .line_height(relative(line_height)) + .child( + h_flex() + .items_start() + .gap(gap) + .child( + div().mt(icon_mt).child( + self.icon + .unwrap_or(IconName::Info.into()) + .with_size(self.size) + .flex_shrink_0(), + ), ) - }) - .child(div().overflow_hidden().child(self.message)), - ), - ) + .child( + div() + .overflow_hidden() + .when(!self.banner, |this| { + this.when_some(self.title, |this, title| { + this.child( + div() + .w_full() + .truncate() + .mb_1() + .font_semibold() + .child(title), + ) + }) + }) + .child(div().overflow_hidden().child(self.message)), + ), + ) + .when_some(self.on_close, |this, on_close| { + this.child( + div() + .id("close") + .p_0p5() + .rounded(cx.theme().radius) + .hover(|this| this.bg(color.opacity(0.1))) + .active(|this| this.bg(color.opacity(0.2))) + .on_click(move |ev, window, cx| { + on_close(ev, window, cx); + }) + .child( + Icon::new(IconName::Close) + .text_color(cx.theme().foreground) + .with_size(self.size.max(Size::Medium)) + .flex_shrink_0(), + ), + ) + }), + ) + .into_any_element() } } diff --git a/crates/ui/src/styled.rs b/crates/ui/src/styled.rs index 80192f83..dd653df5 100644 --- a/crates/ui/src/styled.rs +++ b/crates/ui/src/styled.rs @@ -180,6 +180,16 @@ pub enum Size { } impl Size { + fn as_f32(&self) -> f32 { + match self { + Size::Size(val) => val.0, + Size::XSmall => 0., + Size::Small => 1., + Size::Medium => 2., + Size::Large => 3., + } + } + /// Returns the height for table row. #[inline] pub fn table_row_height(&self) -> Pixels { @@ -243,6 +253,32 @@ impl Size { Size::Size(val) => Size::Size(*val * 1.2), } } + + /// Return the max size between two sizes. + /// + /// e.g. `Size::XSmall.max(Size::Small)` will return `Size::XSmall`. + pub fn max(&self, other: Self) -> Self { + match (self, other) { + (Size::Size(a), Size::Size(b)) => Size::Size(px(a.0.min(b.0))), + (Size::Size(a), _) => Size::Size(*a), + (_, Size::Size(b)) => Size::Size(b), + (a, b) if a.as_f32() < b.as_f32() => *a, + _ => other, + } + } + + /// Return the min size between two sizes. + /// + /// e.g. `Size::XSmall.min(Size::Small)` will return `Size::Small`. + pub fn min(&self, other: Self) -> Self { + match (self, other) { + (Size::Size(a), Size::Size(b)) => Size::Size(px(a.0.max(b.0))), + (Size::Size(a), _) => Size::Size(*a), + (_, Size::Size(b)) => Size::Size(b), + (a, b) if a.as_f32() > b.as_f32() => *a, + _ => other, + } + } } impl From for Size { @@ -520,3 +556,36 @@ pub trait Collapsible { fn collapsed(self, collapsed: bool) -> Self; fn is_collapsed(&self) -> bool; } + +#[cfg(test)] +mod tests { + use gpui::px; + + use crate::Size; + + #[test] + fn test_size_max_min() { + assert_eq!(Size::Small.min(Size::XSmall), Size::Small); + assert_eq!(Size::XSmall.min(Size::Small), Size::Small); + assert_eq!(Size::Small.min(Size::Medium), Size::Medium); + assert_eq!(Size::Medium.min(Size::Large), Size::Large); + assert_eq!(Size::Large.min(Size::Small), Size::Large); + + assert_eq!( + Size::Size(px(10.)).min(Size::Size(px(20.))), + Size::Size(px(20.)) + ); + + // Min + assert_eq!(Size::Small.max(Size::XSmall), Size::XSmall); + assert_eq!(Size::XSmall.max(Size::Small), Size::XSmall); + assert_eq!(Size::Small.max(Size::Medium), Size::Small); + assert_eq!(Size::Medium.max(Size::Large), Size::Medium); + assert_eq!(Size::Large.max(Size::Small), Size::Small); + + assert_eq!( + Size::Size(px(10.)).max(Size::Size(px(20.))), + Size::Size(px(10.)) + ); + } +}