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