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