1use std::rc::Rc;
4
5use gpui_kit::{
6 AnyElement, App, ElementId, Entity, FocusHandle, FontWeight, InteractiveElement as _,
7 IntoElement, ParentElement, Pixels, RenderOnce, SharedString, StyleRefinement, Styled, Window,
8 assets::IconName,
9 base::{self, StyledExt as _, TestSupportExt as _, actions::Cancel, h_flex, v_flex},
10 div,
11};
12
13use crate::{
14 ActiveTheme as _, Button, ButtonSize, Icon,
15 menu::{MenuLook, MenuMotion},
16};
17
18type OpenChange = Rc<dyn Fn(bool, &mut Window, &mut App)>;
19
20struct Focus {
23 surface: FocusHandle,
24 previous: Option<FocusHandle>,
25 opened: bool,
26}
27
28#[derive(IntoElement)]
62pub struct Dialog {
63 id: ElementId,
64 open: bool,
65 title: Option<SharedString>,
66 description: Option<SharedString>,
67 children: Vec<AnyElement>,
68 footer: Option<AnyElement>,
69 show_close_button: bool,
70 on_open_change: Option<OpenChange>,
71 focus: Option<FocusHandle>,
72 top: Option<Pixels>,
73 style: StyleRefinement,
74}
75
76impl Dialog {
77 pub fn new(id: impl Into<ElementId>) -> Self {
79 Self {
80 id: id.into(),
81 open: false,
82 title: None,
83 description: None,
84 children: Vec::new(),
85 footer: None,
86 show_close_button: true,
87 on_open_change: None,
88 focus: None,
89 top: None,
90 style: StyleRefinement::default(),
91 }
92 }
93
94 pub fn open(mut self, open: bool) -> Self {
96 self.open = open;
97 self
98 }
99
100 pub fn on_open_change(
103 mut self,
104 handler: impl Fn(bool, &mut Window, &mut App) + 'static,
105 ) -> Self {
106 self.on_open_change = Some(Rc::new(handler));
107 self
108 }
109
110 pub fn title(mut self, title: impl Into<SharedString>) -> Self {
112 self.title = Some(title.into());
113 self
114 }
115
116 pub fn description(mut self, description: impl Into<SharedString>) -> Self {
118 self.description = Some(description.into());
119 self
120 }
121
122 pub fn footer(mut self, footer: impl IntoElement) -> Self {
124 self.footer = Some(footer.into_any_element());
125 self
126 }
127
128 pub fn show_close_button(mut self, show: bool) -> Self {
130 self.show_close_button = show;
131 self
132 }
133
134 pub(crate) fn top(mut self, offset: Pixels) -> Self {
137 self.top = Some(offset);
138 self
139 }
140
141 pub fn track_focus(mut self, focus: &FocusHandle) -> Self {
144 self.focus = Some(focus.clone());
145 self
146 }
147}
148
149impl Styled for Dialog {
150 fn style(&mut self) -> &mut StyleRefinement {
151 &mut self.style
152 }
153}
154
155impl ParentElement for Dialog {
156 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
157 self.children.extend(elements);
158 }
159}
160
161impl RenderOnce for Dialog {
162 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
163 let child = |name: &'static str| ElementId::NamedChild(self.id.clone().into(), name.into());
164 let focus: Entity<Focus> = window.use_keyed_state(child("focus"), cx, |_, cx| Focus {
165 surface: cx.focus_handle(),
166 previous: None,
167 opened: false,
168 });
169 let surface = focus.read(cx).surface.clone();
170 move_focus(&focus, self.open, self.focus.as_ref(), window, cx);
171
172 let theme = cx.theme();
173 let look = MenuLook::of(theme, window.rem_size());
174 let overlay = theme.dialog_overlay;
175 let width = theme.metrics.dialog_width;
176 let padding = theme.metrics.dialog_padding;
177 let title_style = theme.base.typography.lg;
178 let Some(motion) =
179 MenuMotion::sample(child("presence"), self.open, false, &look, window, cx)
180 else {
181 return div().into_any_element();
182 };
183
184 let close = self.show_close_button.then(|| {
185 let target = surface.clone();
186 div().absolute().top_3().right_3().child(
187 Button::new(child("close"))
188 .ghost()
189 .size(ButtonSize::Sm)
190 .icon(Icon::from(IconName::Close))
191 .accessibility_label("Close")
192 .on_click(move |_, window, cx| {
193 target.dispatch_action(&Cancel, window, cx);
194 }),
195 )
196 });
197 let header = (self.title.is_some() || self.description.is_some()).then(|| {
198 v_flex()
199 .gap_2()
200 .children(self.title.map(|title| {
201 div()
202 .text_size(title_style.size)
203 .line_height(title_style.line_height)
204 .font_weight(FontWeight::SEMIBOLD)
205 .child(title)
206 }))
207 .children(
208 self.description
209 .map(|text| div().text_color(look.muted_foreground).child(text)),
210 )
211 });
212 let popup = v_flex()
213 .id(child("popup"))
214 .test_support()
215 .relative()
216 .occlude()
217 .w(width)
218 .max_w_full()
219 .gap_4()
220 .p(padding)
221 .rounded(look.radius)
222 .bg(look.surface)
223 .border_1()
224 .border_color(look.border)
225 .shadow(look.shadow(motion.shadow_strength))
226 .opacity(motion.opacity)
227 .text_size(look.text_size)
228 .line_height(look.line_height)
229 .text_color(look.foreground)
230 .refine_style(&self.style)
231 .children(close)
232 .children(header)
233 .children(self.children)
234 .children(
235 self.footer
236 .map(|footer| h_flex().justify_end().gap_2().child(footer)),
237 );
238
239 let on_open_change = self.on_open_change;
240 let mut dialog = base::Dialog::new(cx)
241 .focus_handle(surface)
242 .open(true)
243 .backdrop(
244 div()
245 .id(child("backdrop"))
246 .test_support()
247 .absolute()
248 .size_full()
249 .bg(overlay.opacity(motion.opacity)),
250 )
251 .popup(popup)
252 .p_4();
253 if let Some(offset) = self.top {
254 dialog = dialog.items_start().pt(offset);
255 }
256 if let Some(handler) = on_open_change {
257 dialog = dialog.on_open_change(move |open, _, window, cx| handler(open, window, cx));
258 }
259 dialog.into_any_element()
260 }
261}
262
263fn move_focus(
267 focus: &Entity<Focus>,
268 open: bool,
269 target: Option<&FocusHandle>,
270 window: &mut Window,
271 cx: &mut App,
272) {
273 let (surface, previous, opened) = {
274 let focus = focus.read(cx);
275 (focus.surface.clone(), focus.previous.clone(), focus.opened)
276 };
277 if open && !opened {
278 let before = window.focused(cx);
279 target.unwrap_or(&surface).focus(window, cx);
280 focus.update(cx, |focus, _| {
281 focus.previous = before;
282 focus.opened = true;
283 });
284 } else if !open && opened {
285 if surface.contains_focused(window, cx)
286 && let Some(previous) = previous
287 {
288 previous.focus(window, cx);
289 }
290 focus.update(cx, |focus, _| {
291 focus.previous = None;
292 focus.opened = false;
293 });
294 }
295}