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