Skip to main content

gpui_base/
global_state.rs

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