1use std::rc::Rc;
9
10use gpui::{
11 AnyElement, App, Context, EventEmitter, FocusHandle, Focusable, InteractiveElement,
12 IntoElement, KeyDownEvent, ParentElement, Render, SharedString, Styled, Window, div, px,
13};
14use gpui_kit_semantics::{NodeSpec, Role, Semantic};
15use gpui_kit_theme::{ActiveTheme, Elevation, Space};
16
17use crate::controls::button::{Button, ButtonVariant};
18use crate::foundation::{Ident, StyledExt};
19use crate::overlay::focus::FocusTrap;
20use crate::overlay::layer::{Overlay, surface};
21use crate::overlay::panel::{self, Body};
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum DialogEvent {
29 Opened,
30 Confirmed,
32 Cancelled,
34 Dismissed,
36 Closed,
37}
38
39impl EventEmitter<DialogEvent> for Dialog {}
40
41pub struct Dialog {
43 ident: Ident,
44 focus_handle: FocusHandle,
45 confirm_focus: FocusHandle,
46 cancel_focus: FocusHandle,
47 title: SharedString,
48 description: Option<SharedString>,
49 body: Option<Body>,
50 confirm_label: Option<SharedString>,
51 cancel_label: Option<SharedString>,
52 dismissable: bool,
53 destructive: bool,
54 open: bool,
55 pending_focus: bool,
57 trap: FocusTrap,
58}
59
60impl std::fmt::Debug for Dialog {
61 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 formatter
63 .debug_struct("Dialog")
64 .field("ident", &self.ident)
65 .field("title", &self.title)
66 .field("has_body", &self.body.is_some())
67 .field("dismissable", &self.dismissable)
68 .field("destructive", &self.destructive)
69 .field("open", &self.open)
70 .finish()
71 }
72}
73
74impl Dialog {
75 pub fn new(ident: impl Into<Ident>, _window: &mut Window, cx: &mut Context<Self>) -> Self {
76 Self {
77 ident: ident.into(),
78 focus_handle: cx.focus_handle(),
79 confirm_focus: cx.focus_handle(),
80 cancel_focus: cx.focus_handle(),
81 title: SharedString::default(),
82 description: None,
83 body: None,
84 confirm_label: None,
85 cancel_label: None,
86 dismissable: true,
87 destructive: false,
88 open: false,
89 pending_focus: false,
90 trap: FocusTrap::new(),
91 }
92 }
93
94 pub fn title(mut self, title: impl Into<SharedString>) -> Self {
95 self.title = title.into();
96 self
97 }
98
99 pub fn description(mut self, description: impl Into<SharedString>) -> Self {
100 self.description = Some(description.into());
101 self
102 }
103
104 pub fn content(mut self, body: impl Fn(&mut Window, &mut App) -> AnyElement + 'static) -> Self {
106 self.body = Some(Rc::new(body));
107 self
108 }
109
110 pub fn dismissable(mut self, dismissable: bool) -> Self {
113 self.dismissable = dismissable;
114 self
115 }
116
117 pub fn destructive(mut self, destructive: bool) -> Self {
119 self.destructive = destructive;
120 self
121 }
122
123 pub fn confirm_label(mut self, label: impl Into<SharedString>) -> Self {
124 self.confirm_label = Some(label.into());
125 self
126 }
127
128 pub fn cancel_label(mut self, label: impl Into<SharedString>) -> Self {
129 self.cancel_label = Some(label.into());
130 self
131 }
132
133 pub fn is_open(&self) -> bool {
134 self.open
135 }
136
137 pub fn is_dismissable(&self) -> bool {
138 self.dismissable
139 }
140
141 pub fn set_title(&mut self, title: impl Into<SharedString>, cx: &mut Context<Self>) {
142 self.title = title.into();
143 cx.notify();
144 }
145
146 pub fn set_description(&mut self, description: Option<SharedString>, cx: &mut Context<Self>) {
147 self.description = description;
148 cx.notify();
149 }
150
151 pub fn open(&mut self, window: &mut Window, cx: &mut Context<Self>) {
152 if self.open {
153 return;
154 }
155 self.open = true;
156 self.pending_focus = true;
157 self.trap.engage(window, cx);
158 cx.emit(DialogEvent::Opened);
159 cx.notify();
160 }
161
162 pub fn close(&mut self, window: &mut Window, cx: &mut Context<Self>) {
165 if !self.open {
166 return;
167 }
168 self.open = false;
169 self.pending_focus = false;
170 self.trap.release(window, cx);
171 cx.emit(DialogEvent::Closed);
172 cx.notify();
173 }
174
175 pub fn confirm(&mut self, window: &mut Window, cx: &mut Context<Self>) {
176 if !self.open {
177 return;
178 }
179 cx.emit(DialogEvent::Confirmed);
180 self.close(window, cx);
181 }
182
183 pub fn cancel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
184 if !self.open {
185 return;
186 }
187 cx.emit(DialogEvent::Cancelled);
188 self.close(window, cx);
189 }
190
191 pub fn dismiss(&mut self, window: &mut Window, cx: &mut Context<Self>) {
194 if !self.open || !self.dismissable {
195 return;
196 }
197 cx.emit(DialogEvent::Dismissed);
198 self.close(window, cx);
199 }
200
201 fn initial_focus(&self) -> FocusHandle {
206 if self.destructive && self.cancel_label.is_some() {
207 return self.cancel_focus.clone();
208 }
209 if self.confirm_label.is_some() {
210 return self.confirm_focus.clone();
211 }
212 if self.cancel_label.is_some() {
213 return self.cancel_focus.clone();
214 }
215 self.focus_handle.clone()
216 }
217
218 fn on_navigation_key(
219 &mut self,
220 event: &KeyDownEvent,
221 window: &mut Window,
222 cx: &mut Context<Self>,
223 ) {
224 if !self.open || event.keystroke.key.as_str() != "tab" {
225 return;
226 }
227 if event.keystroke.modifiers.shift {
228 self.trap.focus_prev(window, cx);
229 } else {
230 self.trap.focus_next(window, cx);
231 }
232 cx.stop_propagation();
233 }
234
235 fn on_dismiss_key(
236 &mut self,
237 event: &KeyDownEvent,
238 window: &mut Window,
239 cx: &mut Context<Self>,
240 ) {
241 if !self.open || event.keystroke.key.as_str() != "escape" {
242 return;
243 }
244 self.dismiss(window, cx);
245 cx.stop_propagation();
246 }
247
248 fn actions(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
249 if self.confirm_label.is_none() && self.cancel_label.is_none() {
250 return None;
251 }
252 let theme = cx.theme().clone();
253 let dialog = cx.entity().downgrade();
254 let cancel = self.cancel_label.clone().map(|label| {
255 let dialog = dialog.clone();
256 Button::new(self.ident.child("cancel"))
257 .label(label)
258 .secondary()
259 .track_focus(&self.cancel_focus)
260 .on_click(move |window, cx| {
261 dialog
262 .update(cx, |dialog, cx| dialog.cancel(window, cx))
263 .ok();
264 })
265 });
266 let confirm = self.confirm_label.clone().map(|label| {
267 let dialog = dialog.clone();
268 Button::new(self.ident.child("confirm"))
269 .label(label)
270 .variant(if self.destructive {
271 ButtonVariant::Danger
272 } else {
273 ButtonVariant::Primary
274 })
275 .track_focus(&self.confirm_focus)
276 .on_click(move |window, cx| {
277 dialog
278 .update(cx, |dialog, cx| dialog.confirm(window, cx))
279 .ok();
280 })
281 });
282
283 Some(
284 div()
285 .row()
286 .justify_end()
287 .gap_token(&theme, Space::Sm)
288 .children(cancel)
289 .children(confirm)
290 .into_any_element(),
291 )
292 }
293}
294
295impl Focusable for Dialog {
296 fn focus_handle(&self, _cx: &App) -> FocusHandle {
297 self.focus_handle.clone()
298 }
299}
300
301impl Render for Dialog {
302 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
303 self.trap.begin_frame();
304 if !self.open {
305 return div().into_any_element();
306 }
307
308 if self.cancel_label.is_some() {
309 self.trap.register(self.cancel_focus.clone());
310 }
311 if self.confirm_label.is_some() {
312 self.trap.register(self.confirm_focus.clone());
313 }
314 if self.trap.stops().is_empty() {
315 self.trap.register(self.focus_handle.clone());
316 }
317 if self.pending_focus {
318 self.pending_focus = false;
321 self.initial_focus().focus(window, cx);
322 }
323
324 let theme = cx.theme().clone();
325 let title = self.title.clone();
326 let description = self.description.clone();
327 let body = self.body.clone().map(|body| body(window, cx));
328 let actions = self.actions(cx);
329
330 let mut spec = NodeSpec::new(self.ident.semantic_id(), Role::Dialog)
331 .modal(true)
332 .focus(&self.focus_handle);
333 if !title.is_empty() {
334 spec = spec.text(title.clone());
335 }
336 if let Some(description) = description.clone() {
337 spec = spec.description(description);
338 }
339
340 let heading = (!title.is_empty()).then(|| panel::heading(&self.ident, &theme, title, cx));
341 let description =
342 description.map(|description| panel::description(&self.ident, &theme, description, cx));
343
344 let mut card = surface(&theme, Elevation::Modal)
345 .w(px(360.0))
346 .p_token(&theme, Space::Lg)
347 .gap_token(&theme, Space::Sm)
348 .track_focus(&self.focus_handle)
349 .on_key_down(cx.listener(Self::on_navigation_key));
350 if self.dismissable {
351 card = card.on_key_down(cx.listener(Self::on_dismiss_key));
352 }
353 let card = card
354 .children(heading)
355 .children(description)
356 .children(body)
357 .children(actions)
358 .semantic_in(cx, spec);
359
360 let mut overlay = Overlay::modal(self.ident.child("overlay")).child(card);
361 if self.dismissable {
362 let dialog = cx.entity().downgrade();
363 overlay = overlay.on_dismiss(move |window, cx| {
364 dialog
365 .update(cx, |dialog, cx| dialog.dismiss(window, cx))
366 .ok();
367 });
368 }
369 overlay.into_any_element()
370 }
371}