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}