modal.rs

  1#![allow(missing_docs)]
  2
  3use crate::{
  4    h_flex, v_flex, Clickable, Color, DynamicSpacing, Headline, HeadlineSize, IconButton,
  5    IconButtonShape, IconName, Label, LabelCommon, LabelSize,
  6};
  7use gpui::{prelude::FluentBuilder, *};
  8use smallvec::SmallVec;
  9use theme::ActiveTheme;
 10
 11#[derive(IntoElement)]
 12pub struct Modal {
 13    id: ElementId,
 14    header: ModalHeader,
 15    children: SmallVec<[AnyElement; 2]>,
 16    footer: Option<ModalFooter>,
 17    container_id: ElementId,
 18    container_scroll_handler: Option<ScrollHandle>,
 19}
 20
 21impl Modal {
 22    pub fn new(id: impl Into<SharedString>, scroll_handle: Option<ScrollHandle>) -> Self {
 23        let id = id.into();
 24
 25        let container_id = ElementId::Name(format!("{}_container", id.clone()).into());
 26        Self {
 27            id: ElementId::Name(id),
 28            header: ModalHeader::new(),
 29            children: SmallVec::new(),
 30            footer: None,
 31            container_id,
 32            container_scroll_handler: scroll_handle,
 33        }
 34    }
 35
 36    pub fn header(mut self, header: ModalHeader) -> Self {
 37        self.header = header;
 38        self
 39    }
 40
 41    pub fn section(mut self, section: Section) -> Self {
 42        self.children.push(section.into_any_element());
 43        self
 44    }
 45
 46    pub fn footer(mut self, footer: ModalFooter) -> Self {
 47        self.footer = Some(footer);
 48        self
 49    }
 50
 51    pub fn show_dismiss(mut self, show: bool) -> Self {
 52        self.header.show_dismiss_button = show;
 53        self
 54    }
 55
 56    pub fn show_back(mut self, show: bool) -> Self {
 57        self.header.show_back_button = show;
 58        self
 59    }
 60}
 61
 62impl ParentElement for Modal {
 63    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
 64        self.children.extend(elements)
 65    }
 66}
 67
 68impl RenderOnce for Modal {
 69    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
 70        v_flex()
 71            .id(self.id.clone())
 72            .size_full()
 73            .flex_1()
 74            .overflow_hidden()
 75            .child(self.header)
 76            .child(
 77                v_flex()
 78                    .id(self.container_id.clone())
 79                    .w_full()
 80                    .flex_1()
 81                    .gap(DynamicSpacing::Base08.rems(cx))
 82                    .when_some(
 83                        self.container_scroll_handler,
 84                        |this, container_scroll_handle| {
 85                            this.overflow_y_scroll()
 86                                .track_scroll(&container_scroll_handle)
 87                        },
 88                    )
 89                    .children(self.children),
 90            )
 91            .children(self.footer)
 92    }
 93}
 94
 95#[derive(IntoElement)]
 96pub struct ModalHeader {
 97    headline: 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            headline: None,
113            children: SmallVec::new(),
114            show_dismiss_button: false,
115            show_back_button: false,
116        }
117    }
118
119    /// Set the headline of the modal.
120    ///
121    /// This will insert the headline as the first item
122    /// of `children` if it is not already present.
123    pub fn headline(mut self, headline: impl Into<SharedString>) -> Self {
124        self.headline = Some(headline.into());
125        self
126    }
127
128    pub fn show_dismiss_button(mut self, show: bool) -> Self {
129        self.show_dismiss_button = show;
130        self
131    }
132
133    pub fn show_back_button(mut self, show: bool) -> Self {
134        self.show_back_button = show;
135        self
136    }
137}
138
139impl ParentElement for ModalHeader {
140    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
141        self.children.extend(elements)
142    }
143}
144
145impl RenderOnce for ModalHeader {
146    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
147        let mut children = self.children;
148
149        if self.headline.is_some() {
150            children.insert(
151                0,
152                Headline::new(self.headline.unwrap())
153                    .size(HeadlineSize::XSmall)
154                    .color(Color::Muted)
155                    .into_any_element(),
156            );
157        }
158
159        h_flex()
160            .flex_none()
161            .justify_between()
162            .w_full()
163            .px(DynamicSpacing::Base12.rems(cx))
164            .pt(DynamicSpacing::Base08.rems(cx))
165            .pb(DynamicSpacing::Base04.rems(cx))
166            .gap(DynamicSpacing::Base08.rems(cx))
167            .when(self.show_back_button, |this| {
168                this.child(
169                    IconButton::new("back", IconName::ArrowLeft)
170                        .shape(IconButtonShape::Square)
171                        .on_click(|_, window, cx| {
172                            window.dispatch_action(menu::Cancel.boxed_clone(), cx);
173                        }),
174                )
175            })
176            .child(div().flex_1().children(children))
177            .when(self.show_dismiss_button, |this| {
178                this.child(
179                    IconButton::new("dismiss", IconName::Close)
180                        .shape(IconButtonShape::Square)
181                        .on_click(|_, window, cx| {
182                            window.dispatch_action(menu::Cancel.boxed_clone(), cx);
183                        }),
184                )
185            })
186    }
187}
188
189#[derive(IntoElement)]
190pub struct ModalRow {
191    children: SmallVec<[AnyElement; 2]>,
192}
193
194impl Default for ModalRow {
195    fn default() -> Self {
196        Self::new()
197    }
198}
199
200impl ModalRow {
201    pub fn new() -> Self {
202        Self {
203            children: SmallVec::new(),
204        }
205    }
206}
207
208impl ParentElement for ModalRow {
209    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
210        self.children.extend(elements)
211    }
212}
213
214impl RenderOnce for ModalRow {
215    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
216        h_flex().w_full().py_1().children(self.children)
217    }
218}
219
220#[derive(IntoElement)]
221pub struct ModalFooter {
222    start_slot: Option<AnyElement>,
223    end_slot: Option<AnyElement>,
224}
225
226impl Default for ModalFooter {
227    fn default() -> Self {
228        Self::new()
229    }
230}
231
232impl ModalFooter {
233    pub fn new() -> Self {
234        Self {
235            start_slot: None,
236            end_slot: None,
237        }
238    }
239
240    pub fn start_slot<E: IntoElement>(mut self, start_slot: impl Into<Option<E>>) -> Self {
241        self.start_slot = start_slot.into().map(IntoElement::into_any_element);
242        self
243    }
244
245    pub fn end_slot<E: IntoElement>(mut self, end_slot: impl Into<Option<E>>) -> Self {
246        self.end_slot = end_slot.into().map(IntoElement::into_any_element);
247        self
248    }
249}
250
251impl RenderOnce for ModalFooter {
252    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
253        h_flex()
254            .flex_none()
255            .w_full()
256            .p(DynamicSpacing::Base08.rems(cx))
257            .justify_between()
258            .child(div().when_some(self.start_slot, |this, start_slot| this.child(start_slot)))
259            .child(div().when_some(self.end_slot, |this, end_slot| this.child(end_slot)))
260    }
261}
262
263#[derive(IntoElement)]
264pub struct Section {
265    contained: bool,
266    padded: bool,
267    header: Option<SectionHeader>,
268    meta: Option<SharedString>,
269    children: SmallVec<[AnyElement; 2]>,
270}
271
272impl Default for Section {
273    fn default() -> Self {
274        Self::new()
275    }
276}
277
278impl Section {
279    pub fn new() -> Self {
280        Self {
281            contained: false,
282            padded: true,
283            header: None,
284            meta: None,
285            children: SmallVec::new(),
286        }
287    }
288
289    pub fn new_contained() -> Self {
290        Self {
291            contained: true,
292            padded: true,
293            header: None,
294            meta: None,
295            children: SmallVec::new(),
296        }
297    }
298
299    pub fn contained(mut self, contained: bool) -> Self {
300        self.contained = contained;
301        self
302    }
303
304    pub fn header(mut self, header: SectionHeader) -> Self {
305        self.header = Some(header);
306        self
307    }
308
309    pub fn meta(mut self, meta: impl Into<SharedString>) -> Self {
310        self.meta = Some(meta.into());
311        self
312    }
313    pub fn padded(mut self, padded: bool) -> Self {
314        self.padded = padded;
315        self
316    }
317}
318
319impl ParentElement for Section {
320    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
321        self.children.extend(elements)
322    }
323}
324
325impl RenderOnce for Section {
326    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
327        let mut section_bg = cx.theme().colors().text;
328        section_bg.fade_out(0.96);
329
330        let children = if self.contained {
331            v_flex()
332                .flex_1()
333                .when(self.padded, |this| this.px(DynamicSpacing::Base12.rems(cx)))
334                .child(
335                    v_flex()
336                        .w_full()
337                        .rounded_md()
338                        .border_1()
339                        .border_color(cx.theme().colors().border)
340                        .bg(section_bg)
341                        .py(DynamicSpacing::Base06.rems(cx))
342                        .gap_y(DynamicSpacing::Base04.rems(cx))
343                        .child(div().flex().flex_1().size_full().children(self.children)),
344                )
345        } else {
346            v_flex()
347                .w_full()
348                .flex_1()
349                .gap_y(DynamicSpacing::Base04.rems(cx))
350                .when(self.padded, |this| {
351                    this.px(DynamicSpacing::Base06.rems(cx) + DynamicSpacing::Base06.rems(cx))
352                })
353                .children(self.children)
354        };
355
356        v_flex()
357            .size_full()
358            .flex_1()
359            .child(
360                v_flex()
361                    .flex_none()
362                    .px(DynamicSpacing::Base12.rems(cx))
363                    .children(self.header)
364                    .when_some(self.meta, |this, meta| {
365                        this.child(Label::new(meta).size(LabelSize::Small).color(Color::Muted))
366                    }),
367            )
368            .child(children)
369            // fill any leftover space
370            .child(div().flex().flex_1())
371    }
372}
373
374#[derive(IntoElement)]
375pub struct SectionHeader {
376    /// The label of the header.
377    label: SharedString,
378    /// A slot for content that appears after the label, usually on the other side of the header.
379    /// This might be a button, a disclosure arrow, a face pile, etc.
380    end_slot: Option<AnyElement>,
381}
382
383impl SectionHeader {
384    pub fn new(label: impl Into<SharedString>) -> Self {
385        Self {
386            label: label.into(),
387            end_slot: None,
388        }
389    }
390
391    pub fn end_slot<E: IntoElement>(mut self, end_slot: impl Into<Option<E>>) -> Self {
392        self.end_slot = end_slot.into().map(IntoElement::into_any_element);
393        self
394    }
395}
396
397impl RenderOnce for SectionHeader {
398    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
399        h_flex()
400            .id(self.label.clone())
401            .w_full()
402            .px(DynamicSpacing::Base08.rems(cx))
403            .child(
404                div()
405                    .h_7()
406                    .flex()
407                    .items_center()
408                    .justify_between()
409                    .w_full()
410                    .gap(DynamicSpacing::Base04.rems(cx))
411                    .child(
412                        div().flex_1().child(
413                            Label::new(self.label.clone())
414                                .size(LabelSize::Small)
415                                .into_element(),
416                        ),
417                    )
418                    .child(h_flex().children(self.end_slot)),
419            )
420    }
421}
422
423impl From<SharedString> for SectionHeader {
424    fn from(val: SharedString) -> Self {
425        SectionHeader::new(val)
426    }
427}
428
429impl From<&'static str> for SectionHeader {
430    fn from(val: &'static str) -> Self {
431        let label: SharedString = val.into();
432        SectionHeader::new(label)
433    }
434}