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.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, _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            .flex_none()
253            .w_full()
254            .p(DynamicSpacing::Base08.rems(cx))
255            .justify_between()
256            .child(div().when_some(self.start_slot, |this, start_slot| this.child(start_slot)))
257            .child(div().when_some(self.end_slot, |this, end_slot| this.child(end_slot)))
258    }
259}
260
261#[derive(IntoElement)]
262pub struct Section {
263    contained: bool,
264    padded: bool,
265    header: Option<SectionHeader>,
266    meta: Option<SharedString>,
267    children: SmallVec<[AnyElement; 2]>,
268}
269
270impl Default for Section {
271    fn default() -> Self {
272        Self::new()
273    }
274}
275
276impl Section {
277    pub fn new() -> Self {
278        Self {
279            contained: false,
280            padded: true,
281            header: None,
282            meta: None,
283            children: SmallVec::new(),
284        }
285    }
286
287    pub fn new_contained() -> Self {
288        Self {
289            contained: true,
290            padded: true,
291            header: None,
292            meta: None,
293            children: SmallVec::new(),
294        }
295    }
296
297    pub fn contained(mut self, contained: bool) -> Self {
298        self.contained = contained;
299        self
300    }
301
302    pub fn header(mut self, header: SectionHeader) -> Self {
303        self.header = Some(header);
304        self
305    }
306
307    pub fn meta(mut self, meta: impl Into<SharedString>) -> Self {
308        self.meta = Some(meta.into());
309        self
310    }
311    pub fn padded(mut self, padded: bool) -> Self {
312        self.padded = padded;
313        self
314    }
315}
316
317impl ParentElement for Section {
318    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
319        self.children.extend(elements)
320    }
321}
322
323impl RenderOnce for Section {
324    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
325        let mut section_bg = cx.theme().colors().text;
326        section_bg.fade_out(0.96);
327
328        let children = if self.contained {
329            v_flex()
330                .flex_1()
331                .when(self.padded, |this| this.px(DynamicSpacing::Base12.rems(cx)))
332                .child(
333                    v_flex()
334                        .w_full()
335                        .rounded_sm()
336                        .border_1()
337                        .border_color(cx.theme().colors().border)
338                        .bg(section_bg)
339                        .py(DynamicSpacing::Base06.rems(cx))
340                        .gap_y(DynamicSpacing::Base04.rems(cx))
341                        .child(div().flex().flex_1().size_full().children(self.children)),
342                )
343        } else {
344            v_flex()
345                .w_full()
346                .flex_1()
347                .gap_y(DynamicSpacing::Base04.rems(cx))
348                .when(self.padded, |this| {
349                    this.px(DynamicSpacing::Base06.rems(cx) + DynamicSpacing::Base06.rems(cx))
350                })
351                .children(self.children)
352        };
353
354        v_flex()
355            .size_full()
356            .flex_1()
357            .child(
358                v_flex()
359                    .flex_none()
360                    .px(DynamicSpacing::Base12.rems(cx))
361                    .children(self.header)
362                    .when_some(self.meta, |this, meta| {
363                        this.child(Label::new(meta).size(LabelSize::Small).color(Color::Muted))
364                    }),
365            )
366            .child(children)
367            // fill any leftover space
368            .child(div().flex().flex_1())
369    }
370}
371
372#[derive(IntoElement)]
373pub struct SectionHeader {
374    /// The label of the header.
375    label: SharedString,
376    /// A slot for content that appears after the label, usually on the other side of the header.
377    /// This might be a button, a disclosure arrow, a face pile, etc.
378    end_slot: Option<AnyElement>,
379}
380
381impl SectionHeader {
382    pub fn new(label: impl Into<SharedString>) -> Self {
383        Self {
384            label: label.into(),
385            end_slot: None,
386        }
387    }
388
389    pub fn end_slot<E: IntoElement>(mut self, end_slot: impl Into<Option<E>>) -> Self {
390        self.end_slot = end_slot.into().map(IntoElement::into_any_element);
391        self
392    }
393}
394
395impl RenderOnce for SectionHeader {
396    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
397        h_flex()
398            .id(self.label.clone())
399            .w_full()
400            .px(DynamicSpacing::Base08.rems(cx))
401            .child(
402                div()
403                    .h_7()
404                    .flex()
405                    .items_center()
406                    .justify_between()
407                    .w_full()
408                    .gap(DynamicSpacing::Base04.rems(cx))
409                    .child(
410                        div().flex_1().child(
411                            Label::new(self.label.clone())
412                                .size(LabelSize::Small)
413                                .into_element(),
414                        ),
415                    )
416                    .child(h_flex().children(self.end_slot)),
417            )
418    }
419}
420
421impl From<SharedString> for SectionHeader {
422    fn from(val: SharedString) -> Self {
423        SectionHeader::new(val)
424    }
425}
426
427impl From<&'static str> for SectionHeader {
428    fn from(val: &'static str) -> Self {
429        let label: SharedString = val.into();
430        SectionHeader::new(label)
431    }
432}