diff --git a/README.md b/README.md index 466d126e..769f830f 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ UI components for building fantastic desktop application by using [GPUI](https:/ - Badge - TextView (Markdown, Simple HTML) to native rendering, syntax highlighting. - Toggle, ToggleGroup +- Alert ## Showcase diff --git a/crates/story/examples/alert.rs b/crates/story/examples/alert.rs new file mode 100644 index 00000000..32e63b89 --- /dev/null +++ b/crates/story/examples/alert.rs @@ -0,0 +1,115 @@ +use gpui::*; +use gpui_component::{ + alert::Alert, + button::{Button, ButtonGroup}, + v_flex, IconName, Selectable as _, Sizable as _, Size, +}; +use story::Assets; + +pub struct Example { + size: Size, +} + +impl Example { + pub fn new(_: &mut Window, _: &mut Context) -> Self { + Self { + size: Size::default(), + } + } + + fn view(window: &mut Window, cx: &mut App) -> Entity { + cx.new(|cx| Self::new(window, cx)) + } + + fn set_size(&mut self, size: Size, _: &mut Window, cx: &mut Context) { + self.size = size; + cx.notify(); + } +} + +impl Render for Example { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + v_flex() + .m_4() + .gap_4() + .child( + ButtonGroup::new("toggle-size") + .outline() + .compact() + .child( + Button::new("xsmall") + .label("XSmall") + .selected(self.size == Size::XSmall), + ) + .child( + Button::new("small") + .label("Small") + .selected(self.size == Size::Small), + ) + .child( + Button::new("medium") + .label("Medium") + .selected(self.size == Size::Medium), + ) + .child( + Button::new("large") + .label("Large") + .selected(self.size == Size::Large), + ) + .on_click(cx.listener(|this, selecteds: &Vec, window, cx| { + let size = match selecteds[0] { + 0 => Size::XSmall, + 1 => Size::Small, + 2 => Size::Medium, + 3 => Size::Large, + _ => unreachable!(), + }; + this.set_size(size, window, cx); + })), + ) + .child( + Alert::info("This is an info alert.") + .with_size(self.size) + .title("Info message"), + ) + .child( + Alert::success( + "You have successfully submitted your form.\n\ + Thank you for your submission!", + ) + .with_size(self.size) + .title("Submit Successful"), + ) + .child( + Alert::warning( + "This is a warning alert with icon and title.\n\ + This is second line of text to test is the line-height is correct.", + ) + .with_size(self.size), + ) + .child( + Alert::error( + "There was an error submitting your form.\n\ + Please try again later, if you still have issues, please contact support.", + ) + .with_size(self.size) + .title("Error!"), + ) + .child( + Alert::info("Custom info alert with icon.") + .with_size(self.size) + .icon(IconName::Bell), + ) + } +} + +fn main() { + let app = Application::new().with_assets(Assets); + + app.run(move |cx| { + story::init(cx); + cx.activate(true); + + story::create_new_window("Alert Example", Example::view, cx); + }); +} diff --git a/crates/ui/src/alert.rs b/crates/ui/src/alert.rs new file mode 100644 index 00000000..c6121e81 --- /dev/null +++ b/crates/ui/src/alert.rs @@ -0,0 +1,178 @@ +use gpui::{ + div, prelude::FluentBuilder as _, px, relative, App, Hsla, IntoElement, ParentElement as _, + RenderOnce, SharedString, Styled, Window, +}; + +use crate::{h_flex, text::Text, ActiveTheme as _, Icon, IconName, Sizable, Size, StyledExt}; + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum AlertVariant { + #[default] + Info, + Success, + Warning, + Error, +} + +impl AlertVariant { + fn fg(&self, cx: &App) -> Hsla { + match self { + AlertVariant::Info => cx.theme().info, + AlertVariant::Success => cx.theme().success, + AlertVariant::Warning => cx.theme().warning, + AlertVariant::Error => cx.theme().danger, + } + } + + fn color(&self, cx: &App) -> Hsla { + match self { + AlertVariant::Info => cx.theme().info, + AlertVariant::Success => cx.theme().success, + AlertVariant::Warning => cx.theme().warning, + AlertVariant::Error => cx.theme().danger, + } + } +} + +/// Alert used to display a message to the user. +#[derive(IntoElement)] +pub struct Alert { + variant: AlertVariant, + icon: Option, + title: Option, + message: Text, + size: Size, +} + +impl Alert { + /// Create a new alert with the given message. + fn new(message: impl Into) -> Self { + Self { + variant: AlertVariant::default(), + icon: None, + title: None, + message: message.into(), + size: Size::default(), + } + } + + /// Create a new info [`AlertVariant::Info`] with the given message. + pub fn info(message: impl Into) -> Self { + Self::new(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) + .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) + .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) + .with_variant(AlertVariant::Error) + .icon(IconName::CircleX) + } + + /// Sets the [`AlertVariant`] of the alert. + pub fn with_variant(mut self, variant: AlertVariant) -> Self { + self.variant = variant; + self + } + + /// Set the icon for the alert. + pub fn icon(mut self, icon: impl Into) -> Self { + self.icon = Some(icon.into()); + self + } + + /// Set the title for the alert. + pub fn title(mut self, title: impl Into) -> Self { + self.title = Some(title.into()); + self + } +} + +impl Sizable for Alert { + fn with_size(mut self, size: impl Into) -> Self { + self.size = size.into(); + self + } +} + +impl RenderOnce for Alert { + fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { + 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)), + Size::Large => ( + cx.theme().radius * 3., + px(20.), + px(16.), + px(12.), + 1.4, + px(0.), + ), + _ => ( + cx.theme().radius * 2., + px(16.), + px(12.), + px(8.), + 1.3, + px(1.), + ), + }; + + let color = self.variant.color(cx); + + h_flex() + .rounded(radius) + .border_1() + .border_color(color) + .bg(color.opacity(0.05)) + .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), + ) + }) + .child(div().overflow_hidden().child(self.message)), + ) + } +} diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index 84320c67..0674bf8e 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -12,6 +12,7 @@ mod virtual_list; mod window_border; pub mod accordion; +pub mod alert; pub mod animation; pub mod badge; pub mod breadcrumb; diff --git a/crates/ui/src/tab/tab.rs b/crates/ui/src/tab/tab.rs index 3e65c390..cd7c94a5 100644 --- a/crates/ui/src/tab/tab.rs +++ b/crates/ui/src/tab/tab.rs @@ -464,7 +464,7 @@ impl Tab { } /// Set Tab Variant. - pub fn variant(mut self, variant: TabVariant) -> Self { + pub fn with_variant(mut self, variant: TabVariant) -> Self { self.variant = variant; self } diff --git a/crates/ui/src/tab/tab_bar.rs b/crates/ui/src/tab/tab_bar.rs index 29b96761..9d22ad5a 100644 --- a/crates/ui/src/tab/tab_bar.rs +++ b/crates/ui/src/tab/tab_bar.rs @@ -53,7 +53,7 @@ impl TabBar { } /// Set the Tab variant, all children will inherit the variant. - pub fn variant(mut self, variant: TabVariant) -> Self { + pub fn with_variant(mut self, variant: TabVariant) -> Self { self.variant = variant; self } @@ -258,7 +258,7 @@ impl RenderOnce for TabBar { item_labels.push((child.label.clone(), child.disabled)); child .id(ix) - .variant(self.variant) + .with_variant(self.variant) .with_size(self.size) .when_some(self.selected_index, |this, selected_ix| { this.selected(selected_ix == ix) diff --git a/crates/ui/src/theme.rs b/crates/ui/src/theme.rs index a90b08f8..d0940e02 100644 --- a/crates/ui/src/theme.rs +++ b/crates/ui/src/theme.rs @@ -95,6 +95,14 @@ pub struct ThemeColor { pub drop_target: Hsla, /// Default text color. pub foreground: Hsla, + /// Info background color. + pub info: Hsla, + /// Info active background color. + pub info_active: Hsla, + /// Info text color. + pub info_foreground: Hsla, + /// Info hover background color. + pub info_hover: Hsla, /// Border color for inputs such as Input, Dropdown, etc. pub input: Hsla, /// Link text color. @@ -171,6 +179,14 @@ pub struct ThemeColor { pub slider_bar: Hsla, /// Slider thumb background color. pub slider_thumb: Hsla, + /// Success background color. + pub success: Hsla, + /// Success text color. + pub success_foreground: Hsla, + /// Success hover background color. + pub success_hover: Hsla, + /// Success active background color. + pub success_active: Hsla, /// Tab background color. pub tab: Hsla, /// Tab active background color. @@ -205,6 +221,14 @@ pub struct ThemeColor { pub title_bar_border: Hsla, /// Background color for Tiles. pub tiles: Hsla, + /// Warning background color. + pub warning: Hsla, + /// Warning active background color. + pub warning_active: Hsla, + /// Warning hover background color. + pub warning_hover: Hsla, + /// Warning foreground color. + pub warning_foreground: Hsla, /// Window border color. /// /// # Platform specific: @@ -226,15 +250,19 @@ impl ThemeColor { card: hsl(0.0, 0.0, 100.0), card_foreground: hsl(240.0, 10.0, 3.9), caret: hsl(240.0, 10., 3.9), - danger: hsl(0.0, 84.2, 60.2), - danger_active: hsl(0.0, 84.2, 47.0), - danger_foreground: hsl(0.0, 0.0, 98.0), - danger_hover: hsl(0.0, 84.2, 65.0), + danger: crate::red_500(), + danger_active: crate::red_600(), + danger_foreground: crate::red_50(), + danger_hover: crate::red_500().opacity(0.9), description_list_label: hsl(240.0, 5.9, 96.9), description_list_label_foreground: hsl(240.0, 5.9, 10.0), drag_border: crate::blue_500(), drop_target: hsl(235.0, 30., 44.0).opacity(0.25), foreground: hsl(240.0, 10., 3.9), + info: crate::sky_500(), + info_active: crate::sky_600(), + info_hover: crate::sky_500().opacity(0.9), + info_foreground: crate::sky_50(), input: hsl(240.0, 5.9, 90.0), link: hsl(221.0, 83.0, 53.0), link_active: hsl(221.0, 83.0, 53.0).darken(0.2), @@ -273,6 +301,10 @@ impl ThemeColor { skeleton: hsl(223.0, 5.9, 10.0).opacity(0.1), slider_bar: hsl(223.0, 5.9, 10.0), slider_thumb: hsl(0.0, 0.0, 100.0), + success: crate::green_500(), + success_active: crate::green_600(), + success_hover: crate::green_500().opacity(0.9), + success_foreground: crate::gray_50(), tab: gpui::transparent_black(), tab_active: hsl(0.0, 0.0, 100.0), tab_active_foreground: hsl(240.0, 10., 3.9), @@ -287,9 +319,13 @@ impl ThemeColor { table_head_foreground: hsl(240.0, 10., 3.9).opacity(0.7), table_hover: hsl(240.0, 4.8, 95.0), table_row_border: hsl(240.0, 7.7, 94.5), + tiles: hsl(0.0, 0.0, 95.), title_bar: hsl(0.0, 0.0, 100.), title_bar_border: hsl(240.0, 5.9, 90.0), - tiles: hsl(0.0, 0.0, 95.), + warning: crate::yellow_500(), + warning_active: crate::yellow_600(), + warning_hover: crate::yellow_500().opacity(0.9), + warning_foreground: crate::gray_50(), window_border: hsl(240.0, 5.9, 78.0), } } @@ -306,15 +342,19 @@ impl ThemeColor { card: hsl(0.0, 0.0, 8.0), card_foreground: hsl(0.0, 0.0, 78.0), caret: hsl(0., 0., 78.), - danger: hsl(0.0, 62.8, 30.6), - danger_active: hsl(0.0, 62.8, 20.6), - danger_foreground: hsl(0.0, 0.0, 78.0), - danger_hover: hsl(0.0, 62.8, 35.6), + danger: crate::red_900(), + danger_active: crate::red_900().darken(0.2), + danger_foreground: crate::red_50(), + danger_hover: crate::red_900().opacity(0.9), description_list_label: hsl(240.0, 0., 13.0), description_list_label_foreground: hsl(0.0, 0.0, 78.0), drag_border: crate::blue_500(), drop_target: hsl(235.0, 30., 44.0).opacity(0.1), foreground: hsl(0., 0., 78.), + info: crate::sky_900(), + info_active: crate::sky_900().darken(0.2), + info_foreground: crate::sky_50(), + info_hover: crate::sky_900().opacity(0.8), input: hsl(240.0, 3.7, 15.9), link: hsl(221.0, 83.0, 53.0), link_active: hsl(221.0, 83.0, 53.0).darken(0.2), @@ -353,6 +393,10 @@ impl ThemeColor { skeleton: hsla(223.0, 0.0, 98.0, 0.1), slider_bar: hsl(223.0, 0.0, 98.0), slider_thumb: hsl(0.0, 0.0, 8.0), + success: crate::green_900(), + success_active: crate::green_900().darken(0.2), + success_foreground: crate::green_50(), + success_hover: crate::green_900().opacity(0.8), tab: gpui::transparent_black(), tab_active: hsl(0.0, 0.0, 8.0), tab_active_foreground: hsl(0., 0., 78.), @@ -367,9 +411,13 @@ impl ThemeColor { table_head_foreground: hsl(0., 0., 78.).opacity(0.7), table_hover: hsl(240.0, 3.7, 15.9).opacity(0.5), table_row_border: hsl(240.0, 3.7, 16.9).opacity(0.5), + tiles: hsl(0.0, 0.0, 5.0), title_bar: hsl(0., 0., 9.7), title_bar_border: hsl(240.0, 3.7, 15.9), - tiles: hsl(0.0, 0.0, 5.0), + warning: crate::yellow_900(), + warning_active: crate::yellow_900().darken(0.2), + warning_foreground: crate::yellow_50(), + warning_hover: crate::yellow_900().opacity(0.9), window_border: hsl(240.0, 3.7, 28.0), } }