1use crate::component_prelude::*;
2use crate::prelude::*;
3use crate::{Checkbox, ListBulletItem, ToggleState};
4use gpui::Action;
5use gpui::FocusHandle;
6use gpui::IntoElement;
7use gpui::Stateful;
8use smallvec::{SmallVec, smallvec};
9use theme::ActiveTheme;
10
11type ActionHandler = Box<dyn FnOnce(Stateful<Div>) -> Stateful<Div>>;
12
13#[derive(IntoElement, RegisterComponent)]
14pub struct AlertModal {
15 id: ElementId,
16 header: Option<AnyElement>,
17 children: SmallVec<[AnyElement; 2]>,
18 footer: Option<AnyElement>,
19 title: Option<SharedString>,
20 primary_action: Option<SharedString>,
21 dismiss_label: Option<SharedString>,
22 width: Option<DefiniteLength>,
23 key_context: Option<String>,
24 action_handlers: Vec<ActionHandler>,
25 focus_handle: Option<FocusHandle>,
26}
27
28impl AlertModal {
29 pub fn new(id: impl Into<ElementId>) -> Self {
30 Self {
31 id: id.into(),
32 header: None,
33 children: smallvec![],
34 footer: None,
35 title: None,
36 primary_action: None,
37 dismiss_label: None,
38 width: None,
39 key_context: None,
40 action_handlers: Vec::new(),
41 focus_handle: None,
42 }
43 }
44
45 pub fn title(mut self, title: impl Into<SharedString>) -> Self {
46 self.title = Some(title.into());
47 self
48 }
49
50 pub fn header(mut self, header: impl IntoElement) -> Self {
51 self.header = Some(header.into_any_element());
52 self
53 }
54
55 pub fn footer(mut self, footer: impl IntoElement) -> Self {
56 self.footer = Some(footer.into_any_element());
57 self
58 }
59
60 pub fn primary_action(mut self, primary_action: impl Into<SharedString>) -> Self {
61 self.primary_action = Some(primary_action.into());
62 self
63 }
64
65 pub fn dismiss_label(mut self, dismiss_label: impl Into<SharedString>) -> Self {
66 self.dismiss_label = Some(dismiss_label.into());
67 self
68 }
69
70 pub fn width(mut self, width: impl Into<DefiniteLength>) -> Self {
71 self.width = Some(width.into());
72 self
73 }
74
75 pub fn key_context(mut self, key_context: impl Into<String>) -> Self {
76 self.key_context = Some(key_context.into());
77 self
78 }
79
80 pub fn on_action<A: Action>(
81 mut self,
82 listener: impl Fn(&A, &mut Window, &mut App) + 'static,
83 ) -> Self {
84 self.action_handlers
85 .push(Box::new(move |div| div.on_action(listener)));
86 self
87 }
88
89 pub fn track_focus(mut self, focus_handle: &gpui::FocusHandle) -> Self {
90 self.focus_handle = Some(focus_handle.clone());
91 self
92 }
93}
94
95impl RenderOnce for AlertModal {
96 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
97 let width = self.width.unwrap_or_else(|| px(440.).into());
98 let has_default_footer = self.primary_action.is_some() || self.dismiss_label.is_some();
99
100 let mut modal = v_flex()
101 .when_some(self.key_context, |this, key_context| {
102 this.key_context(key_context.as_str())
103 })
104 .when_some(self.focus_handle, |this, focus_handle| {
105 this.track_focus(&focus_handle)
106 })
107 .id(self.id)
108 .elevation_3(cx)
109 .w(width)
110 .bg(cx.theme().colors().elevated_surface_background)
111 .overflow_hidden();
112
113 for handler in self.action_handlers {
114 modal = handler(modal);
115 }
116
117 if let Some(header) = self.header {
118 modal = modal.child(header);
119 } else if let Some(title) = self.title {
120 modal = modal.child(
121 v_flex()
122 .pt_3()
123 .pr_3()
124 .pl_3()
125 .pb_1()
126 .child(Headline::new(title).size(HeadlineSize::Small)),
127 );
128 }
129
130 if !self.children.is_empty() {
131 modal = modal.child(
132 v_flex()
133 .p_3()
134 .text_ui(cx)
135 .text_color(Color::Muted.color(cx))
136 .gap_1()
137 .children(self.children),
138 );
139 }
140
141 if let Some(footer) = self.footer {
142 modal = modal.child(footer);
143 } else if has_default_footer {
144 let primary_action = self.primary_action.unwrap_or_else(|| "Ok".into());
145 let dismiss_label = self.dismiss_label.unwrap_or_else(|| "Cancel".into());
146
147 modal = modal.child(
148 h_flex()
149 .p_3()
150 .items_center()
151 .justify_end()
152 .gap_1()
153 .child(Button::new(dismiss_label.clone(), dismiss_label).color(Color::Muted))
154 .child(Button::new(primary_action.clone(), primary_action)),
155 );
156 }
157
158 modal
159 }
160}
161
162impl ParentElement for AlertModal {
163 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
164 self.children.extend(elements)
165 }
166}
167
168impl Component for AlertModal {
169 fn scope() -> ComponentScope {
170 ComponentScope::Notification
171 }
172
173 fn status() -> ComponentStatus {
174 ComponentStatus::WorkInProgress
175 }
176
177 fn description() -> Option<&'static str> {
178 Some("A modal dialog that presents an alert message with primary and dismiss actions.")
179 }
180
181 fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
182 Some(
183 v_flex()
184 .gap_6()
185 .p_4()
186 .children(vec![
187 example_group(vec![single_example(
188 "Basic Alert",
189 AlertModal::new("simple-modal")
190 .title("Do you want to leave the current call?")
191 .child(
192 "The current window will be closed, and connections to any shared projects will be terminated."
193 )
194 .primary_action("Leave Call")
195 .dismiss_label("Cancel")
196 .into_any_element(),
197 )]),
198 example_group(vec![single_example(
199 "Custom Header",
200 AlertModal::new("custom-header-modal")
201 .header(
202 v_flex()
203 .p_3()
204 .bg(cx.theme().colors().background)
205 .gap_1()
206 .child(
207 h_flex()
208 .gap_1()
209 .child(Icon::new(IconName::Warning).color(Color::Warning))
210 .child(Headline::new("Unrecognized Workspace").size(HeadlineSize::Small))
211 )
212 .child(
213 h_flex()
214 .pl(IconSize::default().rems() + rems(0.5))
215 .child(Label::new("~/projects/my-project").color(Color::Muted))
216 )
217 )
218 .child(
219 "Untrusted workspaces are opened in Restricted Mode to protect your system.
220Review .zed/settings.json for any extensions or commands configured by this project.",
221 )
222 .child(
223 v_flex()
224 .mt_1()
225 .child(Label::new("Restricted mode prevents:").color(Color::Muted))
226 .child(ListBulletItem::new("Project settings from being applied"))
227 .child(ListBulletItem::new("Language servers from running"))
228 .child(ListBulletItem::new("MCP integrations from installing"))
229 )
230 .footer(
231 h_flex()
232 .p_3()
233 .justify_between()
234 .child(
235 Checkbox::new("trust-parent", ToggleState::Unselected)
236 .label("Trust all projects in parent directory")
237 )
238 .child(
239 h_flex()
240 .gap_1()
241 .child(Button::new("restricted", "Stay in Restricted Mode").color(Color::Muted))
242 .child(Button::new("trust", "Trust and Continue").style(ButtonStyle::Filled))
243 )
244 )
245 .width(rems(40.))
246 .into_any_element(),
247 )]),
248 ])
249 .into_any_element(),
250 )
251 }
252}