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