1use crate::{
2 Clickable, Color, DynamicSpacing, Headline, HeadlineSize, Icon, IconButton, IconName, Label,
3 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 self.headline.is_some() {
162 children.insert(
163 0,
164 Headline::new(self.headline.unwrap())
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(IconButton::new("back", IconName::ArrowLeft).on_click(
181 |_, window, cx| {
182 window.dispatch_action(menu::Cancel.boxed_clone(), cx);
183 },
184 ))
185 })
186 .child(
187 v_flex()
188 .flex_1()
189 .child(
190 h_flex()
191 .gap_1()
192 .when_some(self.icon, |this, icon| this.child(icon))
193 .children(children),
194 )
195 .when_some(self.description, |this, description| {
196 this.child(Label::new(description).color(Color::Muted).mb_2())
197 }),
198 )
199 .when(self.show_dismiss_button, |this| {
200 this.child(
201 IconButton::new("dismiss", IconName::Close).on_click(|_, window, cx| {
202 window.dispatch_action(menu::Cancel.boxed_clone(), cx);
203 }),
204 )
205 })
206 }
207}
208
209#[derive(IntoElement)]
210pub struct ModalRow {
211 children: SmallVec<[AnyElement; 2]>,
212}
213
214impl Default for ModalRow {
215 fn default() -> Self {
216 Self::new()
217 }
218}
219
220impl ModalRow {
221 pub fn new() -> Self {
222 Self {
223 children: SmallVec::new(),
224 }
225 }
226}
227
228impl ParentElement for ModalRow {
229 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
230 self.children.extend(elements)
231 }
232}
233
234impl RenderOnce for ModalRow {
235 fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
236 h_flex().w_full().py_1().children(self.children)
237 }
238}
239
240#[derive(IntoElement)]
241pub struct ModalFooter {
242 start_slot: Option<AnyElement>,
243 end_slot: Option<AnyElement>,
244}
245
246impl Default for ModalFooter {
247 fn default() -> Self {
248 Self::new()
249 }
250}
251
252impl ModalFooter {
253 pub fn new() -> Self {
254 Self {
255 start_slot: None,
256 end_slot: None,
257 }
258 }
259
260 pub fn start_slot<E: IntoElement>(mut self, start_slot: impl Into<Option<E>>) -> Self {
261 self.start_slot = start_slot.into().map(IntoElement::into_any_element);
262 self
263 }
264
265 pub fn end_slot<E: IntoElement>(mut self, end_slot: impl Into<Option<E>>) -> Self {
266 self.end_slot = end_slot.into().map(IntoElement::into_any_element);
267 self
268 }
269}
270
271impl RenderOnce for ModalFooter {
272 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
273 h_flex()
274 .w_full()
275 .mt_4()
276 .p(DynamicSpacing::Base08.rems(cx))
277 .flex_none()
278 .justify_between()
279 .gap_1()
280 .border_t_1()
281 .border_color(cx.theme().colors().border_variant)
282 .child(div().when_some(self.start_slot, |this, start_slot| this.child(start_slot)))
283 .child(div().when_some(self.end_slot, |this, end_slot| this.child(end_slot)))
284 }
285}
286
287#[derive(IntoElement)]
288pub struct Section {
289 contained: bool,
290 padded: bool,
291 header: Option<SectionHeader>,
292 meta: Option<SharedString>,
293 children: SmallVec<[AnyElement; 2]>,
294}
295
296impl Default for Section {
297 fn default() -> Self {
298 Self::new()
299 }
300}
301
302impl Section {
303 pub fn new() -> Self {
304 Self {
305 contained: false,
306 padded: true,
307 header: None,
308 meta: None,
309 children: SmallVec::new(),
310 }
311 }
312
313 pub fn new_contained() -> Self {
314 Self {
315 contained: true,
316 padded: true,
317 header: None,
318 meta: None,
319 children: SmallVec::new(),
320 }
321 }
322
323 pub fn contained(mut self, contained: bool) -> Self {
324 self.contained = contained;
325 self
326 }
327
328 pub fn header(mut self, header: SectionHeader) -> Self {
329 self.header = Some(header);
330 self
331 }
332
333 pub fn meta(mut self, meta: impl Into<SharedString>) -> Self {
334 self.meta = Some(meta.into());
335 self
336 }
337 pub fn padded(mut self, padded: bool) -> Self {
338 self.padded = padded;
339 self
340 }
341}
342
343impl ParentElement for Section {
344 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
345 self.children.extend(elements)
346 }
347}
348
349impl RenderOnce for Section {
350 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
351 let mut section_bg = cx.theme().colors().text;
352 section_bg.fade_out(0.96);
353
354 let children = if self.contained {
355 v_flex()
356 .flex_1()
357 .when(self.padded, |this| this.px(DynamicSpacing::Base12.rems(cx)))
358 .child(
359 v_flex()
360 .w_full()
361 .rounded_sm()
362 .border_1()
363 .border_color(cx.theme().colors().border)
364 .bg(section_bg)
365 .py(DynamicSpacing::Base06.rems(cx))
366 .gap_y(DynamicSpacing::Base04.rems(cx))
367 .child(div().flex().flex_1().size_full().children(self.children)),
368 )
369 } else {
370 v_flex()
371 .w_full()
372 .flex_1()
373 .gap_y(DynamicSpacing::Base04.rems(cx))
374 .when(self.padded, |this| {
375 this.px(DynamicSpacing::Base06.rems(cx) + DynamicSpacing::Base06.rems(cx))
376 })
377 .children(self.children)
378 };
379
380 v_flex()
381 .size_full()
382 .flex_1()
383 .child(
384 v_flex()
385 .flex_none()
386 .px(DynamicSpacing::Base12.rems(cx))
387 .children(self.header)
388 .when_some(self.meta, |this, meta| {
389 this.child(Label::new(meta).size(LabelSize::Small).color(Color::Muted))
390 }),
391 )
392 .child(children)
393 // fill any leftover space
394 .child(div().flex().flex_1())
395 }
396}
397
398#[derive(IntoElement)]
399pub struct SectionHeader {
400 /// The label of the header.
401 label: SharedString,
402 /// A slot for content that appears after the label, usually on the other side of the header.
403 /// This might be a button, a disclosure arrow, a face pile, etc.
404 end_slot: Option<AnyElement>,
405}
406
407impl SectionHeader {
408 pub fn new(label: impl Into<SharedString>) -> Self {
409 Self {
410 label: label.into(),
411 end_slot: None,
412 }
413 }
414
415 pub fn end_slot<E: IntoElement>(mut self, end_slot: impl Into<Option<E>>) -> Self {
416 self.end_slot = end_slot.into().map(IntoElement::into_any_element);
417 self
418 }
419}
420
421impl RenderOnce for SectionHeader {
422 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
423 h_flex()
424 .id(self.label.clone())
425 .w_full()
426 .px(DynamicSpacing::Base08.rems(cx))
427 .child(
428 div()
429 .h_7()
430 .flex()
431 .items_center()
432 .justify_between()
433 .w_full()
434 .gap(DynamicSpacing::Base04.rems(cx))
435 .child(
436 div().flex_1().child(
437 Label::new(self.label.clone())
438 .size(LabelSize::Small)
439 .into_element(),
440 ),
441 )
442 .child(h_flex().children(self.end_slot)),
443 )
444 }
445}
446
447impl From<SharedString> for SectionHeader {
448 fn from(val: SharedString) -> Self {
449 SectionHeader::new(val)
450 }
451}
452
453impl From<&'static str> for SectionHeader {
454 fn from(val: &'static str) -> Self {
455 let label: SharedString = val.into();
456 SectionHeader::new(label)
457 }
458}