Store focus handles in AppContext instead of Window (#22158)

Nathan Sobo created

Previously, each window stored its own collection of focus handles. This
meant that to create a focus handle, you needed to have access to a
Window. I'm working on a simplification to gpui's context types that
removes `WindowContext` and `ViewContext` in favor of passing a window
reference explicitly when rendering or handling events. You'll still
need a window to manipulate focus, but it will be helpful to be able to
create focus handles without a window.

cc @mgsloan 

Release Notes:

- N/A

Change summary

crates/gpui/src/app.rs     | 67 +++++++++++++++++++++++----------------
crates/gpui/src/element.rs |  2 
crates/gpui/src/window.rs  | 14 +------
3 files changed, 43 insertions(+), 40 deletions(-)

Detailed changes

crates/gpui/src/app.rs 🔗

@@ -5,7 +5,10 @@ use std::{
     ops::{Deref, DerefMut},
     path::{Path, PathBuf},
     rc::{Rc, Weak},
-    sync::{atomic::Ordering::SeqCst, Arc},
+    sync::{
+        atomic::{AtomicUsize, Ordering::SeqCst},
+        Arc,
+    },
     time::Duration,
 };
 
@@ -16,6 +19,7 @@ use futures::{
     future::{LocalBoxFuture, Shared},
     Future, FutureExt,
 };
+use parking_lot::RwLock;
 use slotmap::SlotMap;
 
 pub use async_context::*;
@@ -30,11 +34,12 @@ use util::ResultExt;
 use crate::{
     current_platform, hash, init_app_menus, Action, ActionRegistry, Any, AnyView, AnyWindowHandle,
     Asset, AssetSource, BackgroundExecutor, ClipboardItem, Context, DispatchPhase, DisplayId,
-    Entity, EventEmitter, ForegroundExecutor, Global, KeyBinding, Keymap, Keystroke, LayoutId,
-    Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform, PlatformDisplay, Point,
-    PromptBuilder, PromptHandle, PromptLevel, Render, RenderablePromptHandle, Reservation,
-    ScreenCaptureSource, SharedString, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem,
-    View, ViewContext, Window, WindowAppearance, WindowContext, WindowHandle, WindowId,
+    Entity, EventEmitter, FocusHandle, FocusId, ForegroundExecutor, Global, KeyBinding, Keymap,
+    Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform,
+    PlatformDisplay, Point, PromptBuilder, PromptHandle, PromptLevel, Render,
+    RenderablePromptHandle, Reservation, ScreenCaptureSource, SharedString, SubscriberSet,
+    Subscription, SvgRenderer, Task, TextSystem, View, ViewContext, Window, WindowAppearance,
+    WindowContext, WindowHandle, WindowId,
 };
 
 mod async_context;
@@ -242,6 +247,7 @@ pub struct AppContext {
     pub(crate) new_view_observers: SubscriberSet<TypeId, NewViewListener>,
     pub(crate) windows: SlotMap<WindowId, Option<Window>>,
     pub(crate) window_handles: FxHashMap<WindowId, AnyWindowHandle>,
+    pub(crate) focus_handles: Arc<RwLock<SlotMap<FocusId, AtomicUsize>>>,
     pub(crate) keymap: Rc<RefCell<Keymap>>,
     pub(crate) keyboard_layout: SharedString,
     pub(crate) global_action_listeners:
@@ -302,8 +308,9 @@ impl AppContext {
                 entities,
                 new_view_observers: SubscriberSet::new(),
                 new_model_observers: SubscriberSet::new(),
-                window_handles: FxHashMap::default(),
                 windows: SlotMap::with_key(),
+                window_handles: FxHashMap::default(),
+                focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
                 keymap: Rc::new(RefCell::new(Keymap::default())),
                 keyboard_layout,
                 global_action_listeners: FxHashMap::default(),
@@ -439,6 +446,7 @@ impl AppContext {
         self.defer(move |_| activate());
         subscription
     }
+
     pub(crate) fn observe_internal<W, E>(
         &mut self,
         entity: &E,
@@ -569,6 +577,12 @@ impl AppContext {
         })
     }
 
+    /// Obtain a new [`FocusHandle`], which allows you to track and manipulate the keyboard focus
+    /// for elements rendered within this window.
+    pub fn focus_handle(&self) -> FocusHandle {
+        FocusHandle::new(&self.focus_handles)
+    }
+
     /// Instructs the platform to activate the application by bringing it to the foreground.
     pub fn activate(&self, ignoring_other_apps: bool) {
         self.platform.activate(ignoring_other_apps);
@@ -844,28 +858,25 @@ impl AppContext {
 
     /// Repeatedly called during `flush_effects` to handle a focused handle being dropped.
     fn release_dropped_focus_handles(&mut self) {
-        for window_handle in self.windows() {
-            window_handle
-                .update(self, |_, cx| {
-                    let mut blur_window = false;
-                    let focus = cx.window.focus;
-                    cx.window.focus_handles.write().retain(|handle_id, count| {
-                        if count.load(SeqCst) == 0 {
-                            if focus == Some(handle_id) {
-                                blur_window = true;
-                            }
-                            false
-                        } else {
-                            true
-                        }
-                    });
-
-                    if blur_window {
-                        cx.blur();
+        self.focus_handles
+            .clone()
+            .write()
+            .retain(|handle_id, count| {
+                if count.load(SeqCst) == 0 {
+                    for window_handle in self.windows() {
+                        window_handle
+                            .update(self, |_, cx| {
+                                if cx.window.focus == Some(handle_id) {
+                                    cx.blur();
+                                }
+                            })
+                            .unwrap();
                     }
-                })
-                .unwrap();
-        }
+                    false
+                } else {
+                    true
+                }
+            });
     }
 
     fn apply_notify_effect(&mut self, emitter: EntityId) {

crates/gpui/src/element.rs 🔗

@@ -500,7 +500,7 @@ impl AnyElement {
 
         if !focus_assigned {
             if let Some(focus_id) = cx.window.next_frame.focus {
-                return FocusHandle::for_id(focus_id, &cx.window.focus_handles);
+                return FocusHandle::for_id(focus_id, &cx.focus_handles);
             }
         }
 

crates/gpui/src/window.rs 🔗

@@ -531,7 +531,6 @@ pub struct Window {
     pub(crate) tooltip_bounds: Option<TooltipBounds>,
     next_frame_callbacks: Rc<RefCell<Vec<FrameCallback>>>,
     pub(crate) dirty_views: FxHashSet<EntityId>,
-    pub(crate) focus_handles: Arc<RwLock<SlotMap<FocusId, AtomicUsize>>>,
     focus_listeners: SubscriberSet<(), AnyWindowFocusListener>,
     focus_lost_listeners: SubscriberSet<(), AnyObserver>,
     default_prevented: bool,
@@ -809,7 +808,6 @@ impl Window {
             next_tooltip_id: TooltipId::default(),
             tooltip_bounds: None,
             dirty_views: FxHashSet::default(),
-            focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
             focus_listeners: SubscriberSet::new(),
             focus_lost_listeners: SubscriberSet::new(),
             default_prevented: true,
@@ -931,17 +929,11 @@ impl<'a> WindowContext<'a> {
         self.window.removed = true;
     }
 
-    /// Obtain a new [`FocusHandle`], which allows you to track and manipulate the keyboard focus
-    /// for elements rendered within this window.
-    pub fn focus_handle(&self) -> FocusHandle {
-        FocusHandle::new(&self.window.focus_handles)
-    }
-
     /// Obtain the currently focused [`FocusHandle`]. If no elements are focused, returns `None`.
     pub fn focused(&self) -> Option<FocusHandle> {
         self.window
             .focus
-            .and_then(|id| FocusHandle::for_id(id, &self.window.focus_handles))
+            .and_then(|id| FocusHandle::for_id(id, &self.app.focus_handles))
     }
 
     /// Move focus to the element associated with the given [`FocusHandle`].
@@ -3021,7 +3013,7 @@ impl<'a> WindowContext<'a> {
                         let event = FocusOutEvent {
                             blurred: WeakFocusHandle {
                                 id: blurred_id,
-                                handles: Arc::downgrade(&cx.window.focus_handles),
+                                handles: Arc::downgrade(&cx.app.focus_handles),
                             },
                         };
                         listener(event, cx)
@@ -4439,7 +4431,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
                             let event = FocusOutEvent {
                                 blurred: WeakFocusHandle {
                                     id: blurred_id,
-                                    handles: Arc::downgrade(&cx.window.focus_handles),
+                                    handles: Arc::downgrade(&cx.app.focus_handles),
                                 },
                             };
                             listener(view, event, cx)