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 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(
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 .mt_4()
280 .p(DynamicSpacing::Base08.rems(cx))
281 .flex_none()
282 .justify_between()
283 .gap_1()
284 .border_t_1()
285 .border_color(cx.theme().colors().border_variant)
286 .child(div().when_some(self.start_slot, |this, start_slot| this.child(start_slot)))
287 .child(div().when_some(self.end_slot, |this, end_slot| this.child(end_slot)))
288 }
289}
290
291#[derive(IntoElement)]
292pub struct Section {
293 contained: bool,
294 padded: bool,
295 header: Option<SectionHeader>,
296 meta: Option<SharedString>,
297 children: SmallVec<[AnyElement; 2]>,
298}
299
300impl Default for Section {
301 fn default() -> Self {
302 Self::new()
303 }
304}
305
306impl Section {
307 pub fn new() -> Self {
308 Self {
309 contained: false,
310 padded: true,
311 header: None,
312 meta: None,
313 children: SmallVec::new(),
314 }
315 }
316
317 pub fn new_contained() -> Self {
318 Self {
319 contained: true,
320 padded: true,
321 header: None,
322 meta: None,
323 children: SmallVec::new(),
324 }
325 }
326
327 pub fn contained(mut self, contained: bool) -> Self {
328 self.contained = contained;
329 self
330 }
331
332 pub fn header(mut self, header: SectionHeader) -> Self {
333 self.header = Some(header);
334 self
335 }
336
337 pub fn meta(mut self, meta: impl Into<SharedString>) -> Self {
338 self.meta = Some(meta.into());
339 self
340 }
341 pub fn padded(mut self, padded: bool) -> Self {
342 self.padded = padded;
343 self
344 }
345}
346
347impl ParentElement for Section {
348 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
349 self.children.extend(elements)
350 }
351}
352
353impl RenderOnce for Section {
354 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
355 let mut section_bg = cx.theme().colors().text;
356 section_bg.fade_out(0.96);
357
358 let children = if self.contained {
359 v_flex()
360 .flex_1()
361 .when(self.padded, |this| this.px(DynamicSpacing::Base12.rems(cx)))
362 .child(
363 v_flex()
364 .w_full()
365 .rounded_sm()
366 .border_1()
367 .border_color(cx.theme().colors().border)
368 .bg(section_bg)
369 .py(DynamicSpacing::Base06.rems(cx))
370 .gap_y(DynamicSpacing::Base04.rems(cx))
371 .child(div().flex().flex_1().size_full().children(self.children)),
372 )
373 } else {
374 v_flex()
375 .w_full()
376 .flex_1()
377 .gap_y(DynamicSpacing::Base04.rems(cx))
378 .when(self.padded, |this| {
379 this.px(DynamicSpacing::Base06.rems(cx) + DynamicSpacing::Base06.rems(cx))
380 })
381 .children(self.children)
382 };
383
384 v_flex()
385 .size_full()
386 .flex_1()
387 .child(
388 v_flex()
389 .flex_none()
390 .px(DynamicSpacing::Base12.rems(cx))
391 .children(self.header)
392 .when_some(self.meta, |this, meta| {
393 this.child(Label::new(meta).size(LabelSize::Small).color(Color::Muted))
394 }),
395 )
396 .child(children)
397 // fill any leftover space
398 .child(div().flex().flex_1())
399 }
400}
401
402#[derive(IntoElement)]
403pub struct SectionHeader {
404 /// The label of the header.
405 label: SharedString,
406 /// A slot for content that appears after the label, usually on the other side of the header.
407 /// This might be a button, a disclosure arrow, a face pile, etc.
408 end_slot: Option<AnyElement>,
409}
410
411impl SectionHeader {
412 pub fn new(label: impl Into<SharedString>) -> Self {
413 Self {
414 label: label.into(),
415 end_slot: None,
416 }
417 }
418
419 pub fn end_slot<E: IntoElement>(mut self, end_slot: impl Into<Option<E>>) -> Self {
420 self.end_slot = end_slot.into().map(IntoElement::into_any_element);
421 self
422 }
423}
424
425impl RenderOnce for SectionHeader {
426 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
427 h_flex()
428 .id(self.label.clone())
429 .w_full()
430 .px(DynamicSpacing::Base08.rems(cx))
431 .child(
432 div()
433 .h_7()
434 .flex()
435 .items_center()
436 .justify_between()
437 .w_full()
438 .gap(DynamicSpacing::Base04.rems(cx))
439 .child(
440 div().flex_1().child(
441 Label::new(self.label.clone())
442 .size(LabelSize::Small)
443 .into_element(),
444 ),
445 )
446 .child(h_flex().children(self.end_slot)),
447 )
448 }
449}
450
451impl From<SharedString> for SectionHeader {
452 fn from(val: SharedString) -> Self {
453 SectionHeader::new(val)
454 }
455}
456
457impl From<&'static str> for SectionHeader {
458 fn from(val: &'static str) -> Self {
459 let label: SharedString = val.into();
460 SectionHeader::new(label)
461 }
462}