1use crate::{
2 ActiveTheme, ElementExt, Placement, StyledExt,
3 dialog::{ANIMATION_DURATION, Dialog},
4 input::{AnyInputState, Copy},
5 native_menu::FallbackMenuOverlay,
6 notification::{Notification, NotificationList},
7 sheet::Sheet,
8 tooltip::render_tooltip,
9 window_border,
10};
11use gpui::{
12 AnyView, App, AppContext, ClipboardItem, Context, DefiniteLength, ElementId, Entity,
13 FocusHandle, InteractiveElement, IntoElement, KeyBinding, ParentElement as _, Pixels, Render,
14 StyleRefinement, Styled, WeakFocusHandle, Window, actions, div, prelude::FluentBuilder as _,
15};
16use gpui_base::{TextSelection, TextSelectionLayer, TextSelectionScopeId};
17use std::{any::TypeId, rc::Rc};
18
19actions!(root, [Tab, TabPrev]);
20
21const CONTEXT: &str = "Root";
22pub(crate) fn init(cx: &mut App) {
23 cx.bind_keys([
24 KeyBinding::new("tab", Tab, Some(CONTEXT)),
25 KeyBinding::new("shift-tab", TabPrev, Some(CONTEXT)),
26 #[cfg(target_os = "macos")]
27 KeyBinding::new("cmd-c", Copy, Some(CONTEXT)),
28 #[cfg(not(target_os = "macos"))]
29 KeyBinding::new("ctrl-c", Copy, Some(CONTEXT)),
30 ]);
31}
32
33pub struct Root {
37 style: StyleRefinement,
38 view: AnyView,
39 pub(crate) active_sheet: Option<ActiveSheet>,
40 pub(crate) active_dialogs: Vec<ActiveDialog>,
41 pub(super) focused_input: Option<AnyInputState>,
42 pub notification: Entity<NotificationList>,
43 pub(crate) tooltip_overlay: Entity<gpui_base::TooltipOverlay>,
44 pub(crate) native_menu_overlay: Entity<FallbackMenuOverlay>,
45 sheet_size: Option<DefiniteLength>,
46 window_shadow_size: Pixels,
47 bordered: bool,
49 pending_focus_restore: Option<WeakFocusHandle>,
52 window_id: gpui::WindowId,
53}
54
55#[derive(Clone)]
56pub(crate) struct ActiveSheet {
57 focus_handle: FocusHandle,
58 previous_focused_handle: Option<WeakFocusHandle>,
60 placement: Placement,
61 selection_scope: TextSelectionScopeId,
62 builder: Rc<dyn Fn(Sheet, &mut Window, &mut App) -> Sheet + 'static>,
63}
64
65#[derive(Clone)]
66pub(crate) struct ActiveDialog {
67 focus_handle: FocusHandle,
68 previous_focused_handle: Option<WeakFocusHandle>,
70 selection_scope: TextSelectionScopeId,
71 builder: Rc<dyn Fn(Dialog, &mut Window, &mut App) -> Dialog + 'static>,
72}
73
74impl ActiveDialog {
75 pub(crate) fn new(
76 focus_handle: FocusHandle,
77 previous_focused_handle: Option<WeakFocusHandle>,
78 selection_scope: TextSelectionScopeId,
79 builder: impl Fn(Dialog, &mut Window, &mut App) -> Dialog + 'static,
80 ) -> Self {
81 Self {
82 focus_handle,
83 previous_focused_handle,
84 selection_scope,
85 builder: Rc::new(builder),
86 }
87 }
88}
89
90impl Root {
91 #[deprecated(note = "use gpui_base::TextSelection::clear instead")]
93 pub fn clear_text_selection(&mut self, cx: &mut Context<Self>) {
94 gpui_base::TextSelection::clear_for_window(self.window_id, cx);
95 }
96
97 pub fn new(view: impl Into<AnyView>, window: &mut Window, cx: &mut Context<Self>) -> Self {
99 #[cfg(all(target_os = "macos", not(test)))]
100 gpui_base::install_window_hit_test_forwarder(window);
101
102 Self {
103 style: StyleRefinement::default(),
104 view: view.into(),
105 active_sheet: None,
106 active_dialogs: Vec::new(),
107 focused_input: None,
108 notification: cx.new(|cx| NotificationList::new(window, cx)),
109 tooltip_overlay: cx
110 .new(|_| gpui_base::TooltipOverlay::new().render_with(render_tooltip)),
111 native_menu_overlay: cx.new(|_| FallbackMenuOverlay::new()),
112 sheet_size: None,
113 window_shadow_size: window_border::SHADOW_SIZE,
114 bordered: true,
115 pending_focus_restore: None,
116 window_id: window.window_handle().window_id(),
117 }
118 }
119
120 fn allocate_text_selection_scope(&mut self) -> TextSelectionScopeId {
121 TextSelectionScopeId::new()
122 }
123
124 pub(crate) fn active_text_selection_scope(&self) -> TextSelectionScopeId {
125 self.active_dialogs
126 .last()
127 .map(|dialog| dialog.selection_scope)
128 .or_else(|| {
129 self.active_sheet
130 .as_ref()
131 .map(|sheet| sheet.selection_scope)
132 })
133 .unwrap_or_default()
134 }
135
136 pub fn bordered(mut self, bordered: bool) -> Self {
141 self.bordered = bordered;
142 self
143 }
144
145 pub fn window_shadow_size(mut self, size: impl Into<Pixels>) -> Self {
149 self.window_shadow_size = size.into();
150 self
151 }
152
153 pub fn update<F, R>(window: &mut Window, cx: &mut App, f: F) -> R
154 where
155 F: FnOnce(&mut Self, &mut Window, &mut Context<Self>) -> R,
156 {
157 let root = window
158 .root::<Root>()
159 .flatten()
160 .expect("BUG: window first layer should be a gpui_component::Root.");
161
162 root.update(cx, |root, cx| f(root, window, cx))
163 }
164
165 pub(crate) fn try_update<F, R>(window: &mut Window, cx: &mut App, f: F) -> Option<R>
166 where
167 F: FnOnce(&mut Self, &mut Window, &mut Context<Self>) -> R,
168 {
169 let root = window.root::<Root>().flatten()?;
170 Some(root.update(cx, |root, cx| f(root, window, cx)))
171 }
172
173 pub fn read<'a>(window: &'a Window, cx: &'a App) -> &'a Self {
174 &window
175 .root::<Root>()
176 .expect("The window root view should be of type `ui::Root`.")
177 .unwrap()
178 .read(cx)
179 }
180
181 pub fn render_notification_layer(
183 window: &mut Window,
184 cx: &mut App,
185 ) -> Option<impl IntoElement + use<>> {
186 let root = window.root::<Root>()??;
187
188 let active_sheet_placement = root.read(cx).active_sheet.clone().map(|d| d.placement);
189
190 let sheet_size = root.read(cx).sheet_size;
191 let (mt, mr, mb, ml) = match active_sheet_placement {
192 Some(Placement::Top) => (sheet_size, None, None, None),
193 Some(Placement::Right) => (None, sheet_size, None, None),
194 Some(Placement::Bottom) => (None, None, sheet_size, None),
195 Some(Placement::Left) => (None, None, None, sheet_size),
196 _ => (None, None, None, None),
197 };
198
199 Some(
200 div()
201 .absolute()
202 .inset_0()
203 .when_some(mt, |this, offset| this.mt(offset))
204 .when_some(mr, |this, offset| this.mr(offset))
205 .when_some(mb, |this, offset| this.mb(offset))
206 .when_some(ml, |this, offset| this.ml(offset))
207 .child(root.read(cx).notification.clone()),
208 )
209 }
210
211 pub fn render_sheet_layer(
213 window: &mut Window,
214 cx: &mut App,
215 ) -> Option<impl IntoElement + use<>> {
216 let root = window.root::<Root>()??;
217
218 if let Some(active_sheet) = root.read(cx).active_sheet.clone() {
219 let mut sheet = Sheet::new(window, cx);
220 sheet = (active_sheet.builder)(sheet, window, cx);
221 sheet.focus_handle = active_sheet.focus_handle.clone();
222 sheet.placement = active_sheet.placement;
223 sheet.selection_scope = active_sheet.selection_scope;
224
225 let size = sheet.size;
226
227 return Some(
228 div()
229 .relative()
230 .child(sheet)
231 .on_prepaint(move |_, _, cx| root.update(cx, |r, _| r.sheet_size = Some(size))),
232 );
233 }
234
235 None
236 }
237
238 pub fn render_dialog_layer(
240 window: &mut Window,
241 cx: &mut App,
242 ) -> Option<impl IntoElement + use<>> {
243 let root = window.root::<Root>()??;
244
245 let active_dialogs = root.read(cx).active_dialogs.clone();
246
247 if active_dialogs.is_empty() {
248 return None;
249 }
250
251 let mut show_overlay_ix = None;
252
253 let mut dialogs = active_dialogs
254 .iter()
255 .enumerate()
256 .map(|(i, active_dialog)| {
257 let mut dialog = Dialog::new(cx);
258
259 dialog = (active_dialog.builder)(dialog, window, cx);
260
261 dialog.focus_handle = active_dialog.focus_handle.clone();
266 dialog.selection_scope = active_dialog.selection_scope;
267
268 dialog.layer_ix = i;
269 if dialog.has_overlay() {
271 show_overlay_ix = Some(i);
272 }
273
274 dialog
275 })
276 .collect::<Vec<_>>();
277
278 if let Some(ix) = show_overlay_ix {
279 if let Some(dialog) = dialogs.get_mut(ix) {
280 dialog.props.overlay_visible = true;
281 }
282 }
283
284 Some(
288 div()
289 .debug_selector(|| "dialog-layer".to_string())
290 .children(dialogs),
291 )
292 }
293
294 pub fn open_dialog<F>(&mut self, build: F, window: &mut Window, cx: &mut Context<'_, Root>)
295 where
296 F: Fn(Dialog, &mut Window, &mut App) -> Dialog + 'static,
297 {
298 let mut previous_focused_handle = window.focused(cx).map(|h| h.downgrade());
299
300 if let Some(pending_handle) = self.pending_focus_restore.take() {
303 previous_focused_handle = Some(pending_handle);
304 }
305
306 let focus_handle = cx.focus_handle();
307 focus_handle.focus(window, cx);
308
309 let selection_scope = self.allocate_text_selection_scope();
310 self.active_dialogs.push(ActiveDialog::new(
311 focus_handle,
312 previous_focused_handle,
313 selection_scope,
314 build,
315 ));
316 gpui_base::TextSelection::clear(window, cx);
319 cx.notify();
320 }
321
322 fn close_dialog_internal(&mut self) -> Option<FocusHandle> {
323 self.focused_input = None;
324 self.active_dialogs
325 .pop()
326 .and_then(|d| d.previous_focused_handle)
327 .and_then(|h| h.upgrade())
328 }
329
330 pub fn close_dialog(&mut self, window: &mut Window, cx: &mut Context<'_, Root>) {
331 if let Some(handle) = self.close_dialog_internal() {
332 window.focus(&handle, cx);
333 }
334 gpui_base::TextSelection::clear(window, cx);
335 cx.notify();
336 }
337
338 pub(crate) fn defer_close_dialog(&mut self, window: &mut Window, cx: &mut Context<'_, Root>) {
339 if let Some(handle) = self.close_dialog_internal() {
340 let dialogs_count = self.active_dialogs.len();
341
342 self.pending_focus_restore = Some(handle.downgrade());
344
345 cx.spawn_in(window, async move |this, cx| {
346 cx.background_executor().timer(*ANIMATION_DURATION).await;
347 let _ = this.update_in(cx, |this, window, cx| {
348 let current_dialogs_count = this.active_dialogs.len();
349 if current_dialogs_count == dialogs_count {
351 window.focus(&handle, cx);
352 }
353 this.pending_focus_restore = None;
354 });
355 })
356 .detach();
357 }
358 gpui_base::TextSelection::clear(window, cx);
359 cx.notify();
360 }
361
362 pub fn close_all_dialogs(&mut self, window: &mut Window, cx: &mut Context<'_, Root>) {
363 self.focused_input = None;
364 let previous_focused_handle = self
365 .active_dialogs
366 .first()
367 .and_then(|d| d.previous_focused_handle.clone());
368 self.active_dialogs.clear();
369 if let Some(handle) = previous_focused_handle.and_then(|h| h.upgrade()) {
370 window.focus(&handle, cx);
371 }
372 gpui_base::TextSelection::clear(window, cx);
373 cx.notify();
374 }
375
376 pub fn open_sheet_at<F>(
377 &mut self,
378 placement: Placement,
379 build: F,
380 window: &mut Window,
381 cx: &mut Context<'_, Root>,
382 ) where
383 F: Fn(Sheet, &mut Window, &mut App) -> Sheet + 'static,
384 {
385 let previous_focused_handle = self
386 .active_sheet
387 .take()
388 .and_then(|s| s.previous_focused_handle)
389 .or_else(|| window.focused(cx).map(|h| h.downgrade()));
390
391 let focus_handle = cx.focus_handle();
392 focus_handle.focus(window, cx);
393 let selection_scope = self.allocate_text_selection_scope();
394 self.active_sheet = Some(ActiveSheet {
395 focus_handle,
396 previous_focused_handle,
397 placement,
398 selection_scope,
399 builder: Rc::new(build),
400 });
401 gpui_base::TextSelection::clear(window, cx);
404 cx.notify();
405 }
406
407 pub fn close_sheet(&mut self, window: &mut Window, cx: &mut Context<'_, Root>) {
408 self.focused_input = None;
409 if let Some(previous_handle) = self
410 .active_sheet
411 .as_ref()
412 .and_then(|s| s.previous_focused_handle.as_ref())
413 .and_then(|h| h.upgrade())
414 {
415 window.focus(&previous_handle, cx);
416 }
417 self.active_sheet = None;
418 gpui_base::TextSelection::clear(window, cx);
419 cx.notify();
420 }
421
422 pub fn push_notification(
423 &mut self,
424 note: impl Into<Notification>,
425 window: &mut Window,
426 cx: &mut Context<'_, Root>,
427 ) {
428 self.notification
429 .update(cx, |view, cx| view.push(note, window, cx));
430 cx.notify();
431 }
432
433 pub fn remove_notification<T: Sized + 'static>(
436 &mut self,
437 window: &mut Window,
438 cx: &mut Context<'_, Root>,
439 ) {
440 self.notification.update(cx, |view, cx| {
441 view.close_by_type(TypeId::of::<T>(), window, cx);
442 });
443 cx.notify();
444 }
445
446 pub fn remove_notification1<T: Sized + 'static>(
448 &mut self,
449 key: impl Into<ElementId>,
450 window: &mut Window,
451 cx: &mut Context<'_, Root>,
452 ) {
453 let key = key.into();
454 self.notification.update(cx, |view, cx| {
455 view.close((TypeId::of::<T>(), key), window, cx);
456 });
457 cx.notify();
458 }
459
460 pub fn clear_notifications(&mut self, window: &mut Window, cx: &mut Context<'_, Root>) {
461 self.notification
462 .update(cx, |view, cx| view.clear(window, cx));
463 cx.notify();
464 }
465
466 pub(crate) fn tooltip_overlay(
468 window: &Window,
469 cx: &App,
470 ) -> Option<Entity<gpui_base::TooltipOverlay>> {
471 let root = window.root::<Root>()??;
472 Some(root.read(cx).tooltip_overlay.clone())
473 }
474
475 pub(crate) fn native_menu_overlay(
477 window: &Window,
478 cx: &App,
479 ) -> Option<Entity<FallbackMenuOverlay>> {
480 let root = window.root::<Root>()??;
481 Some(root.read(cx).native_menu_overlay.clone())
482 }
483
484 pub fn view(&self) -> &AnyView {
486 &self.view
487 }
488
489 fn on_action_tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
490 if let Some(container_focus_handle) = gpui_base::active_focus_trap(window, cx) {
492 let before_focus = window.focused(cx);
494
495 window.focus_next(cx);
497
498 if !container_focus_handle.contains_focused(window, cx) {
500 let mut attempts = 0;
503 const MAX_ATTEMPTS: usize = 100; while !container_focus_handle.contains_focused(window, cx)
506 && attempts < MAX_ATTEMPTS
507 {
508 window.focus_next(cx);
509 attempts += 1;
510
511 if window.focused(cx) == before_focus {
513 break;
514 }
515 }
516 }
517 return;
518 }
519
520 window.focus_next(cx);
522 }
523
524 fn on_action_tab_prev(&mut self, _: &TabPrev, window: &mut Window, cx: &mut Context<Self>) {
525 if let Some(container_focus_handle) = gpui_base::active_focus_trap(window, cx) {
527 let before_focus = window.focused(cx);
529
530 window.focus_prev(cx);
532
533 if !container_focus_handle.contains_focused(window, cx) {
535 let mut attempts = 0;
538 const MAX_ATTEMPTS: usize = 100; while !container_focus_handle.contains_focused(window, cx)
541 && attempts < MAX_ATTEMPTS
542 {
543 window.focus_prev(cx);
544 attempts += 1;
545
546 if window.focused(cx) == before_focus {
548 break;
549 }
550 }
551 }
552 return;
553 }
554
555 window.focus_prev(cx);
557 }
558
559 fn on_action_copy(&mut self, _: &Copy, window: &mut Window, cx: &mut Context<Self>) {
560 let text = gpui_base::TextSelection::selected_text(window, cx)
561 .trim()
562 .to_string();
563 if text.is_empty() {
564 cx.propagate();
565 return;
566 }
567 cx.write_to_clipboard(ClipboardItem::new_string(text));
568 }
569}
570
571impl Styled for Root {
572 fn style(&mut self) -> &mut StyleRefinement {
573 &mut self.style
574 }
575}
576
577impl Render for Root {
578 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
579 window.set_rem_size(cx.theme().font_size);
580 let active_scope = self.active_text_selection_scope();
581 TextSelection::activate_scope(active_scope, window, cx);
582
583 let inner = div()
584 .id("root")
585 .key_context(CONTEXT)
586 .on_action(cx.listener(Self::on_action_tab))
587 .on_action(cx.listener(Self::on_action_tab_prev))
588 .on_action(cx.listener(Self::on_action_copy))
589 .relative()
590 .size_full()
591 .font_family(cx.theme().font_family.clone())
592 .bg(cx.theme().tokens.background)
593 .text_color(cx.theme().foreground)
594 .refine_style(&self.style)
595 .child(TextSelectionLayer)
596 .child(self.view.clone())
597 .child(self.tooltip_overlay.clone())
598 .child(self.native_menu_overlay.clone());
599
600 if self.bordered {
601 window_border()
602 .shadow_size(self.window_shadow_size)
603 .child(inner)
604 .into_any_element()
605 } else {
606 inner.into_any_element()
607 }
608 }
609}
610
611#[cfg(test)]
612mod tests {
613 use super::*;
614 use gpui::TestAppContext;
615
616 struct TestView;
617
618 impl Render for TestView {
619 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
620 div()
621 }
622 }
623
624 #[gpui::test]
625 fn bordered_builder_toggles_window_border(cx: &mut TestAppContext) {
626 cx.update(crate::init);
627
628 let (default_root, _) = cx.add_window_view(|window, cx| {
629 let view = cx.new(|_| TestView);
630 Root::new(view, window, cx)
631 });
632 assert!(default_root.read_with(cx, |root, _| root.bordered));
633
634 let (root, _) = cx.add_window_view(|window, cx| {
635 let view = cx.new(|_| TestView);
636 Root::new(view, window, cx).bordered(false)
637 });
638 assert!(!root.read_with(cx, |root, _| root.bordered));
639
640 let (root, _) = cx.add_window_view(|window, cx| {
641 let view = cx.new(|_| TestView);
642 Root::new(view, window, cx).bordered(false).bordered(true)
643 });
644 assert!(root.read_with(cx, |root, _| root.bordered));
645 }
646}