#[cfg(not(target_family = "wasm"))]
use std::time::Instant;
use std::{
cell::RefCell,
rc::{Rc, Weak},
};
#[cfg(target_family = "wasm")]
use web_time::Instant;
use gpui::{App, Bounds, Entity, Global, OwnedMenu, Pixels, Point};
use crate::text::TextViewState;
pub struct DeferredPopover(#[allow(dead_code)] Rc<()>);
pub struct GlobalState {
app_menus: Vec<OwnedMenu>,
deferred_popovers: Vec<Weak<()>>,
suppress_text_selection: bool,
text_view_state_stack: RefCell<Vec<Entity<TextViewState>>>,
table_cell_stack: RefCell<Vec<Bounds<Pixels>>>,
last_touch: Option<Instant>,
}
impl Global for GlobalState {}
impl GlobalState {
fn new() -> Self {
Self {
app_menus: Vec::new(),
deferred_popovers: Vec::new(),
suppress_text_selection: false,
text_view_state_stack: RefCell::new(Vec::new()),
table_cell_stack: RefCell::new(Vec::new()),
last_touch: None,
}
}
pub fn note_touch(cx: &mut App) {
Self::init(cx);
Self::global_mut(cx).last_touch = Some(Instant::now());
}
pub fn is_touch_press(cx: &App) -> bool {
cx.try_global::<Self>()
.and_then(|state| state.last_touch)
.is_some_and(|at| at.elapsed() < std::time::Duration::from_secs(1))
}
#[doc(hidden)]
pub fn init(cx: &mut App) {
if !cx.has_global::<Self>() {
cx.set_global(Self::new());
}
}
pub fn suppress_text_selection(cx: &mut App) {
Self::global_mut(cx).suppress_text_selection = true;
}
#[doc(hidden)]
pub fn reset_text_selection_suppression(cx: &mut App) {
Self::global_mut(cx).suppress_text_selection = false;
}
#[doc(hidden)]
pub fn is_text_selection_suppressed(cx: &App) -> bool {
Self::global(cx).suppress_text_selection
}
pub fn global(cx: &App) -> &Self {
cx.global::<Self>()
}
pub fn global_mut(cx: &mut App) -> &mut Self {
cx.global_mut::<Self>()
}
pub(crate) fn text_view_state(&self) -> Option<Entity<TextViewState>> {
self.text_view_state_stack.borrow().last().cloned()
}
pub(crate) fn push_text_view_state(&self, state: Entity<TextViewState>) {
self.text_view_state_stack.borrow_mut().push(state);
}
pub(crate) fn pop_text_view_state(&self) {
self.text_view_state_stack.borrow_mut().pop();
}
pub(crate) fn push_table_cell(&self, bounds: Bounds<Pixels>) {
self.table_cell_stack.borrow_mut().push(bounds);
}
pub(crate) fn pop_table_cell(&self) {
self.table_cell_stack.borrow_mut().pop();
}
pub(crate) fn table_cell(&self) -> Option<Bounds<Pixels>> {
self.table_cell_stack.borrow().last().copied()
}
#[doc(hidden)]
pub fn begin_selection_frame(&self) {}
pub fn app_menus(&self) -> &[OwnedMenu] {
&self.app_menus
}
pub fn set_app_menus(&mut self, menus: Vec<OwnedMenu>) {
self.app_menus = menus;
}
pub fn is_in_deferred_context(cx: &App) -> bool {
Self::global(cx)
.deferred_popovers
.iter()
.any(|popover| popover.strong_count() > 0)
}
pub fn register_deferred_popover(cx: &mut App) -> DeferredPopover {
let token = Rc::new(());
let state = Self::global_mut(cx);
state
.deferred_popovers
.retain(|popover| popover.strong_count() > 0);
state.deferred_popovers.push(Rc::downgrade(&token));
DeferredPopover(token)
}
}
pub(crate) fn confine_to_cell(
anchor_cell: Option<(Point<Pixels>, Bounds<Pixels>)>,
painting: Option<Bounds<Pixels>>,
anchor: Point<Pixels>,
cursor: Point<Pixels>,
) -> Option<(Point<Pixels>, Point<Pixels>)> {
let Some((_, cell)) = anchor_cell.filter(|(cell_anchor, _)| *cell_anchor == anchor) else {
return Some((anchor, cursor));
};
if painting != Some(cell) {
return None;
}
let cursor = gpui::point(
cursor.x.clamp(cell.left(), cell.right()),
cursor.y.clamp(cell.top(), cell.bottom()),
);
Some((anchor, cursor))
}
#[cfg(test)]
mod tests {
use super::*;
mod table_cell {
use gpui::{Bounds, point, px, size};
use super::super::confine_to_cell;
fn cell(x: f32) -> Bounds<gpui::Pixels> {
Bounds::new(point(px(x), px(0.)), size(px(100.), px(30.)))
}
#[test]
fn a_drag_outside_a_cell_is_unchanged() {
let (anchor, cursor) = (point(px(5.), px(5.)), point(px(250.), px(80.)));
assert_eq!(
confine_to_cell(None, Some(cell(0.)), anchor, cursor),
Some((anchor, cursor))
);
let old = Some((point(px(50.), px(10.)), cell(0.)));
assert_eq!(
confine_to_cell(old, None, anchor, cursor),
Some((anchor, cursor))
);
}
#[test]
fn a_drag_from_a_cell_selects_nothing_outside_it() {
let anchor = point(px(10.), px(10.));
let anchor_cell = Some((anchor, cell(0.)));
let cursor = point(px(150.), px(20.));
assert_eq!(
confine_to_cell(anchor_cell, Some(cell(100.)), anchor, cursor),
None
);
assert_eq!(confine_to_cell(anchor_cell, None, anchor, cursor), None);
}
#[test]
fn a_drag_from_a_cell_is_clamped_to_it() {
let anchor = point(px(10.), px(10.));
let anchor_cell = Some((anchor, cell(0.)));
assert_eq!(
confine_to_cell(
anchor_cell,
Some(cell(0.)),
anchor,
point(px(250.), px(80.))
),
Some((anchor, point(px(100.), px(30.))))
);
assert_eq!(
confine_to_cell(
anchor_cell,
Some(cell(0.)),
anchor,
point(px(-20.), px(-5.))
),
Some((anchor, point(px(0.), px(0.))))
);
let inside = point(px(60.), px(12.));
assert_eq!(
confine_to_cell(anchor_cell, Some(cell(0.)), anchor, inside),
Some((anchor, inside))
);
}
}
#[gpui::test]
fn initialization_is_idempotent_and_suppression_can_be_reset(cx: &mut gpui::TestAppContext) {
cx.update(|cx| {
GlobalState::init(cx);
GlobalState::suppress_text_selection(cx);
GlobalState::init(cx);
assert!(GlobalState::is_text_selection_suppressed(cx));
GlobalState::reset_text_selection_suppression(cx);
assert!(!GlobalState::is_text_selection_suppressed(cx));
assert!(!GlobalState::is_in_deferred_context(cx));
let popover = GlobalState::register_deferred_popover(cx);
assert!(GlobalState::is_in_deferred_context(cx));
drop(popover);
assert!(!GlobalState::is_in_deferred_context(cx));
});
}
#[gpui::test]
fn a_dropped_registration_closes_the_deferred_context(cx: &mut gpui::TestAppContext) {
cx.update(|cx| {
GlobalState::init(cx);
let outer = GlobalState::register_deferred_popover(cx);
{
let _inner = GlobalState::register_deferred_popover(cx);
assert!(GlobalState::is_in_deferred_context(cx));
}
assert!(GlobalState::is_in_deferred_context(cx));
drop(outer);
assert!(!GlobalState::is_in_deferred_context(cx));
let popover = GlobalState::register_deferred_popover(cx);
assert_eq!(GlobalState::global(cx).deferred_popovers.len(), 1);
drop(popover);
assert!(!GlobalState::is_in_deferred_context(cx));
});
}
}