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}