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