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