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