Skip to main content

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}