git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

src/window_state.rs (8.1K)

  1 //! A window's interaction state: what the user is doing in one window right now, apart from
  2 //! the widgets themselves (`docs/rfc-global-state.md`, phase 2).
  3 //!
  4 //! - the open context menu (`widget::context_menu`),
  5 //! - the hover highlight and the cursor it follows (`widget::hover_animation`),
  6 //! - the side swipe being recognized (`widget::side_swipe`),
  7 //! - the input method's composition and the caret it is drawn at (`ime`),
  8 //! - the phase of the wheel event being dispatched (`widget::scroll_motion`),
  9 //! - and the window's own properties ([`Props`]): its HiDPI scale, the display metric,
 10 //!   its app id, whether it is fullscreen or maximized, and vertical text (`scale`,
 11 //!   `units`, `backend::text`).
 12 //!
 13 //! Each lived in a thread-local of its own until 2026-10-08, so every window on a thread
 14 //! shared one menu, one highlight and one composition. Now a window OWNS a
 15 //! [`WindowState`], and its shell makes it the current one ([`enter`]) for as long as it
 16 //! runs that window's code; the modules' free functions — `context_menu::show`,
 17 //! `hover_animation::tick`, `ime::caret`, … — act on the current one, so the many apps
 18 //! and widgets that call them (an app need not have a `UiContext` to show a menu) are
 19 //! unchanged. With none entered — a test, a tool that draws no window — each thread has a
 20 //! default one, which is what the thread-locals were.
 21 //!
 22 //! **The window's properties read differently off a window.** A worker thread asking for the
 23 //! scale (a page rasterized at it, a tile decoded for it) has no window entered; it reads the
 24 //! process-wide value, which every window's setter also writes — the last one set. So a
 25 //! process with one window reads exactly what it did when these were only process-wide,
 26 //! and two windows on one thread each read their own (`docs/rfc-global-state.md`, phase 4).
 27 
 28 use std::cell::{Cell, RefCell};
 29 use std::rc::Rc;
 30 
 31 use crate::widget::context_menu::ContextMenuState;
 32 use crate::widget::hover_animation::HoverState;
 33 use crate::widget::side_swipe::SideSwipe;
 34 
 35 /// One window's interaction state (see the module docs).
 36 pub struct WindowState {
 37     pub(crate) context_menu: RefCell<ContextMenuState>,
 38     pub(crate) hover: RefCell<HoverState>,
 39     pub(crate) cursor: RefCell<(f32, f32)>,
 40     pub(crate) swipe: RefCell<SideSwipe>,
 41     pub(crate) ime: RefCell<crate::ime::State>,
 42     pub(crate) scroll_phase: Cell<crate::widget::ScrollPhase>,
 43     pub(crate) props: RefCell<Props>,
 44 }
 45 
 46 /// A window's own properties (see the module docs).
 47 #[derive(Debug, Clone)]
 48 pub struct Props {
 49     /// The HiDPI scale it is drawn at (`scale::scale_factor`).
 50     pub scale: f32,
 51     /// The display it is on, in logical px per mm (`units::metric`).
 52     pub metric: Option<crate::units::Metric>,
 53     pub app_id: String,
 54     pub fullscreen: bool,
 55     pub maximized: bool,
 56     /// Set while it stands on a screen edge as a vertical bar of this thickness
 57     /// (`backend::text::vertical_text`).
 58     pub vertical_text: Option<u32>,
 59 }
 60 
 61 impl Props {
 62     /// The process-wide values: what a window starts from.
 63     fn from_process() -> Props {
 64         Props {
 65             scale: crate::scale::process_scale_factor(),
 66             metric: None,
 67             app_id: crate::scale::process_app_id(),
 68             fullscreen: false,
 69             maximized: false,
 70             vertical_text: crate::backend::text::process_vertical_text(),
 71         }
 72     }
 73 }
 74 
 75 impl WindowState {
 76     /// A window's state with nothing open, hovered, swiped or composed.
 77     pub fn new() -> Rc<WindowState> {
 78         Rc::new(WindowState {
 79             context_menu: RefCell::new(ContextMenuState::new()),
 80             hover: RefCell::new(HoverState::new()),
 81             cursor: RefCell::new((0.0, 0.0)),
 82             swipe: RefCell::new(SideSwipe::new()),
 83             ime: RefCell::new(crate::ime::State::default()),
 84             scroll_phase: Cell::new(crate::widget::ScrollPhase::Wheel),
 85             props: RefCell::new(Props::from_process()),
 86         })
 87     }
 88 }
 89 
 90 thread_local! {
 91     static CURRENT: RefCell<Option<Rc<WindowState>>> = const { RefCell::new(None) };
 92     static DEFAULT: Rc<WindowState> = WindowState::new();
 93 }
 94 
 95 /// Run `f` on the current window's state: the one entered last on this thread, else the
 96 /// thread's default.
 97 pub fn with<R>(f: impl FnOnce(&WindowState) -> R) -> R {
 98     let state = CURRENT.with(|c| c.borrow().clone()).unwrap_or_else(|| DEFAULT.with(Rc::clone));
 99     f(&state)
100 }
101 
102 /// Run `f` on the window entered on this thread, if one is: `None` off any window's thread
103 /// (where a window's properties read as the process-wide values).
104 pub fn entered<R>(f: impl FnOnce(&WindowState) -> R) -> Option<R> {
105     let state = CURRENT.with(|c| c.borrow().clone())?;
106     Some(f(&state))
107 }
108 
109 /// The display metric of the window whose code is running: what `units::metric` answers
110 /// first ([`crate::units::set_metric_resolver`]).
111 fn window_metric() -> Option<crate::units::Metric> {
112     entered(|w| w.props.borrow().metric).flatten()
113 }
114 
115 /// The display metric changed: the current window's, if one is entered, and the
116 /// process-wide one (a forced PPI applied to both, `units::effective`).
117 pub fn set_metric(m: crate::units::Metric) {
118     entered(|w| w.props.borrow_mut().metric = Some(crate::units::effective(m)));
119     crate::units::set_metric(m);
120 }
121 
122 /// While this lives, `state` is the current window's (see [`enter`]).
123 #[must_use = "the window's state is current only while this guard lives"]
124 pub struct Entered {
125     previous: Option<Rc<WindowState>>,
126 }
127 
128 impl Drop for Entered {
129     fn drop(&mut self) {
130         let previous = self.previous.take();
131         CURRENT.with(|c| *c.borrow_mut() = previous);
132     }
133 }
134 
135 /// Make `state` the current window's until the guard drops (then the one before it is
136 /// again). A shell enters its window's state around everything it runs for that window.
137 pub fn enter(state: &Rc<WindowState>) -> Entered {
138     crate::units::set_metric_resolver(window_metric);
139     let previous = CURRENT.with(|c| c.replace(Some(Rc::clone(state))));
140     Entered { previous }
141 }
142 
143 #[cfg(test)]
144 mod tests {
145     use super::*;
146 
147     /// Two windows draw at their own scales; a worker thread, on no window, reads the last
148     /// one set; and the scroll phase is the window's.
149     #[test]
150     fn each_window_has_its_own_scale_and_a_worker_reads_the_last() {
151         let (a, b) = (WindowState::new(), WindowState::new());
152         {
153             let _in_a = enter(&a);
154             crate::scale::set_scale_factor(2.0);
155             crate::widget::scroll_motion::set_scroll_phase(crate::widget::ScrollPhase::Finger);
156         }
157         {
158             let _in_b = enter(&b);
159             crate::scale::set_scale_factor(1.5);
160             assert_eq!(crate::scale::scale_factor(), 1.5);
161             assert_eq!(crate::widget::scroll_motion::current_scroll_phase(), crate::widget::ScrollPhase::Wheel, "b's own phase");
162             let (seen, process) = std::thread::spawn(|| (crate::scale::scale_factor(), crate::scale::process_scale_factor())).join().unwrap();
163             assert_eq!(seen, process, "a worker, on no window, reads the process-wide scale");
164         }
165         let _in_a = enter(&a);
166         assert_eq!(crate::scale::scale_factor(), 2.0, "a keeps its own");
167         assert_eq!(crate::widget::scroll_motion::current_scroll_phase(), crate::widget::ScrollPhase::Finger);
168         crate::widget::scroll_motion::set_scroll_phase(crate::widget::ScrollPhase::Wheel);
169     }
170 
171     /// Two windows keep two menus: what one shows the other does not, and the one entered
172     /// before comes back when the guard drops.
173     #[test]
174     fn each_window_has_its_own_menu() {
175         use crate::widget::context_menu as cm;
176         let (a, b) = (WindowState::new(), WindowState::new());
177         let _in_a = enter(&a);
178         cm::show(10.0, 10.0, vec!["Copy".into()], 0, crate::widget::WidgetId(1));
179         assert!(cm::is_visible());
180         {
181             let _in_b = enter(&b);
182             assert!(!cm::is_visible(), "the other window has no menu open");
183             cm::show(20.0, 20.0, vec!["Paste".into(), "Cut".into()], 0, crate::widget::WidgetId(2));
184             assert_eq!(cm::options().len(), 2);
185         }
186         assert!(cm::is_visible());
187         assert_eq!(cm::options(), ["Copy"], "back in the first window, its own menu");
188         cm::hide();
189     }
190 }