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 }