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            .min_w_0()
 71            .py_0p5()
 72            .gap_1p5()
 73            .when(self.wrap_content, |this| this.flex_wrap())
 74            .justify_between()
 75            .rounded_sm()
 76            .border_1();
 77
 78        let (icon, icon_color, bg_color, border_color) = match self.severity {
 79            Severity::Info => (
 80                IconName::Info,
 81                Color::Muted,
 82                cx.theme().status().info_background.opacity(0.5),
 83                cx.theme().colors().border.opacity(0.5),
 84            ),
 85            Severity::Success => (
 86                IconName::Check,
 87                Color::Success,
 88                cx.theme().status().success.opacity(0.1),
 89                cx.theme().status().success.opacity(0.2),
 90            ),
 91            Severity::Warning => (
 92                IconName::Warning,
 93                Color::Warning,
 94                cx.theme().status().warning_background.opacity(0.5),
 95                cx.theme().status().warning_border.opacity(0.4),
 96            ),
 97            Severity::Error => (
 98                IconName::XCircle,
 99                Color::Error,
100                cx.theme().status().error.opacity(0.1),
101                cx.theme().status().error.opacity(0.2),
102            ),
103        };
104
105        let mut banner = banner.bg(bg_color).border_color(border_color);
106
107        let icon_and_child = h_flex()
108            .items_start()
109            .min_w_0()
110            .flex_1()
111            .gap_1p5()
112            .child(
113                h_flex()
114                    .h(window.line_height())
115                    .flex_shrink_0()
116                    .child(Icon::new(icon).size(IconSize::XSmall).color(icon_color)),
117            )
118            .child(div().min_w_0().flex_1().children(self.children));
119
120        if let Some(action_slot) = self.action_slot {
121            banner = banner
122                .pl_2()
123                .pr_1()
124                .child(icon_and_child)
125                .child(action_slot);
126        } else {
127            banner = banner.px_2().child(icon_and_child);
128        }
129
130        banner
131    }
132}
133
134impl Component for Banner {
135    fn scope() -> ComponentScope {
136        ComponentScope::DataDisplay
137    }
138
139    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
140        let severity_examples = vec![
141            single_example(
142                "Default",
143                Banner::new()
144                    .child(Label::new("This is a default banner with no customization"))
145                    .into_any_element(),
146            ),
147            single_example(
148                "Info",
149                Banner::new()
150                    .severity(Severity::Info)
151                    .child(Label::new("This is an informational message"))
152                    .action_slot(
153                        Button::new("learn-more", "Learn More")
154                            .icon(IconName::ArrowUpRight)
155                            .icon_size(IconSize::Small)
156                            .icon_position(IconPosition::End),
157                    )
158                    .into_any_element(),
159            ),
160            single_example(
161                "Success",
162                Banner::new()
163                    .severity(Severity::Success)
164                    .child(Label::new("Operation completed successfully"))
165                    .action_slot(Button::new("dismiss", "Dismiss"))
166                    .into_any_element(),
167            ),
168            single_example(
169                "Warning",
170                Banner::new()
171                    .severity(Severity::Warning)
172                    .child(Label::new("Your settings file uses deprecated settings"))
173                    .action_slot(Button::new("update", "Update Settings"))
174                    .into_any_element(),
175            ),
176            single_example(
177                "Error",
178                Banner::new()
179                    .severity(Severity::Error)
180                    .child(Label::new("Connection error: unable to connect to server"))
181                    .action_slot(Button::new("reconnect", "Retry"))
182                    .into_any_element(),
183            ),
184        ];
185
186        Some(
187            example_group(severity_examples)
188                .vertical()
189                .into_any_element(),
190        )
191    }
192}