banner.rs

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