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