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