Skip to main content

gpui_base/
global_state.rs

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