modal.rs

  1use crate::{
  2    h_flex, v_flex, Clickable, Color, Headline, HeadlineSize, IconButton, IconButtonShape,
  3    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().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                    .gap_y(Spacing::Small.rems(cx))
330                    .child(div().flex().flex_1().size_full().children(self.children)),
331            )
332        } else {
333            v_flex()
334                .w_full()
335                .gap_y(Spacing::Small.rems(cx))
336                .px(Spacing::Medium.rems(cx) + Spacing::Medium.rems(cx))
337                .children(self.children)
338        };
339
340        v_flex()
341            .size_full()
342            .flex_1()
343            .child(
344                v_flex()
345                    .flex_none()
346                    .px(Spacing::XLarge.rems(cx))
347                    .children(self.header)
348                    .when_some(self.meta, |this, meta| {
349                        this.child(Label::new(meta).size(LabelSize::Small).color(Color::Muted))
350                    }),
351            )
352            .child(children)
353            // fill any leftover space
354            .child(div().flex().flex_1())
355    }
356}
357
358#[derive(IntoElement)]
359pub struct SectionHeader {
360    /// The label of the header.
361    label: SharedString,
362    /// A slot for content that appears after the label, usually on the other side of the header.
363    /// This might be a button, a disclosure arrow, a face pile, etc.
364    end_slot: Option<AnyElement>,
365}
366
367impl SectionHeader {
368    pub fn new(label: impl Into<SharedString>) -> Self {
369        Self {
370            label: label.into(),
371            end_slot: None,
372        }
373    }
374
375    pub fn end_slot<E: IntoElement>(mut self, end_slot: impl Into<Option<E>>) -> Self {
376        self.end_slot = end_slot.into().map(IntoElement::into_any_element);
377        self
378    }
379}
380
381impl RenderOnce for SectionHeader {
382    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
383        h_flex()
384            .id(self.label.clone())
385            .w_full()
386            .px(Spacing::Large.rems(cx))
387            .child(
388                div()
389                    .h_7()
390                    .flex()
391                    .items_center()
392                    .justify_between()
393                    .w_full()
394                    .gap(Spacing::Small.rems(cx))
395                    .child(
396                        div().flex_1().child(
397                            Label::new(self.label.clone())
398                                .size(LabelSize::Small)
399                                .into_element(),
400                        ),
401                    )
402                    .child(h_flex().children(self.end_slot)),
403            )
404    }
405}
406
407impl From<SharedString> for SectionHeader {
408    fn from(val: SharedString) -> Self {
409        SectionHeader::new(val)
410    }
411}
412
413impl From<&'static str> for SectionHeader {
414    fn from(val: &'static str) -> Self {
415        let label: SharedString = val.into();
416        SectionHeader::new(label)
417    }
418}