modal.rs

  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}