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 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}