modal.rs

  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}