modal.rs

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