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