Skip to main content

gpui_base/
global_state.rs

1#[cfg(not(target_family = "wasm"))]
2use std::time::Instant;
3use std::{
4    cell::RefCell,
5    rc::{Rc, Weak},
6};
7#[cfg(target_family = "wasm")]
8use web_time::Instant;
9
10use gpui::{App, Bounds, Entity, Global, OwnedMenu, Pixels, Point};
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 table cells being prepainted or painted, innermost last.
31    table_cell_stack: RefCell<Vec<Bounds<Pixels>>>,
32    /// When a finger last went down. A tap reaches controls as a mouse press;
33    /// this is how they tell it from one.
34    last_touch: Option<Instant>,
35}
36
37impl Global for GlobalState {}
38
39impl GlobalState {
40    fn new() -> Self {
41        Self {
42            app_menus: Vec::new(),
43            deferred_popovers: Vec::new(),
44            suppress_text_selection: false,
45            text_view_state_stack: RefCell::new(Vec::new()),
46            table_cell_stack: RefCell::new(Vec::new()),
47            last_touch: None,
48        }
49    }
50
51    /// Records that a finger went down. Called from the touch drag GPUI
52    /// offers on every touch, before it becomes a tap, a long press or a pan.
53    pub fn note_touch(cx: &mut App) {
54        Self::init(cx);
55        Self::global_mut(cx).last_touch = Some(Instant::now());
56    }
57
58    /// Whether the press being handled came from a finger: a touch went
59    /// down recently enough that the mouse events of a tap are still
60    /// arriving. A double tap takes up to twice the tap interval.
61    pub fn is_touch_press(cx: &App) -> bool {
62        cx.try_global::<Self>()
63            .and_then(|state| state.last_touch)
64            .is_some_and(|at| at.elapsed() < std::time::Duration::from_secs(1))
65    }
66
67    /// Ensures that the Base global exists.
68    #[doc(hidden)]
69    pub fn init(cx: &mut App) {
70        if !cx.has_global::<Self>() {
71            cx.set_global(Self::new());
72        }
73    }
74
75    /// Suppresses window-level text selection for the current mouse down.
76    ///
77    /// Controls that own a press or drag interaction use this so the same
78    /// pointer event does not also start application text selection.
79    pub fn suppress_text_selection(cx: &mut App) {
80        Self::global_mut(cx).suppress_text_selection = true;
81    }
82
83    /// Clears the current mouse-down text-selection suppression.
84    #[doc(hidden)]
85    pub fn reset_text_selection_suppression(cx: &mut App) {
86        Self::global_mut(cx).suppress_text_selection = false;
87    }
88
89    /// Returns whether the current mouse down suppresses text selection.
90    #[doc(hidden)]
91    pub fn is_text_selection_suppressed(cx: &App) -> bool {
92        Self::global(cx).suppress_text_selection
93    }
94
95    pub fn global(cx: &App) -> &Self {
96        cx.global::<Self>()
97    }
98
99    pub fn global_mut(cx: &mut App) -> &mut Self {
100        cx.global_mut::<Self>()
101    }
102
103    pub(crate) fn text_view_state(&self) -> Option<Entity<TextViewState>> {
104        self.text_view_state_stack.borrow().last().cloned()
105    }
106
107    pub(crate) fn push_text_view_state(&self, state: Entity<TextViewState>) {
108        self.text_view_state_stack.borrow_mut().push(state);
109    }
110
111    pub(crate) fn pop_text_view_state(&self) {
112        self.text_view_state_stack.borrow_mut().pop();
113    }
114
115    /// Enters the table cell being prepainted or painted.
116    pub(crate) fn push_table_cell(&self, bounds: Bounds<Pixels>) {
117        self.table_cell_stack.borrow_mut().push(bounds);
118    }
119
120    pub(crate) fn pop_table_cell(&self) {
121        self.table_cell_stack.borrow_mut().pop();
122    }
123
124    /// The table cell being prepainted or painted, if any.
125    pub(crate) fn table_cell(&self) -> Option<Bounds<Pixels>> {
126        self.table_cell_stack.borrow().last().copied()
127    }
128
129    #[doc(hidden)]
130    // Kept for compatibility; automatic order now belongs to the window's selection state.
131    pub fn begin_selection_frame(&self) {}
132
133    /// Returns the application menus.
134    pub fn app_menus(&self) -> &[OwnedMenu] {
135        &self.app_menus
136    }
137
138    /// Replaces the application menus.
139    pub fn set_app_menus(&mut self, menus: Vec<OwnedMenu>) {
140        self.app_menus = menus;
141    }
142
143    /// Returns whether any deferred popup currently owns an open interaction
144    /// context.
145    pub fn is_in_deferred_context(cx: &App) -> bool {
146        Self::global(cx)
147            .deferred_popovers
148            .iter()
149            .any(|popover| popover.strong_count() > 0)
150    }
151
152    /// Registers an open deferred popup, which stays registered for as long as
153    /// the returned token is held.
154    ///
155    /// A token rather than an identifier, because popup state is routinely
156    /// dropped without ever being closed: state that stops being rendered — a
157    /// popover scrolled out of a virtual list, a panel closed while its menu is
158    /// open — is collected at the end of the frame, with no chance to
159    /// deregister. A registration that outlived its popup would leave the
160    /// application believing a popup is open forever, and everything that
161    /// steps aside for open popups (the native context menu of a text input,
162    /// say) would stay disabled for the rest of the session.
163    pub fn register_deferred_popover(cx: &mut App) -> DeferredPopover {
164        let token = Rc::new(());
165        let state = Self::global_mut(cx);
166        state
167            .deferred_popovers
168            .retain(|popover| popover.strong_count() > 0);
169        state.deferred_popovers.push(Rc::downgrade(&token));
170        DeferredPopover(token)
171    }
172}
173
174/// Confines a selection to its anchor cell and excludes text in other cells.
175pub(crate) fn confine_to_cell(
176    anchor_cell: Option<(Point<Pixels>, Bounds<Pixels>)>,
177    painting: Option<Bounds<Pixels>>,
178    anchor: Point<Pixels>,
179    cursor: Point<Pixels>,
180) -> Option<(Point<Pixels>, Point<Pixels>)> {
181    let Some((_, cell)) = anchor_cell.filter(|(cell_anchor, _)| *cell_anchor == anchor) else {
182        return Some((anchor, cursor));
183    };
184    if painting != Some(cell) {
185        return None;
186    }
187    let cursor = gpui::point(
188        cursor.x.clamp(cell.left(), cell.right()),
189        cursor.y.clamp(cell.top(), cell.bottom()),
190    );
191    Some((anchor, cursor))
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    mod table_cell {
199        use gpui::{Bounds, point, px, size};
200
201        use super::super::confine_to_cell;
202
203        fn cell(x: f32) -> Bounds<gpui::Pixels> {
204            Bounds::new(point(px(x), px(0.)), size(px(100.), px(30.)))
205        }
206
207        #[test]
208        fn a_drag_outside_a_cell_is_unchanged() {
209            let (anchor, cursor) = (point(px(5.), px(5.)), point(px(250.), px(80.)));
210            assert_eq!(
211                confine_to_cell(None, Some(cell(0.)), anchor, cursor),
212                Some((anchor, cursor))
213            );
214            // A cell from an older drag does not hold this anchor.
215            let old = Some((point(px(50.), px(10.)), cell(0.)));
216            assert_eq!(
217                confine_to_cell(old, None, anchor, cursor),
218                Some((anchor, cursor))
219            );
220        }
221
222        #[test]
223        fn a_drag_from_a_cell_selects_nothing_outside_it() {
224            let anchor = point(px(10.), px(10.));
225            let anchor_cell = Some((anchor, cell(0.)));
226            let cursor = point(px(150.), px(20.));
227            assert_eq!(
228                confine_to_cell(anchor_cell, Some(cell(100.)), anchor, cursor),
229                None
230            );
231            assert_eq!(confine_to_cell(anchor_cell, None, anchor, cursor), None);
232        }
233
234        #[test]
235        fn a_drag_from_a_cell_is_clamped_to_it() {
236            let anchor = point(px(10.), px(10.));
237            let anchor_cell = Some((anchor, cell(0.)));
238            assert_eq!(
239                confine_to_cell(
240                    anchor_cell,
241                    Some(cell(0.)),
242                    anchor,
243                    point(px(250.), px(80.))
244                ),
245                Some((anchor, point(px(100.), px(30.))))
246            );
247            assert_eq!(
248                confine_to_cell(
249                    anchor_cell,
250                    Some(cell(0.)),
251                    anchor,
252                    point(px(-20.), px(-5.))
253                ),
254                Some((anchor, point(px(0.), px(0.))))
255            );
256            let inside = point(px(60.), px(12.));
257            assert_eq!(
258                confine_to_cell(anchor_cell, Some(cell(0.)), anchor, inside),
259                Some((anchor, inside))
260            );
261        }
262    }
263
264    #[gpui::test]
265    fn initialization_is_idempotent_and_suppression_can_be_reset(cx: &mut gpui::TestAppContext) {
266        cx.update(|cx| {
267            GlobalState::init(cx);
268            GlobalState::suppress_text_selection(cx);
269            GlobalState::init(cx);
270            assert!(GlobalState::is_text_selection_suppressed(cx));
271
272            GlobalState::reset_text_selection_suppression(cx);
273            assert!(!GlobalState::is_text_selection_suppressed(cx));
274
275            assert!(!GlobalState::is_in_deferred_context(cx));
276            let popover = GlobalState::register_deferred_popover(cx);
277            assert!(GlobalState::is_in_deferred_context(cx));
278            drop(popover);
279            assert!(!GlobalState::is_in_deferred_context(cx));
280        });
281    }
282
283    /// Popup state is routinely dropped without being closed first, and a
284    /// registration that survived it would disable everything that steps aside
285    /// for an open popup, permanently.
286    #[gpui::test]
287    fn a_dropped_registration_closes_the_deferred_context(cx: &mut gpui::TestAppContext) {
288        cx.update(|cx| {
289            GlobalState::init(cx);
290
291            let outer = GlobalState::register_deferred_popover(cx);
292            {
293                let _inner = GlobalState::register_deferred_popover(cx);
294                assert!(GlobalState::is_in_deferred_context(cx));
295            }
296            assert!(GlobalState::is_in_deferred_context(cx));
297
298            drop(outer);
299            assert!(!GlobalState::is_in_deferred_context(cx));
300
301            // Registering again must not resurrect the collected ones.
302            let popover = GlobalState::register_deferred_popover(cx);
303            assert_eq!(GlobalState::global(cx).deferred_popovers.len(), 1);
304            drop(popover);
305            assert!(!GlobalState::is_in_deferred_context(cx));
306        });
307    }
308}