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: Vec<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: Vec::new(),
 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
 69impl ParentElement for Banner {
 70    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
 71        self.children.extend(elements)
 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.opacity(0.5),
 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        content_area = content_area.children(self.children);
121
122        if let Some(action_slot) = self.action_slot {
123            container = container
124                .pl_2()
125                .pr_0p5()
126                .gap_2()
127                .child(content_area)
128                .child(action_slot);
129        } else {
130            container = container.px_2().child(div().w_full().child(content_area));
131        }
132
133        container
134    }
135}
136
137impl Component for Banner {
138    fn scope() -> ComponentScope {
139        ComponentScope::Notification
140    }
141
142    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
143        let severity_examples = vec![
144            single_example(
145                "Default",
146                Banner::new()
147                    .child(Label::new("This is a default banner with no customization"))
148                    .into_any_element(),
149            ),
150            single_example(
151                "Info",
152                Banner::new()
153                    .severity(Severity::Info)
154                    .child(Label::new("This is an informational message"))
155                    .action_slot(
156                        Button::new("learn-more", "Learn More")
157                            .icon(IconName::ArrowUpRight)
158                            .icon_size(IconSize::XSmall)
159                            .icon_position(IconPosition::End),
160                    )
161                    .into_any_element(),
162            ),
163            single_example(
164                "Success",
165                Banner::new()
166                    .severity(Severity::Success)
167                    .child(Label::new("Operation completed successfully"))
168                    .action_slot(Button::new("dismiss", "Dismiss"))
169                    .into_any_element(),
170            ),
171            single_example(
172                "Warning",
173                Banner::new()
174                    .severity(Severity::Warning)
175                    .child(Label::new("Your settings file uses deprecated settings"))
176                    .action_slot(Button::new("update", "Update Settings"))
177                    .into_any_element(),
178            ),
179            single_example(
180                "Error",
181                Banner::new()
182                    .severity(Severity::Error)
183                    .child(Label::new("Connection error: unable to connect to server"))
184                    .action_slot(Button::new("reconnect", "Retry"))
185                    .into_any_element(),
186            ),
187        ];
188
189        Some(
190            example_group(severity_examples)
191                .vertical()
192                .into_any_element(),
193        )
194    }
195}