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