frust_core/app/session.rs
1//! Session state on [`RenderRoot`]: pointer capture, focus/IME, hover, cursor and clipboard
2//! accessors, plus the change-guarded writers that keep their edge generations honest.
3
4use std::rc::Weak;
5
6use super::{RenderRoot, advance_focus_epoch};
7use crate::event::{CursorIcon, ImeState, PointerId};
8use crate::overlay::{OverlayHit, OverlayInput, OverlayKey, OverlayKeys};
9use crate::view::View;
10
11/// Replace `slot` and bump `generation` only when the value actually moves: paint republishes the
12/// IME surface every frame, so an unconditional bump would fire the shell's edge every vsync.
13fn store_ime_state_in(slot: &mut Option<ImeState>, generation: &mut u64, next: Option<ImeState>) {
14 if *slot != next {
15 *slot = next;
16 *generation = generation.wrapping_add(1);
17 }
18}
19
20/// End the focus/IME session, the one primitive every release site goes through: clear
21/// `focus_active` and the IME surface and, if either was set, bump `generation` exactly once and
22/// advance and republish `focus_epoch`, stranding every focus link recorded against the session.
23pub(super) fn release_focus_session_in(
24 focus_active: &mut bool,
25 ime_slot: &mut Option<ImeState>,
26 generation: &mut u64,
27 focus_epoch: &mut u64,
28 root_identity: u64,
29) {
30 let moved = *focus_active || ime_slot.is_some();
31 *focus_active = false;
32 *ime_slot = None;
33 if moved {
34 *generation = generation.wrapping_add(1);
35 // Only on an actual release: a `Down` on already-blurred chrome must move nothing.
36 *focus_epoch = advance_focus_epoch(*focus_epoch);
37 crate::widget::set_live_focus_session(root_identity, *focus_epoch, *focus_epoch);
38 }
39}
40
41impl<State: 'static, V: View<State>> RenderRoot<State, V> {
42 /// Whether a captured pointer gesture is currently in flight.
43 pub fn is_pointer_captured(&self) -> bool {
44 self.capture_claimant.is_some()
45 }
46
47 /// The contact that claimed the capture in flight (only its `Up`/`Cancel` ends it), or `None`.
48 pub fn pointer_capture_claimant(&self) -> Option<PointerId> {
49 self.capture_claimant
50 }
51
52 /// Whether the capture in flight routes the gesture's other contacts to its captor.
53 pub fn pointer_capture_contacts(&self) -> bool {
54 self.capture_contacts
55 }
56
57 /// Whether some widget in the tree currently holds keyboard/IME focus.
58 pub fn is_focus_active(&self) -> bool {
59 self.focus_active
60 }
61
62 /// Whether the last hover pass left a widget holding the hover link; a rebuild removing the
63 /// claimant ends it. A touch drag raises it too (LIMITATIONS `hover-window-leave-standing`).
64 pub fn is_hover_active(&self) -> bool {
65 self.hover_active
66 }
67
68 /// The cursor the tree last asked the host to show: re-resolved on every pointer `Move`
69 /// ([`CursorIcon::Default`] when nothing asked) and standing through every other pass.
70 /// Residual: a widget that is torn down (or moves out from under a stationary pointer) while
71 /// its request stands leaves the last shape in place until the next `Move` re-resolves it.
72 pub fn cursor(&self) -> CursorIcon {
73 self.cursor
74 }
75
76 /// Take (and clear) the text a widget asked the shell to put on the host clipboard; drain after
77 /// every [`RenderRoot::event`]. Destructive: a write is an edge, so a dropped result is lost.
78 pub fn take_clipboard_write(&mut self) -> Option<String> {
79 self.pending_clipboard_write.take()
80 }
81
82 /// Take (and clear) whether a widget asked to read the host clipboard; the shell answers with a
83 /// new `EditCommand::Paste` dispatch, dropping an async answer once `focus_epoch` has moved.
84 pub fn take_paste_request(&mut self) -> bool {
85 std::mem::take(&mut self.pending_paste_request)
86 }
87
88 /// The IME surface the focused widget published, for the platform input method. `None` is the
89 /// only no-session form: an inactive publish is a release, so `is_some()` means a live session.
90 pub fn ime_state(&self) -> Option<ImeState> {
91 self.ime_state.clone()
92 }
93
94 /// Bumped on every actual change of `is_focus_active` or `ime_state` — the frame gate's
95 /// `focus_or_ime_changed` edge. It counts surface changes, not sessions (see `focus_epoch`).
96 pub fn focus_ime_generation(&self) -> u64 {
97 self.focus_ime_gen
98 }
99
100 /// The live focus session's identity. It advances once per honoured claim (an
101 /// `EventCtx::request_focus` or a traversal grant) and once per release, so a Tab step that
102 /// lands from a live session moves it by two (by one from none) and one that lands nowhere leaves it unchanged. It moves when focus
103 /// crosses fields that publish alike. Never `0`: a safe sentinel.
104 pub fn focus_epoch(&self) -> u64 {
105 self.focus_epoch
106 }
107
108 /// Set the focus flag, bumping the generation only on change (in practice it only sets).
109 pub(super) fn set_focus_active(&mut self, active: bool) {
110 if self.focus_active != active {
111 self.focus_active = active;
112 self.focus_ime_gen = self.focus_ime_gen.wrapping_add(1);
113 }
114 }
115
116 /// The `&mut self` form of [`store_ime_state_in`].
117 pub(super) fn store_ime_state(&mut self, ime: Option<ImeState>) {
118 store_ime_state_in(&mut self.ime_state, &mut self.focus_ime_gen, ime);
119 }
120
121 /// The `&mut self` form of [`release_focus_session_in`], also clearing the session's owner
122 /// surface, key forward, selection toolbar and keyboard-focus flag. Idempotent.
123 pub(super) fn release_focus_session(&mut self) {
124 self.set_keyboard_focus(false);
125 release_focus_session_in(
126 &mut self.focus_active,
127 &mut self.ime_state,
128 &mut self.focus_ime_gen,
129 &mut self.focus_epoch,
130 self.root_identity,
131 );
132 self.focus_surface = None;
133 self.key_forward = None;
134 // The toolbar belongs to the session: end it on the blurring pass, not a paint later.
135 if self.selection_toolbar.take().is_some() {
136 self.selection_toolbar_gen = self.selection_toolbar_gen.wrapping_add(1);
137 }
138 }
139
140 /// Modal focus trap: while an Interactive `OverlayKeys::Modal` pod is registered (a consuming
141 /// outside tap alone is a light-dismiss menu, not a trap), end a session no floated pod holds
142 /// (the main tree's), so it takes no more keys. Not restored when the modal closes (v1).
143 pub(super) fn trap_modal_focus(&mut self) {
144 let traps = |hit: &OverlayHit| {
145 hit.input == OverlayInput::Interactive && hit.keys == OverlayKeys::Modal
146 };
147 if !self.focus_active || !self.overlay_hits.iter().any(traps) {
148 return;
149 }
150 self.publish_focus_session();
151 let floated_holds = self
152 .overlay_pods
153 .iter()
154 .filter_map(Weak::upgrade)
155 .any(|pod| pod.try_borrow().map_or(true, |pod| pod.holds_live_focus()));
156 if !floated_holds {
157 self.release_focus_session();
158 }
159 }
160
161 /// Record a pass's forward request against the session live after it; none without one.
162 pub(super) fn record_key_forward(&mut self, requested: Option<OverlayKey>) {
163 if let Some(key) = requested.filter(|_| self.focus_active) {
164 self.key_forward = Some((key, self.focus_epoch));
165 }
166 }
167
168 /// The forward target, while the session it was recorded under is still the live one.
169 pub(super) fn live_key_forward(&self) -> Option<OverlayKey> {
170 self.key_forward
171 .filter(|&(_, epoch)| self.focus_active && epoch == self.focus_epoch)
172 .map(|(key, _)| key)
173 }
174
175 /// Publish this root's at-rest focus session for pods to compare stamps against; called at the
176 /// head of every pass, since the thread-local channel mirrors one root at a time.
177 pub(super) fn publish_focus_session(&self) {
178 crate::widget::set_live_focus_session(
179 self.root_identity,
180 self.focus_epoch,
181 self.focus_epoch,
182 );
183 }
184
185 /// End the standing hover link outside a hover pass: advance the epoch (stranding every stamp
186 /// at once) and clear the mirror.
187 pub(super) fn end_hover_link(&mut self) {
188 self.hover_epoch = self.hover_epoch.wrapping_add(1);
189 self.hover_active = false;
190 self.hover_link = None;
191 crate::event::set_live_hover_link(self.root_identity, 0);
192 }
193}