Skip to main content

gpui_base/
global_state.rs

1use std::rc::{Rc, Weak};
2
3use gpui::{App, Entity, Global, OwnedMenu};
4
5use crate::text::TextViewState;
6
7/// Holds the deferred interaction context open for as long as it is alive.
8///
9/// Handed out by [`GlobalState::register_deferred_popover`]; drop it to close
10/// the context again. The registry only ever weighs the token's liveness, so
11/// there is nothing inside to read.
12pub struct DeferredPopover(#[allow(dead_code)] Rc<()>);
13
14/// Application-wide state shared by Base behaviors.
15pub struct GlobalState {
16    app_menus: Vec<OwnedMenu>,
17    deferred_popovers: Vec<Weak<()>>,
18    suppress_text_selection: bool,
19    pub(crate) text_view_state_stack: Vec<Entity<TextViewState>>,
20    selection_document_order: u64,
21}
22
23impl Global for GlobalState {}
24
25impl GlobalState {
26    fn new() -> Self {
27        Self {
28            app_menus: Vec::new(),
29            deferred_popovers: Vec::new(),
30            suppress_text_selection: false,
31            text_view_state_stack: Vec::new(),
32            selection_document_order: 1,
33        }
34    }
35
36    /// Ensures that the Base global exists.
37    #[doc(hidden)]
38    pub fn init(cx: &mut App) {
39        if !cx.has_global::<Self>() {
40            cx.set_global(Self::new());
41        }
42    }
43
44    /// Suppresses window-level text selection for the current mouse down.
45    ///
46    /// Controls that own a press or drag interaction use this so the same
47    /// pointer event does not also start application text selection.
48    pub fn suppress_text_selection(cx: &mut App) {
49        Self::global_mut(cx).suppress_text_selection = true;
50    }
51
52    /// Clears the current mouse-down text-selection suppression.
53    #[doc(hidden)]
54    pub fn reset_text_selection_suppression(cx: &mut App) {
55        Self::global_mut(cx).suppress_text_selection = false;
56    }
57
58    /// Returns whether the current mouse down suppresses text selection.
59    #[doc(hidden)]
60    pub fn is_text_selection_suppressed(cx: &App) -> bool {
61        Self::global(cx).suppress_text_selection
62    }
63
64    pub fn global(cx: &App) -> &Self {
65        cx.global::<Self>()
66    }
67
68    pub fn global_mut(cx: &mut App) -> &mut Self {
69        cx.global_mut::<Self>()
70    }
71
72    pub(crate) fn text_view_state(&self) -> Option<&Entity<TextViewState>> {
73        self.text_view_state_stack.last()
74    }
75
76    #[doc(hidden)]
77    pub fn begin_selection_frame(&mut self) {
78        self.selection_document_order = 1;
79    }
80
81    pub(crate) fn next_selection_document_order(&mut self) -> u64 {
82        let order = self.selection_document_order;
83        self.selection_document_order = self.selection_document_order.wrapping_add(1);
84        order
85    }
86
87    /// Returns the application menus.
88    pub fn app_menus(&self) -> &[OwnedMenu] {
89        &self.app_menus
90    }
91
92    /// Replaces the application menus.
93    pub fn set_app_menus(&mut self, menus: Vec<OwnedMenu>) {
94        self.app_menus = menus;
95    }
96
97    /// Returns whether any deferred popup currently owns an open interaction
98    /// context.
99    pub fn is_in_deferred_context(cx: &App) -> bool {
100        Self::global(cx)
101            .deferred_popovers
102            .iter()
103            .any(|popover| popover.strong_count() > 0)
104    }
105
106    /// Registers an open deferred popup, which stays registered for as long as
107    /// the returned token is held.
108    ///
109    /// A token rather than an identifier, because popup state is routinely
110    /// dropped without ever being closed: state that stops being rendered — a
111    /// popover scrolled out of a virtual list, a panel closed while its menu is
112    /// open — is collected at the end of the frame, with no chance to
113    /// deregister. A registration that outlived its popup would leave the
114    /// application believing a popup is open forever, and everything that
115    /// steps aside for open popups (the native context menu of a text input,
116    /// say) would stay disabled for the rest of the session.
117    pub fn register_deferred_popover(cx: &mut App) -> DeferredPopover {
118        let token = Rc::new(());
119        let state = Self::global_mut(cx);
120        state
121            .deferred_popovers
122            .retain(|popover| popover.strong_count() > 0);
123        state.deferred_popovers.push(Rc::downgrade(&token));
124        DeferredPopover(token)
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[gpui::test]
133    fn initialization_is_idempotent_and_suppression_can_be_reset(cx: &mut gpui::TestAppContext) {
134        cx.update(|cx| {
135            GlobalState::init(cx);
136            GlobalState::suppress_text_selection(cx);
137            GlobalState::init(cx);
138            assert!(GlobalState::is_text_selection_suppressed(cx));
139
140            GlobalState::reset_text_selection_suppression(cx);
141            assert!(!GlobalState::is_text_selection_suppressed(cx));
142
143            assert!(!GlobalState::is_in_deferred_context(cx));
144            let popover = GlobalState::register_deferred_popover(cx);
145            assert!(GlobalState::is_in_deferred_context(cx));
146            drop(popover);
147            assert!(!GlobalState::is_in_deferred_context(cx));
148        });
149    }
150
151    /// Popup state is routinely dropped without being closed first, and a
152    /// registration that survived it would disable everything that steps aside
153    /// for an open popup, permanently.
154    #[gpui::test]
155    fn a_dropped_registration_closes_the_deferred_context(cx: &mut gpui::TestAppContext) {
156        cx.update(|cx| {
157            GlobalState::init(cx);
158
159            let outer = GlobalState::register_deferred_popover(cx);
160            {
161                let _inner = GlobalState::register_deferred_popover(cx);
162                assert!(GlobalState::is_in_deferred_context(cx));
163            }
164            assert!(GlobalState::is_in_deferred_context(cx));
165
166            drop(outer);
167            assert!(!GlobalState::is_in_deferred_context(cx));
168
169            // Registering again must not resurrect the collected ones.
170            let popover = GlobalState::register_deferred_popover(cx);
171            assert_eq!(GlobalState::global(cx).deferred_popovers.len(), 1);
172            drop(popover);
173            assert!(!GlobalState::is_in_deferred_context(cx));
174        });
175    }
176}