1use gpui::{
14 AnyElement, App, Context, EventEmitter, FocusHandle, Focusable, InteractiveElement,
15 IntoElement, KeyDownEvent, ParentElement, Render, SharedString, Styled, Window, div,
16 prelude::FluentBuilder, px,
17};
18use gpui_kit_semantics::{NodeSpec, Role, Semantic};
19use gpui_kit_theme::{ActiveTheme, Elevation, Space, Theme};
20
21use crate::foundation::{Ident, StyledExt};
22use crate::motion::{self, Easing, MotionSpec, Phase, Presence};
23use crate::overlay::focus::FocusTrap;
24use crate::overlay::layer::{Edge, Overlay, surface};
25use crate::overlay::panel::{self, Body};
26
27const DEFAULT_SIZE: f32 = 360.0;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum DrawerEvent {
38 Opened,
39 Dismissed,
41 Closed,
42}
43
44impl EventEmitter<DrawerEvent> for Drawer {}
45
46pub struct Drawer {
48 ident: Ident,
49 focus_handle: FocusHandle,
50 edge: Edge,
51 size: f32,
52 title: SharedString,
53 description: Option<SharedString>,
54 body: Option<Body>,
55 footer: Option<Body>,
56 dismissable: bool,
57 open: bool,
58 pending_focus: bool,
60 presence: Option<Presence>,
61 progress: f32,
62 stops: Vec<FocusHandle>,
63 trap: FocusTrap,
64}
65
66impl std::fmt::Debug for Drawer {
67 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68 formatter
69 .debug_struct("Drawer")
70 .field("ident", &self.ident)
71 .field("edge", &self.edge)
72 .field("title", &self.title)
73 .field("has_body", &self.body.is_some())
74 .field("dismissable", &self.dismissable)
75 .field("open", &self.open)
76 .field("rendered", &self.is_rendered())
77 .finish()
78 }
79}
80
81impl Drawer {
82 pub fn new(ident: impl Into<Ident>, _window: &mut Window, cx: &mut Context<Self>) -> Self {
83 Self {
84 ident: ident.into(),
85 focus_handle: cx.focus_handle(),
86 edge: Edge::Right,
87 size: DEFAULT_SIZE,
88 title: SharedString::default(),
89 description: None,
90 body: None,
91 footer: None,
92 dismissable: true,
93 open: false,
94 pending_focus: false,
95 presence: None,
96 progress: 0.0,
97 stops: Vec::new(),
98 trap: FocusTrap::new(),
99 }
100 }
101
102 pub fn edge(mut self, edge: Edge) -> Self {
104 self.edge = edge;
105 self
106 }
107
108 pub fn size(mut self, size: f32) -> Self {
111 self.size = size.max(0.0);
112 self
113 }
114
115 pub fn title(mut self, title: impl Into<SharedString>) -> Self {
116 self.title = title.into();
117 self
118 }
119
120 pub fn description(mut self, description: impl Into<SharedString>) -> Self {
121 self.description = Some(description.into());
122 self
123 }
124
125 pub fn content(mut self, body: impl Fn(&mut Window, &mut App) -> AnyElement + 'static) -> Self {
127 self.body = Some(std::rc::Rc::new(body));
128 self
129 }
130
131 pub fn footer(
133 mut self,
134 footer: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
135 ) -> Self {
136 self.footer = Some(std::rc::Rc::new(footer));
137 self
138 }
139
140 pub fn dismissable(mut self, dismissable: bool) -> Self {
143 self.dismissable = dismissable;
144 self
145 }
146
147 pub fn focus_stops(mut self, stops: impl IntoIterator<Item = FocusHandle>) -> Self {
152 self.stops = stops.into_iter().collect();
153 self
154 }
155
156 pub fn set_focus_stops(
157 &mut self,
158 stops: impl IntoIterator<Item = FocusHandle>,
159 cx: &mut Context<Self>,
160 ) {
161 self.stops = stops.into_iter().collect();
162 cx.notify();
163 }
164
165 pub fn is_open(&self) -> bool {
166 self.open
167 }
168
169 pub fn is_dismissable(&self) -> bool {
170 self.dismissable
171 }
172
173 pub fn is_rendered(&self) -> bool {
176 self.presence
177 .as_ref()
178 .is_some_and(|presence| presence.is_rendered())
179 }
180
181 pub fn set_title(&mut self, title: impl Into<SharedString>, cx: &mut Context<Self>) {
182 self.title = title.into();
183 cx.notify();
184 }
185
186 pub fn open(&mut self, window: &mut Window, cx: &mut Context<Self>) {
187 if self.open {
188 return;
189 }
190 let theme = cx.theme().clone();
191 self.open = true;
192 self.pending_focus = true;
193 let presence = self
194 .presence
195 .get_or_insert_with(|| Presence::hidden(enter_spec(&theme), exit_spec(&theme)));
196 presence.show();
197 self.trap.engage(window, cx);
198 cx.emit(DrawerEvent::Opened);
199 cx.notify();
200 }
201
202 pub fn close(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
204 if !self.open {
205 return;
206 }
207 self.open = false;
208 self.pending_focus = false;
209 if let Some(presence) = self.presence.as_mut() {
210 presence.hide();
211 }
212 cx.notify();
213 }
214
215 pub fn dismiss(&mut self, window: &mut Window, cx: &mut Context<Self>) {
218 if !self.open || !self.dismissable {
219 return;
220 }
221 cx.emit(DrawerEvent::Dismissed);
222 self.close(window, cx);
223 }
224
225 pub fn settle(&mut self, cx: &mut Context<Self>) {
228 if let Some(presence) = self.presence.as_mut() {
229 presence.settle();
230 self.progress = presence.progress();
231 }
232 cx.notify();
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 on_navigation_key(
249 &mut self,
250 event: &KeyDownEvent,
251 window: &mut Window,
252 cx: &mut Context<Self>,
253 ) {
254 if !self.open || event.keystroke.key.as_str() != "tab" {
255 return;
256 }
257 if event.keystroke.modifiers.shift {
258 self.trap.focus_prev(window, cx);
259 } else {
260 self.trap.focus_next(window, cx);
261 }
262 cx.stop_propagation();
263 }
264
265 fn travel(&self) -> f32 {
267 self.size * (1.0 - self.progress)
268 }
269}
270
271fn enter_spec(theme: &Theme) -> MotionSpec {
274 motion::dialog_arrival(theme)
275}
276
277fn exit_spec(theme: &Theme) -> MotionSpec {
278 MotionSpec::new(theme.motion.quick_ms, Easing::Exit.curve(theme))
279}
280
281impl Focusable for Drawer {
282 fn focus_handle(&self, _cx: &App) -> FocusHandle {
283 self.focus_handle.clone()
284 }
285}
286
287impl Render for Drawer {
288 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
289 self.trap.begin_frame();
290 if self.presence.is_none() {
291 return div().into_any_element();
292 }
293
294 self.progress = self
295 .presence
296 .as_mut()
297 .map(|presence| presence.animate(window, cx))
298 .unwrap_or_default();
299 if self
300 .presence
301 .as_ref()
302 .is_none_or(|presence| presence.phase() == Phase::Gone)
303 {
304 self.presence = None;
307 self.progress = 0.0;
308 self.trap.release(window, cx);
309 cx.emit(DrawerEvent::Closed);
310 return div().into_any_element();
311 }
312
313 for stop in self.stops.clone() {
314 self.trap.register(stop);
315 }
316 if self.trap.stops().is_empty() {
317 self.trap.register(self.focus_handle.clone());
318 }
319 if self.pending_focus {
320 self.pending_focus = false;
323 self.trap.focus_first(window, cx);
324 }
325
326 let theme = cx.theme().clone();
327 let title = self.title.clone();
328 let description = self.description.clone();
329 let body = self.body.clone().map(|body| body(window, cx));
330 let footer = self.footer.clone().map(|footer| footer(window, cx));
331 let horizontal = self.edge.is_horizontal();
332 let travel = self.travel();
333
334 let mut spec = NodeSpec::new(self.ident.semantic_id(), Role::Dialog)
335 .expanded(self.open)
336 .focus(&self.focus_handle);
337 if !title.is_empty() {
338 spec = spec.text(title.clone());
339 }
340
341 let heading = (!title.is_empty()).then(|| panel::heading(&self.ident, &theme, title, cx));
342 let description =
343 description.map(|description| panel::description(&self.ident, &theme, description, cx));
344
345 let mut card = surface(&theme, Elevation::Modal)
346 .relative()
347 .when(horizontal, |element| element.w(px(self.size)).h_full())
348 .when(!horizontal, |element| element.h(px(self.size)).w_full())
349 .p_token(&theme, Space::Lg)
350 .gap_token(&theme, Space::Sm)
351 .track_focus(&self.focus_handle)
352 .on_key_down(cx.listener(Self::on_navigation_key));
353 card = match self.edge {
354 Edge::Left => card.left(px(-travel)),
355 Edge::Right => card.left(px(travel)),
356 Edge::Top => card.top(px(-travel)),
357 Edge::Bottom => card.top(px(travel)),
358 };
359 if self.dismissable {
360 card = card.on_key_down(cx.listener(Self::on_dismiss_key));
361 }
362 let card = card
363 .children(heading)
364 .children(description)
365 .children(body.map(|body| div().flex_1().overflow_hidden().child(body)))
366 .children(footer)
367 .semantic_in(cx, spec);
368
369 let mut overlay = Overlay::edge(self.ident.child("overlay"), self.edge).child(card);
370 if self.dismissable && self.open {
371 let drawer = cx.entity().downgrade();
372 overlay = overlay.on_dismiss(move |window, cx| {
373 drawer
374 .update(cx, |drawer, cx| drawer.dismiss(window, cx))
375 .ok();
376 });
377 }
378 overlay.into_any_element()
379 }
380}