1#![allow(missing_docs)]
2
3use crate::{
4 h_flex, v_flex, Clickable, Color, Headline, HeadlineSize, IconButton, IconButtonShape,
5 IconName, Label, LabelCommon, LabelSize, Spacing,
6};
7use gpui::{prelude::FluentBuilder, *};
8use smallvec::SmallVec;
9use theme::ActiveTheme;
10
11#[derive(IntoElement)]
12pub struct Modal {
13 id: ElementId,
14 header: ModalHeader,
15 children: SmallVec<[AnyElement; 2]>,
16 footer: Option<ModalFooter>,
17 container_id: ElementId,
18 container_scroll_handler: Option<ScrollHandle>,
19}
20
21impl Modal {
22 pub fn new(id: impl Into<SharedString>, scroll_handle: Option<ScrollHandle>) -> Self {
23 let id = id.into();
24
25 let container_id = ElementId::Name(format!("{}_container", id.clone()).into());
26 Self {
27 id: ElementId::Name(id),
28 header: ModalHeader::new(),
29 children: SmallVec::new(),
30 footer: None,
31 container_id,
32 container_scroll_handler: scroll_handle,
33 }
34 }
35
36 pub fn header(mut self, header: ModalHeader) -> Self {
37 self.header = header;
38 self
39 }
40
41 pub fn section(mut self, section: Section) -> Self {
42 self.children.push(section.into_any_element());
43 self
44 }
45
46 pub fn footer(mut self, footer: ModalFooter) -> Self {
47 self.footer = Some(footer);
48 self
49 }
50
51 pub fn show_dismiss(mut self, show: bool) -> Self {
52 self.header.show_dismiss_button = show;
53 self
54 }
55
56 pub fn show_back(mut self, show: bool) -> Self {
57 self.header.show_back_button = show;
58 self
59 }
60}
61
62impl ParentElement for Modal {
63 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
64 self.children.extend(elements)
65 }
66}
67
68impl RenderOnce for Modal {
69 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
70 v_flex()
71 .id(self.id.clone())
72 .size_full()
73 .flex_1()
74 .overflow_hidden()
75 .child(self.header)
76 .child(
77 v_flex()
78 .id(self.container_id.clone())
79 .w_full()
80 .gap(Spacing::Large.rems(cx))
81 .when_some(
82 self.container_scroll_handler,
83 |this, container_scroll_handle| {
84 this.overflow_y_scroll()
85 .track_scroll(&container_scroll_handle)
86 },
87 )
88 .children(self.children),
89 )
90 .children(self.footer)
91 }
92}
93
94#[derive(IntoElement)]
95pub struct ModalHeader {
96 headline: Option<SharedString>,
97 children: SmallVec<[AnyElement; 2]>,
98 show_dismiss_button: bool,
99 show_back_button: bool,
100}
101
102impl Default for ModalHeader {
103 fn default() -> Self {
104 Self::new()
105 }
106}
107
108impl ModalHeader {
109 pub fn new() -> Self {
110 Self {
111 headline: None,
112 children: SmallVec::new(),
113 show_dismiss_button: false,
114 show_back_button: false,
115 }
116 }
117
118 /// Set the headline of the modal.
119 ///
120 /// This will insert the headline as the first item
121 /// of `children` if it is not already present.
122 pub fn headline(mut self, headline: impl Into<SharedString>) -> Self {
123 self.headline = Some(headline.into());
124 self
125 }
126
127 pub fn show_dismiss_button(mut self, show: bool) -> Self {
128 self.show_dismiss_button = show;
129 self
130 }
131
132 pub fn show_back_button(mut self, show: bool) -> Self {
133 self.show_back_button = show;
134 self
135 }
136}
137
138impl ParentElement for ModalHeader {
139 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
140 self.children.extend(elements)
141 }
142}
143
144impl RenderOnce for ModalHeader {
145 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
146 let mut children = self.children;
147
148 if self.headline.is_some() {
149 children.insert(
150 0,
151 Headline::new(self.headline.unwrap())
152 .size(HeadlineSize::XSmall)
153 .color(Color::Muted)
154 .into_any_element(),
155 );
156 }
157
158 h_flex()
159 .flex_none()
160 .justify_between()
161 .w_full()
162 .px(Spacing::XLarge.rems(cx))
163 .pt(Spacing::Large.rems(cx))
164 .pb(Spacing::Small.rems(cx))
165 .gap(Spacing::Large.rems(cx))
166 .when(self.show_back_button, |this| {
167 this.child(
168 IconButton::new("back", IconName::ArrowLeft)
169 .shape(IconButtonShape::Square)
170 .on_click(|_, cx| {
171 cx.dispatch_action(menu::Cancel.boxed_clone());
172 }),
173 )
174 })
175 .child(div().flex_1().children(children))
176 .when(self.show_dismiss_button, |this| {
177 this.child(
178 IconButton::new("dismiss", IconName::Close)
179 .shape(IconButtonShape::Square)
180 .on_click(|_, cx| {
181 cx.dispatch_action(menu::Cancel.boxed_clone());
182 }),
183 )
184 })
185 }
186}
187
188#[derive(IntoElement)]
189pub struct ModalRow {
190 children: SmallVec<[AnyElement; 2]>,
191}
192
193impl Default for ModalRow {
194 fn default() -> Self {
195 Self::new()
196 }
197}
198
199impl ModalRow {
200 pub fn new() -> Self {
201 Self {
202 children: SmallVec::new(),
203 }
204 }
205}
206
207impl ParentElement for ModalRow {
208 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
209 self.children.extend(elements)
210 }
211}
212
213impl RenderOnce for ModalRow {
214 fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
215 h_flex().w_full().py_1().children(self.children)
216 }
217}
218
219#[derive(IntoElement)]
220pub struct ModalFooter {
221 start_slot: Option<AnyElement>,
222 end_slot: Option<AnyElement>,
223}
224
225impl Default for ModalFooter {
226 fn default() -> Self {
227 Self::new()
228 }
229}
230
231impl ModalFooter {
232 pub fn new() -> Self {
233 Self {
234 start_slot: None,
235 end_slot: None,
236 }
237 }
238
239 pub fn start_slot<E: IntoElement>(mut self, start_slot: impl Into<Option<E>>) -> Self {
240 self.start_slot = start_slot.into().map(IntoElement::into_any_element);
241 self
242 }
243
244 pub fn end_slot<E: IntoElement>(mut self, end_slot: impl Into<Option<E>>) -> Self {
245 self.end_slot = end_slot.into().map(IntoElement::into_any_element);
246 self
247 }
248}
249
250impl RenderOnce for ModalFooter {
251 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
252 h_flex()
253 .flex_none()
254 .w_full()
255 .p(Spacing::Large.rems(cx))
256 .justify_between()
257 .child(div().when_some(self.start_slot, |this, start_slot| this.child(start_slot)))
258 .child(div().when_some(self.end_slot, |this, end_slot| this.child(end_slot)))
259 }
260}
261
262#[derive(IntoElement)]
263pub struct Section {
264 contained: bool,
265 padded: bool,
266 header: Option<SectionHeader>,
267 meta: Option<SharedString>,
268 children: SmallVec<[AnyElement; 2]>,
269}
270
271impl Default for Section {
272 fn default() -> Self {
273 Self::new()
274 }
275}
276
277impl Section {
278 pub fn new() -> Self {
279 Self {
280 contained: false,
281 padded: true,
282 header: None,
283 meta: None,
284 children: SmallVec::new(),
285 }
286 }
287
288 pub fn new_contained() -> Self {
289 Self {
290 contained: true,
291 padded: true,
292 header: None,
293 meta: None,
294 children: SmallVec::new(),
295 }
296 }
297
298 pub fn contained(mut self, contained: bool) -> Self {
299 self.contained = contained;
300 self
301 }
302
303 pub fn header(mut self, header: SectionHeader) -> Self {
304 self.header = Some(header);
305 self
306 }
307
308 pub fn meta(mut self, meta: impl Into<SharedString>) -> Self {
309 self.meta = Some(meta.into());
310 self
311 }
312 pub fn padded(mut self, padded: bool) -> Self {
313 self.padded = padded;
314 self
315 }
316}
317
318impl ParentElement for Section {
319 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
320 self.children.extend(elements)
321 }
322}
323
324impl RenderOnce for Section {
325 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
326 let mut section_bg = cx.theme().colors().text;
327 section_bg.fade_out(0.96);
328
329 let children = if self.contained {
330 v_flex()
331 .flex_1()
332 .when(self.padded, |this| this.px(Spacing::XLarge.rems(cx)))
333 .child(
334 v_flex()
335 .w_full()
336 .rounded_md()
337 .border_1()
338 .border_color(cx.theme().colors().border)
339 .bg(section_bg)
340 .py(Spacing::Medium.rems(cx))
341 .gap_y(Spacing::Small.rems(cx))
342 .child(div().flex().flex_1().size_full().children(self.children)),
343 )
344 } else {
345 v_flex()
346 .w_full()
347 .gap_y(Spacing::Small.rems(cx))
348 .when(self.padded, |this| {
349 this.px(Spacing::Medium.rems(cx) + Spacing::Medium.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(Spacing::XLarge.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, cx: &mut WindowContext) -> impl IntoElement {
397 h_flex()
398 .id(self.label.clone())
399 .w_full()
400 .px(Spacing::Large.rems(cx))
401 .child(
402 div()
403 .h_7()
404 .flex()
405 .items_center()
406 .justify_between()
407 .w_full()
408 .gap(Spacing::Small.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}