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