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(self.footer.is_some(), |this| this.pb_4())
81 .when_some(
82 self.container_scroll_handler,
83 |this, container_scroll_handle| {
84 this.overflow_y_scroll()
85 .track_scroll(&container_scroll_handle)
86 },
87 )
88 .children(self.children),
89 )
90 .children(self.footer)
91 }
92}
93
94#[derive(IntoElement)]
95pub struct ModalHeader {
96 icon: Option<Icon>,
97 headline: Option<SharedString>,
98 description: Option<SharedString>,
99 children: SmallVec<[AnyElement; 2]>,
100 show_dismiss_button: bool,
101 show_back_button: bool,
102}
103
104impl Default for ModalHeader {
105 fn default() -> Self {
106 Self::new()
107 }
108}
109
110impl ModalHeader {
111 pub fn new() -> Self {
112 Self {
113 icon: None,
114 headline: None,
115 description: None,
116 children: SmallVec::new(),
117 show_dismiss_button: false,
118 show_back_button: false,
119 }
120 }
121
122 pub fn icon(mut self, icon: Icon) -> Self {
123 self.icon = Some(icon);
124 self
125 }
126
127 /// Set the headline of the modal.
128 ///
129 /// This will insert the headline as the first item
130 /// of `children` if it is not already present.
131 pub fn headline(mut self, headline: impl Into<SharedString>) -> Self {
132 self.headline = Some(headline.into());
133 self
134 }
135
136 pub fn description(mut self, description: impl Into<SharedString>) -> Self {
137 self.description = Some(description.into());
138 self
139 }
140
141 pub fn show_dismiss_button(mut self, show: bool) -> Self {
142 self.show_dismiss_button = show;
143 self
144 }
145
146 pub fn show_back_button(mut self, show: bool) -> Self {
147 self.show_back_button = show;
148 self
149 }
150}
151
152impl ParentElement for ModalHeader {
153 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
154 self.children.extend(elements)
155 }
156}
157
158impl RenderOnce for ModalHeader {
159 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
160 let mut children = self.children;
161
162 if self.headline.is_some() {
163 children.insert(
164 0,
165 Headline::new(self.headline.unwrap())
166 .size(HeadlineSize::XSmall)
167 .color(Color::Muted)
168 .into_any_element(),
169 );
170 }
171
172 h_flex()
173 .flex_none()
174 .justify_between()
175 .w_full()
176 .px(DynamicSpacing::Base12.rems(cx))
177 .pt(DynamicSpacing::Base08.rems(cx))
178 .pb(DynamicSpacing::Base04.rems(cx))
179 .gap(DynamicSpacing::Base08.rems(cx))
180 .when(self.show_back_button, |this| {
181 this.child(
182 IconButton::new("back", IconName::ArrowLeft)
183 .shape(IconButtonShape::Square)
184 .on_click(|_, window, cx| {
185 window.dispatch_action(menu::Cancel.boxed_clone(), cx);
186 }),
187 )
188 })
189 .child(
190 v_flex()
191 .flex_1()
192 .child(
193 h_flex()
194 .gap_1()
195 .when_some(self.icon, |this, icon| this.child(icon))
196 .children(children),
197 )
198 .when_some(self.description, |this, description| {
199 this.child(Label::new(description).color(Color::Muted).mb_2())
200 }),
201 )
202 .when(self.show_dismiss_button, |this| {
203 this.child(
204 IconButton::new("dismiss", IconName::Close)
205 .shape(IconButtonShape::Square)
206 .on_click(|_, window, cx| {
207 window.dispatch_action(menu::Cancel.boxed_clone(), cx);
208 }),
209 )
210 })
211 }
212}
213
214#[derive(IntoElement)]
215pub struct ModalRow {
216 children: SmallVec<[AnyElement; 2]>,
217}
218
219impl Default for ModalRow {
220 fn default() -> Self {
221 Self::new()
222 }
223}
224
225impl ModalRow {
226 pub fn new() -> Self {
227 Self {
228 children: SmallVec::new(),
229 }
230 }
231}
232
233impl ParentElement for ModalRow {
234 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
235 self.children.extend(elements)
236 }
237}
238
239impl RenderOnce for ModalRow {
240 fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
241 h_flex().w_full().py_1().children(self.children)
242 }
243}
244
245#[derive(IntoElement)]
246pub struct ModalFooter {
247 start_slot: Option<AnyElement>,
248 end_slot: Option<AnyElement>,
249}
250
251impl Default for ModalFooter {
252 fn default() -> Self {
253 Self::new()
254 }
255}
256
257impl ModalFooter {
258 pub fn new() -> Self {
259 Self {
260 start_slot: None,
261 end_slot: None,
262 }
263 }
264
265 pub fn start_slot<E: IntoElement>(mut self, start_slot: impl Into<Option<E>>) -> Self {
266 self.start_slot = start_slot.into().map(IntoElement::into_any_element);
267 self
268 }
269
270 pub fn end_slot<E: IntoElement>(mut self, end_slot: impl Into<Option<E>>) -> Self {
271 self.end_slot = end_slot.into().map(IntoElement::into_any_element);
272 self
273 }
274}
275
276impl RenderOnce for ModalFooter {
277 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
278 h_flex()
279 .w_full()
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}