1use crate::{
2 Clickable, Color, DynamicSpacing, Headline, HeadlineSize, Icon, IconButton, IconButtonShape,
3 IconName, Label, LabelCommon, LabelSize, h_flex, v_flex,
4};
5use gpui::{prelude::FluentBuilder, *};
6use smallvec::SmallVec;
7use theme::ActiveTheme;
8
9#[derive(IntoElement)]
10pub struct Modal {
11 id: ElementId,
12 header: ModalHeader,
13 children: SmallVec<[AnyElement; 2]>,
14 footer: Option<ModalFooter>,
15 container_id: ElementId,
16 container_scroll_handler: Option<ScrollHandle>,
17 aria_label: Option<SharedString>,
18}
19
20impl Modal {
21 pub fn new(id: impl Into<SharedString>, scroll_handle: Option<ScrollHandle>) -> Self {
22 let id = id.into();
23
24 let container_id = ElementId::Name(format!("{}_container", id).into());
25 Self {
26 id: ElementId::Name(id),
27 header: ModalHeader::new(),
28 children: SmallVec::new(),
29 footer: None,
30 container_id,
31 container_scroll_handler: scroll_handle,
32 aria_label: None,
33 }
34 }
35
36 pub fn header(mut self, header: ModalHeader) -> Self {
37 self.aria_label = header.headline.clone();
38 self.header = header;
39 self
40 }
41
42 pub fn section(mut self, section: Section) -> Self {
43 self.children.push(section.into_any_element());
44 self
45 }
46
47 pub fn footer(mut self, footer: ModalFooter) -> Self {
48 self.footer = Some(footer);
49 self
50 }
51
52 pub fn show_dismiss(mut self, show: bool) -> Self {
53 self.header.show_dismiss_button = show;
54 self
55 }
56
57 pub fn show_back(mut self, show: bool) -> Self {
58 self.header.show_back_button = show;
59 self
60 }
61}
62
63impl ParentElement for Modal {
64 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
65 self.children.extend(elements)
66 }
67}
68
69impl RenderOnce for Modal {
70 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
71 v_flex()
72 .id(self.id.clone())
73 .role(Role::Dialog)
74 .when_some(self.aria_label, |this, label| this.aria_label(label))
75 .size_full()
76 .flex_1()
77 .overflow_hidden()
78 .child(self.header)
79 .child(
80 v_flex()
81 .id(self.container_id.clone())
82 .w_full()
83 .flex_1()
84 .gap(DynamicSpacing::Base08.rems(cx))
85 .when_some(
86 self.container_scroll_handler,
87 |this, container_scroll_handle| {
88 this.overflow_y_scroll()
89 .track_scroll(&container_scroll_handle)
90 },
91 )
92 .children(self.children),
93 )
94 .children(self.footer)
95 }
96}
97
98#[derive(IntoElement)]
99pub struct ModalHeader {
100 icon: Option<Icon>,
101 headline: Option<SharedString>,
102 description: Option<SharedString>,
103 children: SmallVec<[AnyElement; 2]>,
104 show_dismiss_button: bool,
105 show_back_button: bool,
106}
107
108impl Default for ModalHeader {
109 fn default() -> Self {
110 Self::new()
111 }
112}
113
114impl ModalHeader {
115 pub fn new() -> Self {
116 Self {
117 icon: None,
118 headline: None,
119 description: None,
120 children: SmallVec::new(),
121 show_dismiss_button: false,
122 show_back_button: false,
123 }
124 }
125
126 pub fn icon(mut self, icon: Icon) -> Self {
127 self.icon = Some(icon);
128 self
129 }
130
131 /// Set the headline of the modal.
132 ///
133 /// This will insert the headline as the first item
134 /// of `children` if it is not already present.
135 pub fn headline(mut self, headline: impl Into<SharedString>) -> Self {
136 self.headline = Some(headline.into());
137 self
138 }
139
140 pub fn description(mut self, description: impl Into<SharedString>) -> Self {
141 self.description = Some(description.into());
142 self
143 }
144
145 pub fn show_dismiss_button(mut self, show: bool) -> Self {
146 self.show_dismiss_button = show;
147 self
148 }
149
150 pub fn show_back_button(mut self, show: bool) -> Self {
151 self.show_back_button = show;
152 self
153 }
154}
155
156impl ParentElement for ModalHeader {
157 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
158 self.children.extend(elements)
159 }
160}
161
162impl RenderOnce for ModalHeader {
163 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
164 let mut children = self.children;
165
166 if let Some(headline) = self.headline {
167 children.insert(
168 0,
169 Headline::new(headline)
170 .size(HeadlineSize::XSmall)
171 .color(Color::Muted)
172 .into_any_element(),
173 );
174 }
175
176 h_flex()
177 .flex_none()
178 .justify_between()
179 .w_full()
180 .px(DynamicSpacing::Base12.rems(cx))
181 .pt(DynamicSpacing::Base08.rems(cx))
182 .pb(DynamicSpacing::Base04.rems(cx))
183 .gap(DynamicSpacing::Base08.rems(cx))
184 .when(self.show_back_button, |this| {
185 this.child(
186 IconButton::new("back", IconName::ArrowLeft)
187 .shape(IconButtonShape::Square)
188 .on_click(|_, window, cx| {
189 window.dispatch_action(menu::Cancel.boxed_clone(), cx);
190 }),
191 )
192 })
193 .child(
194 v_flex()
195 .flex_1()
196 .child(
197 h_flex()
198 .gap_1()
199 .when_some(self.icon, |this, icon| this.child(icon))
200 .children(children),
201 )
202 .when_some(self.description, |this, description| {
203 this.child(Label::new(description).color(Color::Muted).mb_2())
204 }),
205 )
206 .when(self.show_dismiss_button, |this| {
207 this.child(
208 IconButton::new("dismiss", IconName::Close)
209 .shape(IconButtonShape::Square)
210 .on_click(|_, window, cx| {
211 window.dispatch_action(menu::Cancel.boxed_clone(), cx);
212 }),
213 )
214 })
215 }
216}
217
218#[derive(IntoElement)]
219pub struct ModalRow {
220 children: SmallVec<[AnyElement; 2]>,
221}
222
223impl Default for ModalRow {
224 fn default() -> Self {
225 Self::new()
226 }
227}
228
229impl ModalRow {
230 pub fn new() -> Self {
231 Self {
232 children: SmallVec::new(),
233 }
234 }
235}
236
237impl ParentElement for ModalRow {
238 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
239 self.children.extend(elements)
240 }
241}
242
243impl RenderOnce for ModalRow {
244 fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
245 h_flex().w_full().py_1().children(self.children)
246 }
247}
248
249#[derive(IntoElement)]
250pub struct ModalFooter {
251 start_slot: Option<AnyElement>,
252 end_slot: Option<AnyElement>,
253}
254
255impl Default for ModalFooter {
256 fn default() -> Self {
257 Self::new()
258 }
259}
260
261impl ModalFooter {
262 pub fn new() -> Self {
263 Self {
264 start_slot: None,
265 end_slot: None,
266 }
267 }
268
269 pub fn start_slot<E: IntoElement>(mut self, start_slot: impl Into<Option<E>>) -> Self {
270 self.start_slot = start_slot.into().map(IntoElement::into_any_element);
271 self
272 }
273
274 pub fn end_slot<E: IntoElement>(mut self, end_slot: impl Into<Option<E>>) -> Self {
275 self.end_slot = end_slot.into().map(IntoElement::into_any_element);
276 self
277 }
278}
279
280impl RenderOnce for ModalFooter {
281 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
282 h_flex()
283 .w_full()
284 .p(DynamicSpacing::Base08.rems(cx))
285 .flex_none()
286 .justify_between()
287 .gap_1()
288 .border_t_1()
289 .border_color(cx.theme().colors().border_variant)
290 .child(div().when_some(self.start_slot, |this, start_slot| this.child(start_slot)))
291 .child(div().when_some(self.end_slot, |this, end_slot| this.child(end_slot)))
292 }
293}
294
295#[derive(IntoElement)]
296pub struct Section {
297 contained: bool,
298 padded: bool,
299 header: Option<SectionHeader>,
300 meta: Option<SharedString>,
301 children: SmallVec<[AnyElement; 2]>,
302}
303
304impl Default for Section {
305 fn default() -> Self {
306 Self::new()
307 }
308}
309
310impl Section {
311 pub fn new() -> Self {
312 Self {
313 contained: false,
314 padded: true,
315 header: None,
316 meta: None,
317 children: SmallVec::new(),
318 }
319 }
320
321 pub fn new_contained() -> Self {
322 Self {
323 contained: true,
324 padded: true,
325 header: None,
326 meta: None,
327 children: SmallVec::new(),
328 }
329 }
330
331 pub fn contained(mut self, contained: bool) -> Self {
332 self.contained = contained;
333 self
334 }
335
336 pub fn header(mut self, header: SectionHeader) -> Self {
337 self.header = Some(header);
338 self
339 }
340
341 pub fn meta(mut self, meta: impl Into<SharedString>) -> Self {
342 self.meta = Some(meta.into());
343 self
344 }
345 pub fn padded(mut self, padded: bool) -> Self {
346 self.padded = padded;
347 self
348 }
349}
350
351impl ParentElement for Section {
352 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
353 self.children.extend(elements)
354 }
355}
356
357impl RenderOnce for Section {
358 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
359 let mut section_bg = cx.theme().colors().text;
360 section_bg.fade_out(0.96);
361
362 let children = if self.contained {
363 v_flex()
364 .flex_1()
365 .when(self.padded, |this| this.px(DynamicSpacing::Base12.rems(cx)))
366 .child(
367 v_flex()
368 .w_full()
369 .rounded_sm()
370 .border_1()
371 .border_color(cx.theme().colors().border)
372 .bg(section_bg)
373 .child(
374 div()
375 .flex()
376 .flex_1()
377 .pb_2()
378 .size_full()
379 .children(self.children),
380 ),
381 )
382 } else {
383 v_flex()
384 .w_full()
385 .flex_1()
386 .gap_y(DynamicSpacing::Base04.rems(cx))
387 .pb_2()
388 .when(self.padded, |this| {
389 this.px(DynamicSpacing::Base06.rems(cx) + DynamicSpacing::Base06.rems(cx))
390 })
391 .children(self.children)
392 };
393
394 v_flex()
395 .size_full()
396 .flex_1()
397 .child(
398 v_flex()
399 .flex_none()
400 .px(DynamicSpacing::Base12.rems(cx))
401 .children(self.header)
402 .when_some(self.meta, |this, meta| {
403 this.child(Label::new(meta).size(LabelSize::Small).color(Color::Muted))
404 }),
405 )
406 .child(children)
407 // fill any leftover space
408 .child(div().flex().flex_1())
409 }
410}
411
412#[derive(IntoElement)]
413pub struct SectionHeader {
414 /// The label of the header.
415 label: SharedString,
416 /// A slot for content that appears after the label, usually on the other side of the header.
417 /// This might be a button, a disclosure arrow, a face pile, etc.
418 end_slot: Option<AnyElement>,
419}
420
421impl SectionHeader {
422 pub fn new(label: impl Into<SharedString>) -> Self {
423 Self {
424 label: label.into(),
425 end_slot: None,
426 }
427 }
428
429 pub fn end_slot<E: IntoElement>(mut self, end_slot: impl Into<Option<E>>) -> Self {
430 self.end_slot = end_slot.into().map(IntoElement::into_any_element);
431 self
432 }
433}
434
435impl RenderOnce for SectionHeader {
436 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
437 h_flex()
438 .id(self.label.clone())
439 .w_full()
440 .px(DynamicSpacing::Base08.rems(cx))
441 .child(
442 div()
443 .h_7()
444 .flex()
445 .items_center()
446 .justify_between()
447 .w_full()
448 .gap(DynamicSpacing::Base04.rems(cx))
449 .child(
450 div().flex_1().child(
451 Label::new(self.label.clone())
452 .size(LabelSize::Small)
453 .into_element(),
454 ),
455 )
456 .child(h_flex().children(self.end_slot)),
457 )
458 }
459}
460
461impl From<SharedString> for SectionHeader {
462 fn from(val: SharedString) -> Self {
463 SectionHeader::new(val)
464 }
465}
466
467impl From<&'static str> for SectionHeader {
468 fn from(val: &'static str) -> Self {
469 let label: SharedString = val.into();
470 SectionHeader::new(label)
471 }
472}