modal.rs

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