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