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.clone()).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 .flex_none()
253 .w_full()
254 .p(DynamicSpacing::Base08.rems(cx))
255 .justify_between()
256 .child(div().when_some(self.start_slot, |this, start_slot| this.child(start_slot)))
257 .child(div().when_some(self.end_slot, |this, end_slot| this.child(end_slot)))
258 }
259}
260
261#[derive(IntoElement)]
262pub struct Section {
263 contained: bool,
264 padded: 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 padded: true,
281 header: None,
282 meta: None,
283 children: SmallVec::new(),
284 }
285 }
286
287 pub fn new_contained() -> Self {
288 Self {
289 contained: true,
290 padded: true,
291 header: None,
292 meta: None,
293 children: SmallVec::new(),
294 }
295 }
296
297 pub fn contained(mut self, contained: bool) -> Self {
298 self.contained = contained;
299 self
300 }
301
302 pub fn header(mut self, header: SectionHeader) -> Self {
303 self.header = Some(header);
304 self
305 }
306
307 pub fn meta(mut self, meta: impl Into<SharedString>) -> Self {
308 self.meta = Some(meta.into());
309 self
310 }
311 pub fn padded(mut self, padded: bool) -> Self {
312 self.padded = padded;
313 self
314 }
315}
316
317impl ParentElement for Section {
318 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
319 self.children.extend(elements)
320 }
321}
322
323impl RenderOnce for Section {
324 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
325 let mut section_bg = cx.theme().colors().text;
326 section_bg.fade_out(0.96);
327
328 let children = if self.contained {
329 v_flex()
330 .flex_1()
331 .when(self.padded, |this| this.px(DynamicSpacing::Base12.rems(cx)))
332 .child(
333 v_flex()
334 .w_full()
335 .rounded_sm()
336 .border_1()
337 .border_color(cx.theme().colors().border)
338 .bg(section_bg)
339 .py(DynamicSpacing::Base06.rems(cx))
340 .gap_y(DynamicSpacing::Base04.rems(cx))
341 .child(div().flex().flex_1().size_full().children(self.children)),
342 )
343 } else {
344 v_flex()
345 .w_full()
346 .flex_1()
347 .gap_y(DynamicSpacing::Base04.rems(cx))
348 .when(self.padded, |this| {
349 this.px(DynamicSpacing::Base06.rems(cx) + DynamicSpacing::Base06.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(DynamicSpacing::Base12.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, _window: &mut Window, cx: &mut App) -> impl IntoElement {
397 h_flex()
398 .id(self.label.clone())
399 .w_full()
400 .px(DynamicSpacing::Base08.rems(cx))
401 .child(
402 div()
403 .h_7()
404 .flex()
405 .items_center()
406 .justify_between()
407 .w_full()
408 .gap(DynamicSpacing::Base04.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}