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