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