1use std::rc::Rc;
9
10use gpui::prelude::*;
11use gpui::{
12 deferred, div, px, AnyElement, App, ClickEvent, FontWeight, IntoElement, SharedString, Window,
13};
14
15use crate::devtools::ProbedAny;
16use crate::theme::{theme, Size};
17
18type CloseHandler = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;
19
20#[derive(IntoElement)]
22pub struct Modal {
23 title: Option<SharedString>,
24 children: Vec<AnyElement>,
25 width: f32,
26 padding: Size,
27 radius: Option<Size>,
28 on_close: Option<CloseHandler>,
29}
30
31impl Modal {
32 pub fn new() -> Self {
33 Modal {
34 title: None,
35 children: Vec::new(),
36 width: 440.0,
37 padding: Size::Lg,
38 radius: Some(Size::Md),
39 on_close: None,
40 }
41 }
42
43 pub fn title(mut self, title: impl Into<SharedString>) -> Self {
44 self.title = Some(title.into());
45 self
46 }
47
48 pub fn width(mut self, width: f32) -> Self {
49 self.width = width;
50 self
51 }
52
53 pub fn padding(mut self, padding: Size) -> Self {
54 self.padding = padding;
55 self
56 }
57
58 pub fn radius(mut self, radius: Size) -> Self {
59 self.radius = Some(radius);
60 self
61 }
62
63 pub fn on_close(
66 mut self,
67 handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
68 ) -> Self {
69 self.on_close = Some(Rc::new(handler));
70 self
71 }
72}
73
74impl Default for Modal {
75 fn default() -> Self {
76 Modal::new()
77 }
78}
79
80impl ParentElement for Modal {
81 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
82 self.children.extend(elements);
83 }
84}
85
86impl RenderOnce for Modal {
87 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
88 let t = theme(cx);
89 let radius = t.radius(self.radius.unwrap_or(Size::Md));
90 let padding = t.spacing(self.padding);
91 let gap = t.spacing(Size::Md);
92 let surface = t.surface().hsla();
93 let text = t.text().hsla();
94 let dimmed = t.dimmed().hsla();
95 let scrim = t.black.alpha(0.55);
96
97 let viewport = window.viewport_size();
98 let close = self.on_close.clone();
99
100 let mut dialog = div()
101 .id("guise-modal-dialog")
102 .occlude()
103 .flex()
104 .flex_col()
105 .gap(px(gap))
106 .w(px(self.width))
107 .bg(surface)
108 .rounded(px(radius))
109 .p(px(padding))
110 .shadow_xl()
111 .on_click(|_ev, _window, cx| cx.stop_propagation());
112
113 if self.title.is_some() || close.is_some() {
114 let mut header = div().flex().items_center().justify_between();
115 header = header.child(match self.title {
116 Some(title) => div()
117 .text_size(px(18.0))
118 .font_weight(FontWeight::BOLD)
119 .text_color(text)
120 .child(title),
121 None => div(),
122 });
123 if let Some(handler) = close.clone() {
124 header = header.child(
125 div()
126 .id("guise-modal-close")
127 .px(px(6.0))
128 .rounded(px(4.0))
129 .text_size(px(18.0))
130 .text_color(dimmed)
131 .hover(move |s| s.text_color(text))
132 .child(SharedString::new_static("\u{00d7}"))
133 .on_click(move |ev, window, cx| {
134 handler(ev, window, cx);
135 cx.stop_propagation();
136 }),
137 );
138 }
139 dialog = dialog.child(header);
140 }
141
142 dialog = dialog.children(self.children);
143
144 let mut backdrop = div()
145 .id("guise-modal-backdrop")
146 .occlude()
147 .absolute()
148 .top(px(0.0))
149 .left(px(0.0))
150 .w(viewport.width)
151 .h(viewport.height)
152 .flex()
153 .items_center()
154 .justify_center()
155 .bg(scrim)
156 .child(dialog);
157
158 if let Some(handler) = close {
159 backdrop = backdrop.on_click(move |ev, window, cx| handler(ev, window, cx));
160 }
161
162 deferred(backdrop).probe_any("Modal")
163 }
164}