modal.rs

  1#![allow(missing_docs)]
  2
  3use crate::{
  4    h_flex, v_flex, Clickable, Color, Headline, HeadlineSize, IconButton, IconButtonShape,
  5    IconName, Label, LabelCommon, LabelSize, Spacing,
  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, cx: &mut WindowContext) -> 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                    .gap(Spacing::Large.rems(cx))
 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    headline: Option<SharedString>,
 97    children: SmallVec<[AnyElement; 2]>,
 98    show_dismiss_button: bool,
 99    show_back_button: bool,
100}
101
102impl Default for ModalHeader {
103    fn default() -> Self {
104        Self::new()
105    }
106}
107
108impl ModalHeader {
109    pub fn new() -> Self {
110        Self {
111            headline: None,
112            children: SmallVec::new(),
113            show_dismiss_button: false,
114            show_back_button: false,
115        }
116    }
117
118    /// Set the headline of the modal.
119    ///
120    /// This will insert the headline as the first item
121    /// of `children` if it is not already present.
122    pub fn headline(mut self, headline: impl Into<SharedString>) -> Self {
123        self.headline = Some(headline.into());
124        self
125    }
126
127    pub fn show_dismiss_button(mut self, show: bool) -> Self {
128        self.show_dismiss_button = show;
129        self
130    }
131
132    pub fn show_back_button(mut self, show: bool) -> Self {
133        self.show_back_button = show;
134        self
135    }
136}
137
138impl ParentElement for ModalHeader {
139    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
140        self.children.extend(elements)
141    }
142}
143
144impl RenderOnce for ModalHeader {
145    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
146        let mut children = self.children;
147
148        if self.headline.is_some() {
149            children.insert(
150                0,
151                Headline::new(self.headline.unwrap())
152                    .size(HeadlineSize::XSmall)
153                    .color(Color::Muted)
154                    .into_any_element(),
155            );
156        }
157
158        h_flex()
159            .flex_none()
160            .justify_between()
161            .w_full()
162            .px(Spacing::XLarge.rems(cx))
163            .pt(Spacing::Large.rems(cx))
164            .pb(Spacing::Small.rems(cx))
165            .gap(Spacing::Large.rems(cx))
166            .when(self.show_back_button, |this| {
167                this.child(
168                    IconButton::new("back", IconName::ArrowLeft)
169                        .shape(IconButtonShape::Square)
170                        .on_click(|_, cx| {
171                            cx.dispatch_action(menu::Cancel.boxed_clone());
172                        }),
173                )
174            })
175            .child(div().flex_1().children(children))
176            .when(self.show_dismiss_button, |this| {
177                this.child(
178                    IconButton::new("dismiss", IconName::Close)
179                        .shape(IconButtonShape::Square)
180                        .on_click(|_, cx| {
181                            cx.dispatch_action(menu::Cancel.boxed_clone());
182                        }),
183                )
184            })
185    }
186}
187
188#[derive(IntoElement)]
189pub struct ModalRow {
190    children: SmallVec<[AnyElement; 2]>,
191}
192
193impl Default for ModalRow {
194    fn default() -> Self {
195        Self::new()
196    }
197}
198
199impl ModalRow {
200    pub fn new() -> Self {
201        Self {
202            children: SmallVec::new(),
203        }
204    }
205}
206
207impl ParentElement for ModalRow {
208    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
209        self.children.extend(elements)
210    }
211}
212
213impl RenderOnce for ModalRow {
214    fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
215        h_flex().w_full().py_1().children(self.children)
216    }
217}
218
219#[derive(IntoElement)]
220pub struct ModalFooter {
221    start_slot: Option<AnyElement>,
222    end_slot: Option<AnyElement>,
223}
224
225impl Default for ModalFooter {
226    fn default() -> Self {
227        Self::new()
228    }
229}
230
231impl ModalFooter {
232    pub fn new() -> Self {
233        Self {
234            start_slot: None,
235            end_slot: None,
236        }
237    }
238
239    pub fn start_slot<E: IntoElement>(mut self, start_slot: impl Into<Option<E>>) -> Self {
240        self.start_slot = start_slot.into().map(IntoElement::into_any_element);
241        self
242    }
243
244    pub fn end_slot<E: IntoElement>(mut self, end_slot: impl Into<Option<E>>) -> Self {
245        self.end_slot = end_slot.into().map(IntoElement::into_any_element);
246        self
247    }
248}
249
250impl RenderOnce for ModalFooter {
251    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
252        h_flex()
253            .flex_none()
254            .w_full()
255            .p(Spacing::Large.rems(cx))
256            .justify_between()
257            .child(div().when_some(self.start_slot, |this, start_slot| this.child(start_slot)))
258            .child(div().when_some(self.end_slot, |this, end_slot| this.child(end_slot)))
259    }
260}
261
262#[derive(IntoElement)]
263pub struct Section {
264    contained: bool,
265    padded: bool,
266    header: Option<SectionHeader>,
267    meta: Option<SharedString>,
268    children: SmallVec<[AnyElement; 2]>,
269}
270
271impl Default for Section {
272    fn default() -> Self {
273        Self::new()
274    }
275}
276
277impl Section {
278    pub fn new() -> Self {
279        Self {
280            contained: false,
281            padded: true,
282            header: None,
283            meta: None,
284            children: SmallVec::new(),
285        }
286    }
287
288    pub fn new_contained() -> Self {
289        Self {
290            contained: true,
291            padded: true,
292            header: None,
293            meta: None,
294            children: SmallVec::new(),
295        }
296    }
297
298    pub fn contained(mut self, contained: bool) -> Self {
299        self.contained = contained;
300        self
301    }
302
303    pub fn header(mut self, header: SectionHeader) -> Self {
304        self.header = Some(header);
305        self
306    }
307
308    pub fn meta(mut self, meta: impl Into<SharedString>) -> Self {
309        self.meta = Some(meta.into());
310        self
311    }
312    pub fn padded(mut self, padded: bool) -> Self {
313        self.padded = padded;
314        self
315    }
316}
317
318impl ParentElement for Section {
319    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
320        self.children.extend(elements)
321    }
322}
323
324impl RenderOnce for Section {
325    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
326        let mut section_bg = cx.theme().colors().text;
327        section_bg.fade_out(0.96);
328
329        let children = if self.contained {
330            v_flex()
331                .flex_1()
332                .when(self.padded, |this| this.px(Spacing::XLarge.rems(cx)))
333                .child(
334                    v_flex()
335                        .w_full()
336                        .rounded_md()
337                        .border_1()
338                        .border_color(cx.theme().colors().border)
339                        .bg(section_bg)
340                        .py(Spacing::Medium.rems(cx))
341                        .gap_y(Spacing::Small.rems(cx))
342                        .child(div().flex().flex_1().size_full().children(self.children)),
343                )
344        } else {
345            v_flex()
346                .w_full()
347                .gap_y(Spacing::Small.rems(cx))
348                .when(self.padded, |this| {
349                    this.px(Spacing::Medium.rems(cx) + Spacing::Medium.rems(cx))
350                })
351                .children(self.children)
352        };
353
354        v_flex()
355            .size_full()
356            .flex_1()
357            .child(
358                v_flex()
359                    .flex_none()
360                    .px(Spacing::XLarge.rems(cx))
361                    .children(self.header)
362                    .when_some(self.meta, |this, meta| {
363                        this.child(Label::new(meta).size(LabelSize::Small).color(Color::Muted))
364                    }),
365            )
366            .child(children)
367            // fill any leftover space
368            .child(div().flex().flex_1())
369    }
370}
371
372#[derive(IntoElement)]
373pub struct SectionHeader {
374    /// The label of the header.
375    label: SharedString,
376    /// A slot for content that appears after the label, usually on the other side of the header.
377    /// This might be a button, a disclosure arrow, a face pile, etc.
378    end_slot: Option<AnyElement>,
379}
380
381impl SectionHeader {
382    pub fn new(label: impl Into<SharedString>) -> Self {
383        Self {
384            label: label.into(),
385            end_slot: None,
386        }
387    }
388
389    pub fn end_slot<E: IntoElement>(mut self, end_slot: impl Into<Option<E>>) -> Self {
390        self.end_slot = end_slot.into().map(IntoElement::into_any_element);
391        self
392    }
393}
394
395impl RenderOnce for SectionHeader {
396    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
397        h_flex()
398            .id(self.label.clone())
399            .w_full()
400            .px(Spacing::Large.rems(cx))
401            .child(
402                div()
403                    .h_7()
404                    .flex()
405                    .items_center()
406                    .justify_between()
407                    .w_full()
408                    .gap(Spacing::Small.rems(cx))
409                    .child(
410                        div().flex_1().child(
411                            Label::new(self.label.clone())
412                                .size(LabelSize::Small)
413                                .into_element(),
414                        ),
415                    )
416                    .child(h_flex().children(self.end_slot)),
417            )
418    }
419}
420
421impl From<SharedString> for SectionHeader {
422    fn from(val: SharedString) -> Self {
423        SectionHeader::new(val)
424    }
425}
426
427impl From<&'static str> for SectionHeader {
428    fn from(val: &'static str) -> Self {
429        let label: SharedString = val.into();
430        SectionHeader::new(label)
431    }
432}