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(self.footer.is_some(), |this| this.pb_4())
 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    icon: Option<Icon>,
 97    headline: Option<SharedString>,
 98    description: Option<SharedString>,
 99    children: SmallVec<[AnyElement; 2]>,
100    show_dismiss_button: bool,
101    show_back_button: bool,
102}
103
104impl Default for ModalHeader {
105    fn default() -> Self {
106        Self::new()
107    }
108}
109
110impl ModalHeader {
111    pub fn new() -> Self {
112        Self {
113            icon: None,
114            headline: None,
115            description: None,
116            children: SmallVec::new(),
117            show_dismiss_button: false,
118            show_back_button: false,
119        }
120    }
121
122    pub fn icon(mut self, icon: Icon) -> Self {
123        self.icon = Some(icon);
124        self
125    }
126
127    /// Set the headline of the modal.
128    ///
129    /// This will insert the headline as the first item
130    /// of `children` if it is not already present.
131    pub fn headline(mut self, headline: impl Into<SharedString>) -> Self {
132        self.headline = Some(headline.into());
133        self
134    }
135
136    pub fn description(mut self, description: impl Into<SharedString>) -> Self {
137        self.description = Some(description.into());
138        self
139    }
140
141    pub fn show_dismiss_button(mut self, show: bool) -> Self {
142        self.show_dismiss_button = show;
143        self
144    }
145
146    pub fn show_back_button(mut self, show: bool) -> Self {
147        self.show_back_button = show;
148        self
149    }
150}
151
152impl ParentElement for ModalHeader {
153    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
154        self.children.extend(elements)
155    }
156}
157
158impl RenderOnce for ModalHeader {
159    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
160        let mut children = self.children;
161
162        if self.headline.is_some() {
163            children.insert(
164                0,
165                Headline::new(self.headline.unwrap())
166                    .size(HeadlineSize::XSmall)
167                    .color(Color::Muted)
168                    .into_any_element(),
169            );
170        }
171
172        h_flex()
173            .flex_none()
174            .justify_between()
175            .w_full()
176            .px(DynamicSpacing::Base12.rems(cx))
177            .pt(DynamicSpacing::Base08.rems(cx))
178            .pb(DynamicSpacing::Base04.rems(cx))
179            .gap(DynamicSpacing::Base08.rems(cx))
180            .when(self.show_back_button, |this| {
181                this.child(
182                    IconButton::new("back", IconName::ArrowLeft)
183                        .shape(IconButtonShape::Square)
184                        .on_click(|_, window, cx| {
185                            window.dispatch_action(menu::Cancel.boxed_clone(), cx);
186                        }),
187                )
188            })
189            .child(
190                v_flex()
191                    .flex_1()
192                    .child(
193                        h_flex()
194                            .gap_1()
195                            .when_some(self.icon, |this, icon| this.child(icon))
196                            .children(children),
197                    )
198                    .when_some(self.description, |this, description| {
199                        this.child(Label::new(description).color(Color::Muted).mb_2())
200                    }),
201            )
202            .when(self.show_dismiss_button, |this| {
203                this.child(
204                    IconButton::new("dismiss", IconName::Close)
205                        .shape(IconButtonShape::Square)
206                        .on_click(|_, window, cx| {
207                            window.dispatch_action(menu::Cancel.boxed_clone(), cx);
208                        }),
209                )
210            })
211    }
212}
213
214#[derive(IntoElement)]
215pub struct ModalRow {
216    children: SmallVec<[AnyElement; 2]>,
217}
218
219impl Default for ModalRow {
220    fn default() -> Self {
221        Self::new()
222    }
223}
224
225impl ModalRow {
226    pub fn new() -> Self {
227        Self {
228            children: SmallVec::new(),
229        }
230    }
231}
232
233impl ParentElement for ModalRow {
234    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
235        self.children.extend(elements)
236    }
237}
238
239impl RenderOnce for ModalRow {
240    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
241        h_flex().w_full().py_1().children(self.children)
242    }
243}
244
245#[derive(IntoElement)]
246pub struct ModalFooter {
247    start_slot: Option<AnyElement>,
248    end_slot: Option<AnyElement>,
249}
250
251impl Default for ModalFooter {
252    fn default() -> Self {
253        Self::new()
254    }
255}
256
257impl ModalFooter {
258    pub fn new() -> Self {
259        Self {
260            start_slot: None,
261            end_slot: None,
262        }
263    }
264
265    pub fn start_slot<E: IntoElement>(mut self, start_slot: impl Into<Option<E>>) -> Self {
266        self.start_slot = start_slot.into().map(IntoElement::into_any_element);
267        self
268    }
269
270    pub fn end_slot<E: IntoElement>(mut self, end_slot: impl Into<Option<E>>) -> Self {
271        self.end_slot = end_slot.into().map(IntoElement::into_any_element);
272        self
273    }
274}
275
276impl RenderOnce for ModalFooter {
277    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
278        h_flex()
279            .w_full()
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}