modal.rs

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