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
14pub struct DeferredPopover(#[allow(dead_code)] Rc<()>);
20
21pub struct GlobalState {
23 app_menus: Vec<OwnedMenu>,
24 deferred_popovers: Vec<Weak<()>>,
25 suppress_text_selection: bool,
26 text_view_state_stack: RefCell<Vec<Entity<TextViewState>>>,
30 table_cell_stack: RefCell<Vec<Bounds<Pixels>>>,
32 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 pub fn note_touch(cx: &mut App) {
54 Self::init(cx);
55 Self::global_mut(cx).last_touch = Some(Instant::now());
56 }
57
58 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 #[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 pub fn suppress_text_selection(cx: &mut App) {
80 Self::global_mut(cx).suppress_text_selection = true;
81 }
82
83 #[doc(hidden)]
85 pub fn reset_text_selection_suppression(cx: &mut App) {
86 Self::global_mut(cx).suppress_text_selection = false;
87 }
88
89 #[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 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 pub(crate) fn table_cell(&self) -> Option<Bounds<Pixels>> {
126 self.table_cell_stack.borrow().last().copied()
127 }
128
129 #[doc(hidden)]
130 pub fn begin_selection_frame(&self) {}
132
133 pub fn app_menus(&self) -> &[OwnedMenu] {
135 &self.app_menus
136 }
137
138 pub fn set_app_menus(&mut self, menus: Vec<OwnedMenu>) {
140 self.app_menus = menus;
141 }
142
143 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 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
174pub(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 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 #[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 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}