banner.rs

  1use crate::prelude::*;
  2use gpui::{AnyElement, IntoElement, ParentElement, Styled};
  3
  4/// Severity levels that determine the style of the banner.
  5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
  6pub enum Severity {
  7    Info,
  8    Success,
  9    Warning,
 10    Error,
 11}
 12
 13/// Banners provide informative and brief messages without interrupting the user.
 14/// This component offers four severity levels that can be used depending on the message.
 15///
 16/// # Usage Example
 17///
 18/// ```
 19/// use ui::{Banner};
 20///
 21///    Banner::new()
 22///     .severity(Severity::Info)
 23///     .children(Label::new("This is an informational message"))
 24///     .action_slot(
 25///         Button::new("learn-more", "Learn More")
 26///             .icon(IconName::ArrowUpRight)
 27///             .icon_size(IconSize::XSmall)
 28///             .icon_position(IconPosition::End),
 29///     )
 30/// ```
 31#[derive(IntoElement, IntoComponent)]
 32#[component(scope = "Notification")]
 33pub struct Banner {
 34    severity: Severity,
 35    children: Option<AnyElement>,
 36    icon: Option<(IconName, Option<Color>)>,
 37    action_slot: Option<AnyElement>,
 38}
 39
 40impl Banner {
 41    /// Creates a new `Banner` component with default styling.
 42    pub fn new() -> Self {
 43        Self {
 44            severity: Severity::Info,
 45            children: None,
 46            icon: None,
 47            action_slot: None,
 48        }
 49    }
 50
 51    /// Sets the severity of the banner.
 52    pub fn severity(mut self, severity: Severity) -> Self {
 53        self.severity = severity;
 54        self
 55    }
 56
 57    /// Sets an icon to display in the banner with an optional color.
 58    pub fn icon(mut self, icon: IconName, color: Option<impl Into<Color>>) -> Self {
 59        self.icon = Some((icon, color.map(|c| c.into())));
 60        self
 61    }
 62
 63    /// A slot for actions, such as CTA or dismissal buttons.
 64    pub fn action_slot(mut self, element: impl IntoElement) -> Self {
 65        self.action_slot = Some(element.into_any_element());
 66        self
 67    }
 68
 69    /// A general container for the banner's main content.
 70    pub fn children(mut self, element: impl IntoElement) -> Self {
 71        self.children = Some(element.into_any_element());
 72        self
 73    }
 74}
 75
 76impl RenderOnce for Banner {
 77    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
 78        let base = h_flex()
 79            .py_0p5()
 80            .rounded_sm()
 81            .flex_wrap()
 82            .justify_between()
 83            .border_1();
 84
 85        let (icon, icon_color, bg_color, border_color) = match self.severity {
 86            Severity::Info => (
 87                IconName::Info,
 88                Color::Muted,
 89                cx.theme().status().info_background.opacity(0.5),
 90                cx.theme().colors().border_variant,
 91            ),
 92            Severity::Success => (
 93                IconName::Check,
 94                Color::Success,
 95                cx.theme().status().success.opacity(0.1),
 96                cx.theme().status().success.opacity(0.2),
 97            ),
 98            Severity::Warning => (
 99                IconName::Warning,
100                Color::Warning,
101                cx.theme().status().warning_background.opacity(0.5),
102                cx.theme().status().warning_border.opacity(0.4),
103            ),
104            Severity::Error => (
105                IconName::XCircle,
106                Color::Error,
107                cx.theme().status().error.opacity(0.1),
108                cx.theme().status().error.opacity(0.2),
109            ),
110        };
111
112        let mut container = base.bg(bg_color).border_color(border_color);
113
114        let mut content_area = h_flex().id("content_area").gap_1p5().overflow_x_scroll();
115
116        if self.icon.is_none() {
117            content_area =
118                content_area.child(Icon::new(icon).size(IconSize::XSmall).color(icon_color));
119        }
120
121        if let Some(children) = self.children {
122            content_area = content_area.child(children);
123        }
124
125        if let Some(action_slot) = self.action_slot {
126            container = container
127                .pl_2()
128                .pr_0p5()
129                .gap_2()
130                .child(content_area)
131                .child(action_slot);
132        } else {
133            container = container.px_2().child(div().w_full().child(content_area));
134        }
135
136        container
137    }
138}
139
140impl ComponentPreview for Banner {
141    fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
142        let severity_examples = vec![
143            single_example(
144                "Default",
145                Banner::new()
146                    .children(Label::new("This is a default banner with no customization"))
147                    .into_any_element(),
148            ),
149            single_example(
150                "Info",
151                Banner::new()
152                    .severity(Severity::Info)
153                    .children(Label::new("This is an informational message"))
154                    .action_slot(
155                        Button::new("learn-more", "Learn More")
156                            .icon(IconName::ArrowUpRight)
157                            .icon_size(IconSize::XSmall)
158                            .icon_position(IconPosition::End),
159                    )
160                    .into_any_element(),
161            ),
162            single_example(
163                "Success",
164                Banner::new()
165                    .severity(Severity::Success)
166                    .children(Label::new("Operation completed successfully"))
167                    .action_slot(Button::new("dismiss", "Dismiss"))
168                    .into_any_element(),
169            ),
170            single_example(
171                "Warning",
172                Banner::new()
173                    .severity(Severity::Warning)
174                    .children(Label::new("Your settings file uses deprecated settings"))
175                    .action_slot(Button::new("update", "Update Settings"))
176                    .into_any_element(),
177            ),
178            single_example(
179                "Error",
180                Banner::new()
181                    .severity(Severity::Error)
182                    .children(Label::new("Connection error: unable to connect to server"))
183                    .action_slot(Button::new("reconnect", "Retry"))
184                    .into_any_element(),
185            ),
186        ];
187
188        example_group(severity_examples)
189            .vertical()
190            .into_any_element()
191    }
192}