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