Skip to main content

cranpose_ui/
render_state.rs

1#[cfg(test)]
2use std::sync::{Mutex, OnceLock};
3use std::{
4    cell::{Cell, RefCell},
5    rc::{Rc, Weak},
6    sync::Arc,
7};
8
9use cranpose_core::{
10    NodeId, SnapshotStateObserver, collections::map::HashSet, current_runtime_handle,
11};
12
13pub(crate) type ModifierChainTraceCallback =
14    dyn Fn(&[crate::modifier::ModifierChainInspectorNode]) + Send + Sync + 'static;
15
16struct RenderState {
17    layout_repasses: RefCell<DirtyNodeSet>,
18    measure_repasses: RefCell<DirtyNodeSet>,
19    draw_repasses: RefCell<DirtyNodeSet>,
20    layer_property_repasses: RefCell<DirtyNodeSet>,
21    modifier_slice_repasses: RefCell<DirtyNodeSet>,
22    geometry_scene_nodes: RefCell<DirtyNodeSet>,
23    moved_scene_nodes: RefCell<DirtyNodeSet>,
24    /// Nodes whose z index changed: their parents' layers hold the order.
25    z_order_changes: RefCell<DirtyNodeSet>,
26    render_invalidated: Cell<bool>,
27    pointer_invalidated: Cell<bool>,
28    focus_invalidated: Cell<bool>,
29    layout_invalidated: Cell<bool>,
30    density_bits: Cell<u32>,
31    font_scale: Cell<crate::font_scale::FontScaleCurve>,
32    /// Whether `font_scale` leaves every size as it is, read by every text
33    /// measured instead of the curve.
34    font_scale_identity: Cell<bool>,
35    /// `font_scale`'s fingerprint, computed when the curve is set.
36    font_scale_fingerprint: Cell<u32>,
37}
38
39#[doc(hidden)]
40pub struct AppContext {
41    id: AppContextId,
42    self_weak: RefCell<Weak<AppContext>>,
43    state: RenderState,
44    draw_observer: SnapshotStateObserver,
45    text: crate::text::measure::TextService,
46    layout_frame_arena: RefCell<crate::layout::FrameLayoutArena>,
47    /// The host the next layout pass lends its applier to; `None` while a
48    /// pass has it.
49    layout_host: RefCell<Option<crate::layout::LayoutHost>>,
50    layout_cache_epoch: Cell<u64>,
51    layout_cache_floor: Cell<u64>,
52    last_fling_velocity_bits: Cell<u32>,
53    scroll_motion_contexts: crate::scroll::ScrollMotionContextStore,
54    lazy_prefetch: crate::lazy_prefetch::LazyPrefetchState,
55    layout_node_registry: crate::widgets::nodes::layout_node::LayoutNodeRegistryState,
56    pointer_dispatch: crate::pointer_dispatch::PointerDispatchState,
57    mouse_input: crate::mouse_input::MouseInputQueue,
58    pointer_claim: Cell<Option<cranpose_core::NodeId>>,
59    focus_dispatch: crate::focus_dispatch::FocusInvalidationState,
60    modal: crate::modal::ModalState,
61    modal_nodes: crate::modal_nodes::ModalNodes,
62    semantics_layout: crate::semantics_layout_log::SemanticsLayoutLog,
63    hosted_popups: crate::widgets::popup::HostedPopupRegistries,
64    semantics_dispatch: crate::semantics_dispatch::SemanticsInvalidationState,
65    cursor_animation: crate::cursor_animation::CursorAnimationState,
66    text_field_focus: crate::text_field_focus::TextFieldFocusState,
67    text_input_session: crate::text_input_session::PlatformTextInputState,
68    clipboard_session: crate::clipboard_session::ClipboardSessionState,
69    pointer_icon: crate::pointer_icon_session::PointerIconState,
70    pointer_input_tasks: crate::modifier::pointer_input::PointerInputTaskRegistry,
71    modifier_chain_trace: RefCell<Option<Arc<ModifierChainTraceCallback>>>,
72    window_roots: crate::modifier::WindowRootRegistry,
73    drag_and_drop: crate::modifier::DragAndDropState,
74}
75
76/// Nonzero, so that an absent one takes no more room than one: every
77/// layout node keeps the id of the context it registered with.
78#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
79pub(crate) struct AppContextId(std::num::NonZeroU64);
80
81#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
82pub(crate) struct DrawObservationScope {
83    node_id: NodeId,
84    modifier_index: usize,
85    command_index: usize,
86    kind: DrawObservationKind,
87}
88
89#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
90enum DrawObservationKind {
91    Content,
92    LayerProperties,
93}
94
95impl DrawObservationScope {
96    pub(crate) fn new(node_id: NodeId, modifier_index: usize, command_index: usize) -> Self {
97        Self {
98            node_id,
99            modifier_index,
100            command_index,
101            kind: DrawObservationKind::Content,
102        }
103    }
104
105    pub(crate) fn layer_properties(node_id: NodeId, modifier_index: usize) -> Self {
106        Self {
107            node_id,
108            modifier_index,
109            command_index: 0,
110            kind: DrawObservationKind::LayerProperties,
111        }
112    }
113}
114
115fn new_draw_observer() -> SnapshotStateObserver {
116    let observer = SnapshotStateObserver::new(|callback| {
117        if let Some(runtime) = current_runtime_handle() {
118            runtime.enqueue_ui_task(callback);
119        } else {
120            callback();
121        }
122    });
123    observer.start();
124    observer
125}
126
127struct CurrentDrawNodeGuard {
128    previous: Option<NodeId>,
129}
130
131impl Drop for CurrentDrawNodeGuard {
132    fn drop(&mut self) {
133        crate::thread_state::cells(|cells| cells.current_draw_node.set(self.previous));
134    }
135}
136
137pub(crate) fn observe_draw_reads<R>(scope: DrawObservationScope, block: impl FnOnce() -> R) -> R {
138    let context = require_current_app_context("draw observer access");
139    let context_id = context.id;
140    let _guard = CurrentDrawNodeGuard {
141        previous: crate::thread_state::cells(|cells| {
142            cells.current_draw_node.replace(Some(scope.node_id))
143        }),
144    };
145    context.draw_observer.observe_reads(
146        scope,
147        move |scope| match scope.kind {
148            DrawObservationKind::Content => {
149                schedule_draw_repass_for_app_context(context_id, scope.node_id);
150            }
151            DrawObservationKind::LayerProperties => {
152                schedule_layer_property_repass_for_app_context(context_id, scope.node_id);
153            }
154        },
155        block,
156    )
157}
158
159/// The draw-phase animation contract: a draw closure that advances its own
160/// spring or clock (a Cell no observation can see) must schedule the next
161/// frame's re-record of ITS OWN node — a bare render invalidation only
162/// re-presents the retained scene, which would freeze the animation
163/// mid-flight. Callable only from inside a recording draw closure; anywhere
164/// else it degrades to a plain frame request.
165pub fn request_current_draw_redraw() {
166    if let Some(node_id) = crate::thread_state::cells(|cells| cells.current_draw_node.get()) {
167        schedule_draw_repass(node_id);
168    }
169    request_render_invalidation();
170}
171
172pub(crate) fn clear_draw_observations_for_node(node_id: NodeId) {
173    with_draw_observer(|observer| {
174        observer.clear_if(|scope| {
175            scope
176                .downcast_ref::<DrawObservationScope>()
177                .is_some_and(|scope| scope.node_id == node_id)
178        });
179    });
180}
181
182/// Removes draw observations whose owners are absent from the retained scene.
183pub fn prune_draw_observations_to_nodes(retained: &HashSet<NodeId>) {
184    with_draw_observer(|observer| {
185        observer.clear_if(|scope| {
186            scope
187                .downcast_ref::<DrawObservationScope>()
188                .is_some_and(|scope| !retained.contains(&scope.node_id))
189        });
190    });
191}
192
193impl RenderState {
194    fn new_with_density(density: f32) -> Self {
195        Self {
196            layout_repasses: RefCell::new(DirtyNodeSet::new()),
197            measure_repasses: RefCell::new(DirtyNodeSet::new()),
198            draw_repasses: RefCell::new(DirtyNodeSet::new()),
199            layer_property_repasses: RefCell::new(DirtyNodeSet::new()),
200            modifier_slice_repasses: RefCell::new(DirtyNodeSet::new()),
201            geometry_scene_nodes: RefCell::new(DirtyNodeSet::new()),
202            moved_scene_nodes: RefCell::new(DirtyNodeSet::new()),
203            z_order_changes: RefCell::new(DirtyNodeSet::new()),
204            render_invalidated: Cell::new(false),
205            pointer_invalidated: Cell::new(false),
206            focus_invalidated: Cell::new(false),
207            layout_invalidated: Cell::new(false),
208            density_bits: Cell::new(normalize_density(density).to_bits()),
209            font_scale: Cell::new(crate::font_scale::FontScaleCurve::linear(1.0)),
210            font_scale_identity: Cell::new(true),
211            font_scale_fingerprint: Cell::new(
212                crate::font_scale::FontScaleCurve::linear(1.0).fingerprint(),
213            ),
214        }
215    }
216}
217
218fn next_app_context_id() -> AppContextId {
219    crate::thread_state::cells(|cells| {
220        let next = &cells.next_app_context_id;
221        let id = next.get();
222        next.set(id.checked_add(1).unwrap_or(std::num::NonZeroU64::MIN));
223        AppContextId(id)
224    })
225}
226
227#[doc(hidden)]
228pub struct AppContextScope;
229
230impl Drop for AppContextScope {
231    fn drop(&mut self) {
232        crate::thread_state::try_with(|state| {
233            state.app_context_stack.borrow_mut().pop();
234        });
235    }
236}
237
238impl AppContext {
239    pub fn new() -> Rc<Self> {
240        Self::new_with_density(1.0)
241    }
242
243    pub fn new_with_density(density: f32) -> Rc<Self> {
244        let context = Rc::new(Self {
245            id: next_app_context_id(),
246            self_weak: RefCell::new(Weak::new()),
247            state: RenderState::new_with_density(density),
248            draw_observer: new_draw_observer(),
249            text: crate::text::measure::TextService::new(),
250            layout_frame_arena: RefCell::new(crate::layout::FrameLayoutArena::default()),
251            layout_host: RefCell::new(None),
252            layout_cache_epoch: Cell::new(1),
253            layout_cache_floor: Cell::new(0),
254            last_fling_velocity_bits: Cell::new(0.0f32.to_bits()),
255            scroll_motion_contexts: crate::scroll::ScrollMotionContextStore::new(),
256            lazy_prefetch: crate::lazy_prefetch::LazyPrefetchState::new(),
257            layout_node_registry: crate::widgets::nodes::layout_node::LayoutNodeRegistryState::new(
258            ),
259            pointer_dispatch: crate::pointer_dispatch::PointerDispatchState::new(),
260            mouse_input: crate::mouse_input::MouseInputQueue::default(),
261            pointer_claim: Cell::new(None),
262            focus_dispatch: crate::focus_dispatch::FocusInvalidationState::new(),
263            modal: crate::modal::ModalState::new(),
264            modal_nodes: crate::modal_nodes::ModalNodes::default(),
265            semantics_layout: crate::semantics_layout_log::SemanticsLayoutLog::default(),
266            hosted_popups: crate::widgets::popup::HostedPopupRegistries::default(),
267            semantics_dispatch: crate::semantics_dispatch::SemanticsInvalidationState::new(),
268            cursor_animation: crate::cursor_animation::CursorAnimationState::new(),
269            text_field_focus: crate::text_field_focus::TextFieldFocusState::new(),
270            text_input_session: crate::text_input_session::PlatformTextInputState::new(),
271            clipboard_session: crate::clipboard_session::ClipboardSessionState::new(),
272            pointer_icon: crate::pointer_icon_session::PointerIconState::new(),
273            pointer_input_tasks: crate::modifier::pointer_input::PointerInputTaskRegistry::new(),
274            modifier_chain_trace: RefCell::new(None),
275            window_roots: crate::modifier::WindowRootRegistry::default(),
276            drag_and_drop: crate::modifier::DragAndDropState::default(),
277        });
278        *context.self_weak.borrow_mut() = Rc::downgrade(&context);
279        crate::thread_state::with(|state| {
280            state
281                .app_contexts
282                .borrow_mut()
283                .insert(context.id, Rc::downgrade(&context));
284        });
285        context
286    }
287
288    pub fn enter<R>(self: &Rc<Self>, block: impl FnOnce() -> R) -> R {
289        let _scope = self.enter_scope();
290        block()
291    }
292
293    pub(crate) fn id(&self) -> AppContextId {
294        self.id
295    }
296
297    pub(crate) fn mouse_input(&self) -> &crate::mouse_input::MouseInputQueue {
298        &self.mouse_input
299    }
300
301    /// Number of coalesced mouse movements waiting for host dispatch.
302    #[doc(hidden)]
303    pub fn pending_mouse_moves(&self) -> usize {
304        self.mouse_input.len()
305    }
306
307    /// Takes the oldest queued mouse movement, retaining the queue's storage.
308    #[doc(hidden)]
309    pub fn take_mouse_move(&self) -> Option<crate::mouse_input::MouseMoveRequest> {
310        self.mouse_input.take()
311    }
312
313    /// Asks the shell to give the rest of the held press to `node` alone: the
314    /// other targets under the press see it cancelled.
315    pub(crate) fn claim_pointer(&self, node: cranpose_core::NodeId) {
316        self.pointer_claim.set(Some(node));
317    }
318
319    /// Takes the node that claimed the held press since the last call.
320    #[doc(hidden)]
321    pub fn take_pointer_claim(&self) -> Option<cranpose_core::NodeId> {
322        self.pointer_claim.take()
323    }
324
325    /// The window roots attached in this context.
326    pub fn window_roots(&self) -> &crate::modifier::WindowRootRegistry {
327        &self.window_roots
328    }
329
330    /// The drag and drop transfer and targets of this context.
331    pub fn drag_and_drop(&self) -> &crate::modifier::DragAndDropState {
332        &self.drag_and_drop
333    }
334
335    #[doc(hidden)]
336    pub fn enter_scope(self: &Rc<Self>) -> AppContextScope {
337        crate::thread_state::with(|state| {
338            state
339                .app_context_stack
340                .borrow_mut()
341                .push(Rc::downgrade(self));
342        });
343        AppContextScope
344    }
345
346    pub fn set_text_measurer<M: crate::text::TextMeasurer>(&self, measurer: M) {
347        self.set_text_measurer_rc(Rc::new(measurer));
348    }
349
350    pub fn set_text_measurer_rc(&self, measurer: Rc<dyn crate::text::TextMeasurer>) {
351        self.text.set_measurer(measurer);
352        self.invalidate_layout_caches();
353        self.state.layout_invalidated.set(true);
354        self.state.render_invalidated.set(true);
355    }
356
357    fn invalidate_layout_caches(&self) {
358        let floor = self.layout_cache_epoch.get() + 1;
359        self.layout_cache_epoch.set(floor);
360        self.layout_cache_floor.set(floor);
361    }
362
363    #[doc(hidden)]
364    pub fn downgrade(&self) -> Weak<Self> {
365        self.self_weak.borrow().clone()
366    }
367}
368
369impl Drop for AppContext {
370    fn drop(&mut self) {
371        let id = self.id;
372        crate::thread_state::try_with(|state| {
373            state.app_contexts.borrow_mut().remove(&id);
374        });
375    }
376}
377
378fn app_context_by_id(id: AppContextId) -> Option<Rc<AppContext>> {
379    crate::thread_state::try_with(|state| {
380        let contexts = &state.app_contexts;
381        let context = contexts.borrow().get(&id).cloned()?;
382        let Some(context) = context.upgrade() else {
383            contexts.borrow_mut().remove(&id);
384            return None;
385        };
386        Some(context)
387    })
388    .flatten()
389}
390
391#[cfg(test)]
392fn app_context_registry_entry_count() -> usize {
393    crate::thread_state::try_with(|state| state.app_contexts.borrow().len()).unwrap_or_default()
394}
395
396pub(crate) fn with_app_context_by_id<R>(
397    id: AppContextId,
398    f: impl FnOnce(&Rc<AppContext>) -> R,
399) -> Option<R> {
400    app_context_by_id(id).map(|context| f(&context))
401}
402
403pub(crate) fn current_app_context_id() -> AppContextId {
404    require_current_app_context("app context identity access").id
405}
406
407pub(crate) fn with_layout_node_registry_by_app_context<R>(
408    id: AppContextId,
409    f: impl FnOnce(&crate::widgets::nodes::layout_node::LayoutNodeRegistryState) -> R,
410) -> Option<R> {
411    with_app_context_by_id(id, |context| f(&context.layout_node_registry))
412}
413
414pub(crate) fn enter_app_context_by_id<R>(id: AppContextId, f: impl FnOnce() -> R) -> Option<R> {
415    with_app_context_by_id(id, |context| context.enter(f))
416}
417
418pub(crate) fn current_app_context() -> Option<Rc<AppContext>> {
419    crate::thread_state::try_with(|state| {
420        let mut stack = state.app_context_stack.borrow_mut();
421        loop {
422            let context = stack.last()?;
423            if let Some(context) = context.upgrade() {
424                return Some(context);
425            }
426            stack.pop();
427        }
428    })
429    .flatten()
430}
431
432#[doc(hidden)]
433pub fn has_current_app_context() -> bool {
434    current_app_context().is_some()
435}
436
437pub(crate) fn require_current_app_context(operation: &str) -> Rc<AppContext> {
438    if let Some(context) = current_app_context() {
439        return context;
440    }
441    require_current_app_context_without_scope(operation)
442}
443
444fn require_current_app_context_without_scope(operation: &str) -> Rc<AppContext> {
445    panic!("{operation} requires an active AppContext")
446}
447
448fn with_render_state<R>(f: impl FnOnce(&RenderState) -> R) -> R {
449    let context = require_current_app_context("render state access");
450    f(&context.state)
451}
452
453fn normalize_density(density: f32) -> f32 {
454    if density.is_finite() && density > 0.0 {
455        density
456    } else {
457        1.0
458    }
459}
460
461fn normalize_font_scale(scale: f32) -> f32 {
462    if scale.is_finite() && scale > 0.0 {
463        scale.clamp(MIN_FONT_SCALE, MAX_FONT_SCALE)
464    } else {
465        1.0
466    }
467}
468
469/// Smallest system font scale honoured; below this, text stops being readable
470/// as text.
471pub const MIN_FONT_SCALE: f32 = 0.5;
472/// Largest system font scale honoured. Android's own accessibility slider tops
473/// out at 2.0.
474pub const MAX_FONT_SCALE: f32 = 3.0;
475
476pub(crate) fn with_text_measurer<R>(f: impl FnOnce(&dyn crate::text::TextMeasurer) -> R) -> R {
477    let context = require_current_app_context("text measurer access");
478    context.text.with_measurer(f)
479}
480
481/// Tells the current app context's text service, and its measurer, a
482/// layout pass starts. Outside an app context there is neither to tell.
483#[cfg(test)]
484pub(crate) fn begin_text_layout_pass() {
485    if let Some(context) = current_app_context() {
486        context.begin_text_layout_pass();
487    }
488}
489
490pub(crate) fn with_text_service<R>(f: impl FnOnce(&crate::text::measure::TextService) -> R) -> R {
491    let context = require_current_app_context("text service access");
492    f(&context.text)
493}
494
495pub(crate) fn set_current_text_measurer(measurer: Rc<dyn crate::text::TextMeasurer>) {
496    let Some(context) = current_app_context() else {
497        panic!("set_text_measurer requires an active AppContext");
498    };
499    context.set_text_measurer_rc(measurer);
500}
501
502pub(crate) fn set_modifier_chain_trace(callback: Arc<ModifierChainTraceCallback>) -> AppContextId {
503    let context = require_current_app_context("modifier chain trace installation");
504    *context.modifier_chain_trace.borrow_mut() = Some(callback);
505    context.id
506}
507
508pub(crate) fn clear_modifier_chain_trace(context_id: AppContextId) {
509    let _ = with_app_context_by_id(context_id, |context| {
510        *context.modifier_chain_trace.borrow_mut() = None;
511    });
512}
513
514pub(crate) fn emit_modifier_chain_trace(nodes: &[crate::modifier::ModifierChainInspectorNode]) {
515    let Some(context) = current_app_context() else {
516        return;
517    };
518    let callback = context.modifier_chain_trace.borrow().clone();
519    if let Some(callback) = callback {
520        callback(nodes);
521    }
522}
523
524/// What a layout pass takes from its app context. A pass looks the context
525/// up once and asks it directly: each lookup reads a thread-local, which on
526/// ESP-IDF goes through a pthread key.
527impl AppContext {
528    /// Tells the text service, and its measurer, a layout pass starts.
529    pub(crate) fn begin_text_layout_pass(&self) {
530        self.text.begin_layout_pass();
531    }
532
533    pub(crate) fn take_layout_frame_arena(&self) -> crate::layout::FrameLayoutArena {
534        std::mem::take(&mut *self.layout_frame_arena.borrow_mut())
535    }
536
537    pub(crate) fn replace_layout_frame_arena(&self, arena: crate::layout::FrameLayoutArena) {
538        *self.layout_frame_arena.borrow_mut() = arena;
539    }
540
541    /// The host for a layout pass: the one the last pass gave back, or a new
542    /// one when a pass is still running or none ran yet.
543    pub(crate) fn take_layout_host(&self) -> crate::layout::LayoutHost {
544        self.layout_host.borrow_mut().take().unwrap_or_default()
545    }
546
547    pub(crate) fn replace_layout_host(&self, host: crate::layout::LayoutHost) {
548        *self.layout_host.borrow_mut() = Some(host);
549    }
550
551    pub(crate) fn next_layout_cache_epoch(&self) -> u64 {
552        let epoch = self.layout_cache_epoch.get();
553        self.layout_cache_epoch.set(epoch + 1);
554        epoch
555    }
556
557    pub(crate) fn current_layout_cache_epoch(&self) -> u64 {
558        self.layout_cache_epoch.get()
559    }
560
561    pub(crate) fn take_modifier_slice_repass_nodes(&self) -> Vec<NodeId> {
562        self.state
563            .modifier_slice_repasses
564            .borrow_mut()
565            .take_dirty_nodes()
566    }
567
568    pub(crate) fn take_measure_repass_nodes(&self) -> Vec<NodeId> {
569        self.state.measure_repasses.borrow_mut().take_dirty_nodes()
570    }
571
572    pub(crate) fn take_layout_repass_nodes(&self) -> Vec<NodeId> {
573        self.state.layout_repasses.borrow_mut().take_dirty_nodes()
574    }
575}
576
577pub(crate) fn invalidate_layout_cache_epoch() {
578    require_current_app_context("layout cache epoch access").invalidate_layout_caches();
579}
580
581pub(crate) fn current_layout_cache_epoch() -> u64 {
582    require_current_app_context("layout cache epoch access").current_layout_cache_epoch()
583}
584
585pub(crate) fn layout_cache_floor() -> u64 {
586    let context = require_current_app_context("layout cache epoch access");
587    context.layout_cache_floor.get()
588}
589
590pub(crate) fn record_last_fling_velocity(velocity: f32) {
591    if let Some(context) = current_app_context() {
592        context.last_fling_velocity_bits.set(velocity.to_bits());
593    }
594}
595
596#[doc(hidden)]
597pub fn debug_last_fling_velocity() -> f32 {
598    let context = require_current_app_context("fling velocity diagnostics access");
599    f32::from_bits(context.last_fling_velocity_bits.get())
600}
601
602#[doc(hidden)]
603pub fn debug_reset_last_fling_velocity() {
604    let context = require_current_app_context("fling velocity diagnostics access");
605    context.last_fling_velocity_bits.set(0.0f32.to_bits());
606}
607
608impl AppContext {
609    pub(crate) fn lazy_prefetch(&self) -> &crate::lazy_prefetch::LazyPrefetchState {
610        &self.lazy_prefetch
611    }
612
613    /// Whether the next frame has work in this context: a render, pointer,
614    /// focus or layout invalidation, or a layout, measure or layer-property
615    /// repass. Reads the context directly, without looking up the current
616    /// one.
617    pub fn has_pending_frame_work(&self) -> bool {
618        let state = &self.state;
619        state.render_invalidated.get()
620            || state.pointer_invalidated.get()
621            || state.focus_invalidated.get()
622            || state.layout_invalidated.get()
623            || state.layout_repasses.borrow_mut().has_pending_repass()
624            || state.measure_repasses.borrow_mut().has_pending_repass()
625            || state
626                .layer_property_repasses
627                .borrow_mut()
628                .has_pending_repass()
629    }
630
631    /// [`Self::has_pending_frame_work`], or a pending draw or pointer repass
632    /// or focus invalidation.
633    pub fn has_stale_work(&self) -> bool {
634        self.has_pending_frame_work()
635            || self.state.draw_repasses.borrow_mut().has_pending_repass()
636            || self.pointer_dispatch.has_pending_repass()
637            || self.focus_dispatch.has_pending_invalidation()
638    }
639}
640
641pub(crate) fn with_scroll_motion_context_store<R>(
642    f: impl FnOnce(&crate::scroll::ScrollMotionContextStore) -> R,
643) -> R {
644    let context = require_current_app_context("scroll motion context access");
645    f(&context.scroll_motion_contexts)
646}
647
648#[doc(hidden)]
649pub fn clear_transient_scroll_motion_contexts() {
650    let Some(context) = current_app_context() else {
651        return;
652    };
653    context.scroll_motion_contexts.clear_transient_after_frame();
654}
655
656#[cfg(test)]
657pub(crate) fn layout_frame_arena_placement_scratch_count() -> usize {
658    let context = require_current_app_context("layout frame arena access");
659
660    context
661        .layout_frame_arena
662        .borrow()
663        .available_placement_scratch_count()
664}
665
666/// Runs `f` on the current app context's layout node registry and returns
667/// that context's id.
668pub(crate) fn with_current_layout_node_registry(
669    f: impl FnOnce(&crate::widgets::nodes::layout_node::LayoutNodeRegistryState),
670) -> AppContextId {
671    let context = require_current_app_context("layout node registry access");
672    f(&context.layout_node_registry);
673    context.id
674}
675
676pub(crate) fn with_layout_node_registry<R>(
677    f: impl FnOnce(&crate::widgets::nodes::layout_node::LayoutNodeRegistryState) -> R,
678) -> R {
679    let context = require_current_app_context("layout node registry access");
680    f(&context.layout_node_registry)
681}
682
683pub(crate) fn with_pointer_dispatch<R>(
684    f: impl FnOnce(&crate::pointer_dispatch::PointerDispatchState) -> R,
685) -> R {
686    let context = require_current_app_context("pointer dispatch access");
687    f(&context.pointer_dispatch)
688}
689
690pub(crate) fn with_focus_dispatch<R>(
691    f: impl FnOnce(&crate::focus_dispatch::FocusInvalidationState) -> R,
692) -> R {
693    let context = require_current_app_context("focus dispatch access");
694    f(&context.focus_dispatch)
695}
696
697pub(crate) fn with_focus_dispatch_by_app_context<R>(
698    id: AppContextId,
699    f: impl FnOnce(&crate::focus_dispatch::FocusInvalidationState) -> R,
700) -> Option<R> {
701    with_app_context_by_id(id, |context| context.enter(|| f(&context.focus_dispatch)))
702}
703
704pub(crate) fn with_modal_state<R>(f: impl FnOnce(&crate::modal::ModalState) -> R) -> R {
705    let context = require_current_app_context("modal state access");
706    f(&context.modal)
707}
708
709pub(crate) fn with_current_modal_nodes<R>(
710    f: impl FnOnce(&crate::modal_nodes::ModalNodes) -> R,
711) -> Option<R> {
712    current_app_context().map(|context| f(&context.modal_nodes))
713}
714
715pub(crate) fn with_current_semantics_layout_log<R>(
716    f: impl FnOnce(&crate::semantics_layout_log::SemanticsLayoutLog) -> R,
717) -> Option<R> {
718    current_app_context().map(|context| f(&context.semantics_layout))
719}
720
721pub(crate) fn with_hosted_popup_registries<R>(
722    f: impl FnOnce(&crate::widgets::popup::HostedPopupRegistries) -> R,
723) -> R {
724    let context = require_current_app_context("hosted popup registries access");
725    f(&context.hosted_popups)
726}
727
728pub(crate) fn with_semantics_dispatch<R>(
729    f: impl FnOnce(&crate::semantics_dispatch::SemanticsInvalidationState) -> R,
730) -> R {
731    let context = require_current_app_context("semantics dispatch access");
732    f(&context.semantics_dispatch)
733}
734
735pub(crate) fn with_semantics_dispatch_by_app_context(
736    id: AppContextId,
737    f: impl FnOnce(&crate::semantics_dispatch::SemanticsInvalidationState),
738) {
739    with_app_context_by_id(id, |context| f(&context.semantics_dispatch));
740}
741
742pub(crate) fn current_app_context_id_opt() -> Option<AppContextId> {
743    current_app_context().map(|context| context.id)
744}
745
746pub(crate) fn with_cursor_animation<R>(
747    f: impl FnOnce(&crate::cursor_animation::CursorAnimationState) -> R,
748) -> R {
749    let context = require_current_app_context("cursor animation access");
750    f(&context.cursor_animation)
751}
752
753pub(crate) fn with_text_field_focus<R>(
754    f: impl FnOnce(&crate::text_field_focus::TextFieldFocusState) -> R,
755) -> R {
756    let context = require_current_app_context("text field focus access");
757    f(&context.text_field_focus)
758}
759
760pub(crate) fn with_text_input_session<R>(
761    f: impl FnOnce(&crate::text_input_session::PlatformTextInputState) -> R,
762) -> R {
763    let context = require_current_app_context("platform text input session access");
764    f(&context.text_input_session)
765}
766
767pub(crate) fn with_clipboard_session<R>(
768    f: impl FnOnce(&crate::clipboard_session::ClipboardSessionState) -> R,
769) -> R {
770    let context = require_current_app_context("clipboard session access");
771    f(&context.clipboard_session)
772}
773
774pub(crate) fn with_pointer_icon_session<R>(
775    f: impl FnOnce(&crate::pointer_icon_session::PointerIconState) -> R,
776) -> R {
777    let context = require_current_app_context("pointer icon session access");
778    f(&context.pointer_icon)
779}
780
781pub(crate) fn register_pointer_input_task(
782    task_id: u64,
783    task: Rc<crate::modifier::pointer_input::PointerInputTaskInner>,
784) -> crate::modifier::pointer_input::PointerInputTaskOwner {
785    let context = require_current_app_context("pointer input task registration");
786    context.pointer_input_tasks.insert(task_id, task);
787    crate::modifier::pointer_input::PointerInputTaskOwner::App(context.id)
788}
789
790pub(crate) fn remove_pointer_input_task(
791    owner: crate::modifier::pointer_input::PointerInputTaskOwner,
792    task_id: u64,
793) {
794    match owner {
795        crate::modifier::pointer_input::PointerInputTaskOwner::App(context_id) => {
796            let _ = with_app_context_by_id(context_id, |context| {
797                context.pointer_input_tasks.remove(task_id);
798            });
799        }
800    }
801}
802
803pub(crate) fn request_pointer_input_task_poll(
804    owner: crate::modifier::pointer_input::PointerInputTaskOwner,
805    task_id: u64,
806) {
807    match owner {
808        crate::modifier::pointer_input::PointerInputTaskOwner::App(context_id) => {
809            let _ = with_app_context_by_id(context_id, |context| {
810                context.enter(|| {
811                    context.pointer_input_tasks.request_poll(task_id, owner);
812                });
813            });
814        }
815    }
816}
817
818fn with_draw_observer<R>(f: impl FnOnce(&SnapshotStateObserver) -> R) -> R {
819    let context = require_current_app_context("draw observer access");
820    f(&context.draw_observer)
821}
822
823struct DirtyNodeSet {
824    dirty_nodes: HashSet<NodeId>,
825}
826
827impl DirtyNodeSet {
828    fn new() -> Self {
829        Self {
830            dirty_nodes: HashSet::default(),
831        }
832    }
833
834    fn schedule_repass(&mut self, node_id: NodeId) {
835        self.dirty_nodes.insert(node_id);
836    }
837
838    fn has_pending_repass(&self) -> bool {
839        !self.dirty_nodes.is_empty()
840    }
841
842    fn take_dirty_nodes(&mut self) -> Vec<NodeId> {
843        self.dirty_nodes.drain().collect()
844    }
845
846    fn take_dirty_nodes_into(&mut self, output: &mut Vec<NodeId>) {
847        output.clear();
848        output.reserve(self.dirty_nodes.len());
849        output.extend(self.dirty_nodes.drain());
850    }
851
852    fn dirty_nodes_snapshot(&self) -> Vec<NodeId> {
853        let mut nodes = self.dirty_nodes.iter().copied().collect::<Vec<_>>();
854        nodes.sort_unstable();
855        nodes
856    }
857}
858
859/// Schedules a layout repass for a specific node.
860///
861/// **This is the preferred way to invalidate layout for local changes** (e.g., scroll, single-node mutations).
862///
863/// The app shell will call `take_layout_repass_nodes()` and bubble dirty flags up the tree
864/// via `bubble_layout_dirty`. This gives you **O(subtree) performance** - only the affected
865/// subtree is remeasured, and layout caches for other parts of the app remain valid.
866///
867/// A scoped repass never invalidates the whole tree, and it never cancels a global
868/// invalidation requested for the same frame.
869///
870/// # For Global Invalidation
871///
872/// For rare global events (window resize, global scale changes), use `request_layout_invalidation()` instead.
873#[track_caller]
874pub fn schedule_layout_repass(node_id: NodeId) {
875    if layout_repass_schedule_diagnostics_enabled_for(node_id) {
876        let caller = std::panic::Location::caller();
877        log::warn!(
878            "[layout-repass-schedule] node={} caller={}:{}:{}",
879            node_id,
880            caller.file(),
881            caller.line(),
882            caller.column()
883        );
884    }
885    with_render_state(|state| {
886        state.layout_repasses.borrow_mut().schedule_repass(node_id);
887    });
888    request_render_invalidation();
889}
890
891#[derive(Clone, Copy)]
892enum LayoutRepassScheduleDiag {
893    Disabled,
894    All,
895    Node(NodeId),
896}
897
898fn layout_repass_schedule_diagnostics_enabled_for(node_id: NodeId) -> bool {
899    static MODE: std::sync::OnceLock<LayoutRepassScheduleDiag> = std::sync::OnceLock::new();
900    match *MODE.get_or_init(|| {
901        let Some(value) = std::env::var_os("CRANPOSE_LAYOUT_REPASS_SCHEDULE_DIAG") else {
902            return LayoutRepassScheduleDiag::Disabled;
903        };
904        if value == "all" {
905            return LayoutRepassScheduleDiag::All;
906        }
907        value.to_string_lossy().parse::<NodeId>().map_or(
908            LayoutRepassScheduleDiag::Disabled,
909            LayoutRepassScheduleDiag::Node,
910        )
911    }) {
912        LayoutRepassScheduleDiag::Disabled => false,
913        LayoutRepassScheduleDiag::All => true,
914        LayoutRepassScheduleDiag::Node(target) => target == node_id,
915    }
916}
917
918pub(crate) fn schedule_modifier_slices_repass(node_id: NodeId) {
919    with_render_state(|state| {
920        state
921            .modifier_slice_repasses
922            .borrow_mut()
923            .schedule_repass(node_id);
924    });
925    schedule_draw_repass(node_id);
926}
927
928/// Schedules a draw-only repass for a specific node.
929///
930/// This ensures draw/pointer data stays in sync when modifier updates do not
931/// require a layout pass (e.g., draw-only modifier changes).
932pub fn schedule_draw_repass(node_id: NodeId) {
933    let context = require_current_app_context("render state access");
934    schedule_draw_repass_in_context(&context, node_id);
935}
936
937fn schedule_draw_repass_for_app_context(context_id: AppContextId, node_id: NodeId) {
938    let _ = with_app_context_by_id(context_id, |context| {
939        schedule_draw_repass_in_context(context, node_id);
940    });
941}
942
943fn schedule_draw_repass_in_context(context: &AppContext, node_id: NodeId) {
944    context
945        .state
946        .draw_repasses
947        .borrow_mut()
948        .schedule_repass(node_id);
949    context.state.render_invalidated.set(true);
950}
951
952fn schedule_layer_property_repass_for_app_context(context_id: AppContextId, node_id: NodeId) {
953    let _ = with_app_context_by_id(context_id, |context| {
954        schedule_layer_property_repass_in_context(context, node_id);
955    });
956}
957
958fn schedule_layer_property_repass_in_context(context: &AppContext, node_id: NodeId) {
959    context
960        .state
961        .layer_property_repasses
962        .borrow_mut()
963        .schedule_repass(node_id);
964    context.state.render_invalidated.set(true);
965}
966
967/// Returns true if any draw repasses are pending.
968pub fn has_pending_draw_repasses() -> bool {
969    with_render_state(|state| state.draw_repasses.borrow_mut().has_pending_repass())
970}
971
972/// Takes all pending draw repass node IDs.
973pub fn take_draw_repass_nodes() -> Vec<NodeId> {
974    with_render_state(|state| state.draw_repasses.borrow_mut().take_dirty_nodes())
975}
976
977/// Takes pending draw repasses into reusable caller-owned storage.
978///
979/// ```
980/// let context = cranpose_ui::AppContext::new();
981/// context.enter(|| {
982///     let mut nodes = Vec::new();
983///     cranpose_ui::take_draw_repass_nodes_into(&mut nodes);
984///     assert!(nodes.is_empty());
985/// });
986/// ```
987pub fn take_draw_repass_nodes_into(output: &mut Vec<NodeId>) {
988    with_render_state(|state| {
989        state
990            .draw_repasses
991            .borrow_mut()
992            .take_dirty_nodes_into(output);
993    });
994}
995
996/// Records that `node_id`'s z index changed. The frame rebuilds the layer of
997/// its parent, which holds the order the parent's children draw and take
998/// taps in.
999pub fn schedule_z_order_change(node_id: NodeId) {
1000    with_render_state(|state| {
1001        state.z_order_changes.borrow_mut().schedule_repass(node_id);
1002        state.render_invalidated.set(true);
1003    });
1004}
1005
1006/// Appends the nodes whose z index changed since the last call to `output`,
1007/// after what it already holds.
1008pub fn append_z_order_changes(output: &mut Vec<NodeId>) {
1009    with_render_state(|state| {
1010        output.extend(state.z_order_changes.borrow_mut().dirty_nodes.drain());
1011    });
1012}
1013
1014/// Returns true if graphics-layer property updates are pending.
1015#[doc(hidden)]
1016pub fn has_pending_layer_property_repasses() -> bool {
1017    with_render_state(|state| {
1018        state
1019            .layer_property_repasses
1020            .borrow_mut()
1021            .has_pending_repass()
1022    })
1023}
1024
1025/// Takes pending graphics-layer property updates into reusable storage.
1026#[doc(hidden)]
1027pub fn take_layer_property_repass_nodes_into(output: &mut Vec<NodeId>) {
1028    with_render_state(|state| {
1029        state
1030            .layer_property_repasses
1031            .borrow_mut()
1032            .take_dirty_nodes_into(output);
1033    });
1034}
1035
1036/// Returns true if any layout repasses are pending.
1037pub fn has_pending_layout_repasses() -> bool {
1038    with_render_state(|state| state.layout_repasses.borrow_mut().has_pending_repass())
1039}
1040
1041/// Returns a stable snapshot of pending layout repass node IDs without consuming them.
1042pub fn pending_layout_repass_nodes_snapshot() -> Vec<NodeId> {
1043    with_render_state(|state| state.layout_repasses.borrow_mut().dirty_nodes_snapshot())
1044}
1045
1046/// Takes all pending layout repass node IDs.
1047///
1048/// The caller should iterate over these and call `bubble_layout_dirty` for each.
1049pub fn take_layout_repass_nodes() -> Vec<NodeId> {
1050    require_current_app_context("render state access").take_layout_repass_nodes()
1051}
1052
1053/// Schedules a scoped re-*measure* of `node_id` on the next frame.
1054///
1055/// Like [`schedule_layout_repass`], but processing bubbles *measure* dirtiness
1056/// (not just layout/placement) up the tree, so the node and its ancestors are
1057/// re-measured. Use this when a node's own measured size changes off a frame
1058/// callback (e.g. a row collapsing after a swipe dismiss): a plain layout
1059/// repass would leave the node's `needs_measure` flag unset, and an enclosing
1060/// `LazyColumn` would reuse its cached, full-height item slot.
1061pub fn schedule_measure_repass(node_id: NodeId) {
1062    with_render_state(|state| {
1063        state.measure_repasses.borrow_mut().schedule_repass(node_id);
1064    });
1065    request_render_invalidation();
1066}
1067
1068/// Returns true if any measure repasses are pending.
1069pub fn has_pending_measure_repasses() -> bool {
1070    with_render_state(|state| state.measure_repasses.borrow_mut().has_pending_repass())
1071}
1072
1073/// Returns a stable snapshot of pending measure repass node IDs without
1074/// consuming them.
1075///
1076/// The layout pass takes these ids to bubble measure dirtiness; the scene phase
1077/// needs the same ids *before* that happens, to scope its graph update to the
1078/// subtree that moved. Without the snapshot a measure repass reaches the scene
1079/// phase as "something changed, but nothing says where", which is
1080/// indistinguishable from a full invalidation.
1081pub fn pending_measure_repass_nodes_snapshot() -> Vec<NodeId> {
1082    with_render_state(|state| state.measure_repasses.borrow_mut().dirty_nodes_snapshot())
1083}
1084
1085/// Takes all pending measure repass node IDs.
1086///
1087/// The caller should iterate over these and call `bubble_measure_dirty` for each.
1088pub fn take_measure_repass_nodes() -> Vec<NodeId> {
1089    require_current_app_context("render state access").take_measure_repass_nodes()
1090}
1091
1092#[cfg(any(test, feature = "test-helpers"))]
1093pub(crate) fn take_modifier_slice_repass_nodes() -> Vec<NodeId> {
1094    require_current_app_context("render state access").take_modifier_slice_repass_nodes()
1095}
1096
1097pub(crate) fn record_geometry_scene_node(node_id: NodeId) {
1098    with_render_state(|state| {
1099        state
1100            .geometry_scene_nodes
1101            .borrow_mut()
1102            .schedule_repass(node_id);
1103    });
1104}
1105
1106pub(crate) fn record_moved_scene_node(node_id: NodeId) {
1107    with_render_state(|state| {
1108        state
1109            .moved_scene_nodes
1110            .borrow_mut()
1111            .schedule_repass(node_id);
1112    });
1113}
1114
1115/// The nodes whose geometry a layout pass changed, by what the scene redoes
1116/// for them.
1117#[derive(Clone, Debug, Default, PartialEq, Eq)]
1118pub struct GeometrySceneNodes {
1119    /// Nodes that changed size or were placed anew: the scene draws them
1120    /// again.
1121    pub reshaped: Vec<NodeId>,
1122    /// Nodes that moved in their parent: the scene moves what it drew for
1123    /// them, unless they are also reshaped or drawn again.
1124    pub moved: Vec<NodeId>,
1125}
1126
1127impl GeometrySceneNodes {
1128    /// Whether the pass changed no node's geometry.
1129    pub fn is_empty(&self) -> bool {
1130        self.reshaped.is_empty() && self.moved.is_empty()
1131    }
1132}
1133
1134/// Takes the nodes whose geometry the last layout pass actually changed.
1135///
1136/// The scene phase merges these into its scoped update scope. Consuming them
1137/// is mandatory whenever layout ran: geometry recorded by one pass is
1138/// meaningless to the next.
1139pub fn take_geometry_scene_nodes() -> GeometrySceneNodes {
1140    let mut nodes = GeometrySceneNodes::default();
1141    take_geometry_scene_nodes_into(&mut nodes);
1142    nodes
1143}
1144
1145/// Takes changed geometry nodes into reusable caller-owned storage.
1146///
1147/// ```
1148/// let context = cranpose_ui::AppContext::new();
1149/// context.enter(|| {
1150///     let mut nodes = cranpose_ui::GeometrySceneNodes::default();
1151///     cranpose_ui::take_geometry_scene_nodes_into(&mut nodes);
1152///     assert!(nodes.is_empty());
1153/// });
1154/// ```
1155pub fn take_geometry_scene_nodes_into(output: &mut GeometrySceneNodes) {
1156    with_render_state(|state| {
1157        state
1158            .geometry_scene_nodes
1159            .borrow_mut()
1160            .take_dirty_nodes_into(&mut output.reshaped);
1161        state
1162            .moved_scene_nodes
1163            .borrow_mut()
1164            .take_dirty_nodes_into(&mut output.moved);
1165    });
1166}
1167
1168/// Returns the current density scale factor (logical px per dp).
1169pub fn current_density() -> f32 {
1170    with_render_state(|state| f32::from_bits(state.density_bits.get()))
1171}
1172
1173/// Updates the current density scale factor.
1174///
1175/// This triggers a global layout invalidation when the value changes because
1176/// density impacts layout, text measurement, and input thresholds.
1177pub fn set_density(density: f32) {
1178    let normalized = normalize_density(density);
1179    let new_bits = normalized.to_bits();
1180    with_render_state(|state| {
1181        let old_bits = state.density_bits.replace(new_bits);
1182        if old_bits != new_bits {
1183            state.layout_invalidated.set(true);
1184        }
1185    });
1186}
1187
1188/// Returns the system font scale — the multiplier the user chose in the
1189/// platform's font-size setting, `1.0` when they left it alone.
1190///
1191/// This is the setting `Sp` is defined against, so text follows it while
1192/// everything measured in `Dp` does not. A platform that does not report one
1193/// leaves it at `1.0`.
1194///
1195/// It is the number to *report*, not the number to multiply by: what a size in
1196/// `Sp` comes to is [`scale_sp`], and on Android 14 and up the two are not the
1197/// same arithmetic. See [`crate::font_scale`].
1198pub fn current_font_scale() -> f32 {
1199    current_font_scale_curve().scale()
1200}
1201
1202/// Returns the conversion the platform performs for a size in `Sp`.
1203///
1204/// The setting is not a multiplier on every platform — see
1205/// [`crate::font_scale`] — so this, and not [`current_font_scale`], is what a
1206/// size in `Sp` is resolved through. The scalar remains the thing to *report*.
1207pub fn current_font_scale_curve() -> crate::font_scale::FontScaleCurve {
1208    with_render_state(|state| state.font_scale.get())
1209}
1210
1211/// The running app's font scale curve, or `None` when it leaves every size as
1212/// it is, which the caller then need not read.
1213pub(crate) fn current_scaling_font_scale_curve() -> Option<crate::font_scale::FontScaleCurve> {
1214    with_render_state(|state| (!state.font_scale_identity.get()).then(|| state.font_scale.get()))
1215}
1216
1217/// What a prepared text layout is valid for: the text service's generation
1218/// and the font scale curve's fingerprint, read in one app context access.
1219pub(crate) fn text_layout_stamp() -> (u64, u32) {
1220    let context = require_current_app_context("text layout stamp access");
1221    (
1222        context.text.generation(),
1223        context.state.font_scale_fingerprint.get(),
1224    )
1225}
1226
1227/// A size in scale-independent pixels, in dp, through the running app's curve.
1228pub fn scale_sp(sp: f32) -> f32 {
1229    current_font_scale_curve().sp_to_dp(sp)
1230}
1231
1232/// Updates the system font scale, taking it as a plain multiplier.
1233///
1234/// Hosts call this when the platform reports the setting, and again whenever it
1235/// changes while the app is running — on Android that is a configuration
1236/// change, which arrives without the process restarting. Like density it
1237/// invalidates layout, because every `Sp` size on screen has just changed.
1238///
1239/// A host whose platform converts `Sp` through a table of its own calls
1240/// [`set_font_scale_curve`] instead.
1241pub fn set_font_scale(scale: f32) {
1242    set_font_scale_curve(crate::font_scale::FontScaleCurve::linear(scale));
1243}
1244
1245/// Updates the system font scale and the conversion behind it.
1246///
1247/// Hosts that can read the platform's real `Sp` conversion call this instead of
1248/// [`set_font_scale`], which is the same thing with no table behind it. A curve
1249/// whose scale is not a value a platform could report is refused the same way a
1250/// bare scalar is, and refusing it drops the table with it: knots sampled at a
1251/// scale that was rejected describe a conversion the app is not going to use.
1252pub fn set_font_scale_curve(curve: crate::font_scale::FontScaleCurve) {
1253    let normalized = normalize_font_scale(curve.scale());
1254    let curve = if (normalized - curve.scale()).abs() <= f32::EPSILON {
1255        curve
1256    } else {
1257        crate::font_scale::FontScaleCurve::linear(normalized)
1258    };
1259    with_render_state(|state| {
1260        if state.font_scale.get() != curve {
1261            state.font_scale.set(curve);
1262            state.font_scale_identity.set(curve.is_identity());
1263            state.font_scale_fingerprint.set(curve.fingerprint());
1264            state.layout_invalidated.set(true);
1265        }
1266    });
1267}
1268
1269/// Requests that the renderer rebuild the current scene.
1270pub fn request_render_invalidation() {
1271    with_render_state(|state| state.render_invalidated.set(true));
1272}
1273
1274/// Returns true if a render invalidation was pending and clears the flag.
1275pub fn take_render_invalidation() -> bool {
1276    with_render_state(|state| state.render_invalidated.replace(false))
1277}
1278
1279/// Returns true if a render invalidation is pending without clearing it.
1280pub fn peek_render_invalidation() -> bool {
1281    with_render_state(|state| state.render_invalidated.get())
1282}
1283
1284/// Requests a new pointer-input pass without touching layout or draw dirties.
1285pub fn request_pointer_invalidation() {
1286    with_render_state(|state| state.pointer_invalidated.set(true));
1287}
1288
1289/// Returns true if a pointer invalidation was pending and clears the flag.
1290pub fn take_pointer_invalidation() -> bool {
1291    with_render_state(|state| state.pointer_invalidated.replace(false))
1292}
1293
1294/// Returns true if a pointer invalidation is pending without clearing it.
1295pub fn peek_pointer_invalidation() -> bool {
1296    with_render_state(|state| state.pointer_invalidated.get())
1297}
1298
1299/// Requests a focus recomposition without affecting layout/draw dirties.
1300pub fn request_focus_invalidation() {
1301    with_render_state(|state| state.focus_invalidated.set(true));
1302}
1303
1304/// Returns true if a focus invalidation was pending and clears the flag.
1305pub fn take_focus_invalidation() -> bool {
1306    with_render_state(|state| state.focus_invalidated.replace(false))
1307}
1308
1309/// Returns true if a focus invalidation is pending without clearing it.
1310pub fn peek_focus_invalidation() -> bool {
1311    with_render_state(|state| state.focus_invalidated.get())
1312}
1313
1314/// Requests a **global** layout re-run.
1315///
1316/// # ⚠️ WARNING: Extremely Expensive - O(entire app size)
1317///
1318/// This triggers internal cache invalidation that forces **every node** in the app
1319/// to re-measure, even if nothing changed. This is a performance footgun!
1320///
1321/// ## Valid Use Cases (rare!)
1322///
1323/// Only use this for **true global changes** that affect layout computation everywhere:
1324/// - Window/viewport resize
1325/// - Global font scale or density changes
1326/// - System-wide theme changes that affect layout
1327/// - Debug toggles that change layout behavior globally
1328///
1329/// ## For Local Changes - DO NOT USE THIS
1330///
1331/// **If you're invalidating layout for scroll, a single widget update, or any local change,
1332/// you MUST use the scoped repass mechanism instead:**
1333///
1334/// ```text
1335/// cranpose_ui::schedule_layout_repass(node_id);
1336/// ```
1337///
1338/// Scoped repasses give you O(subtree) performance instead of O(app), and they don't
1339/// invalidate caches across the entire app.
1340pub fn request_layout_invalidation() {
1341    with_render_state(|state| state.layout_invalidated.set(true));
1342}
1343
1344/// Returns true if a layout invalidation was pending and clears the flag.
1345pub fn take_layout_invalidation() -> bool {
1346    with_render_state(|state| state.layout_invalidated.replace(false))
1347}
1348
1349/// Returns true if a layout invalidation is pending without clearing it.
1350pub fn peek_layout_invalidation() -> bool {
1351    with_render_state(|state| state.layout_invalidated.get())
1352}
1353
1354#[cfg(any(test, feature = "test-helpers"))]
1355#[doc(hidden)]
1356pub fn reset_render_state_for_tests() {
1357    let _ = take_draw_repass_nodes();
1358    let mut layer_nodes = Vec::new();
1359    take_layer_property_repass_nodes_into(&mut layer_nodes);
1360    let _ = take_layout_repass_nodes();
1361    let _ = take_modifier_slice_repass_nodes();
1362    append_z_order_changes(&mut layer_nodes);
1363    let _ = take_render_invalidation();
1364    let _ = take_pointer_invalidation();
1365    let _ = take_focus_invalidation();
1366    let _ = take_layout_invalidation();
1367    debug_reset_last_fling_velocity();
1368    set_density(1.0);
1369    set_font_scale(1.0);
1370    let _ = take_layout_invalidation();
1371}
1372
1373#[cfg(test)]
1374pub(crate) struct TestAppContextScope {
1375    _scope: AppContextScope,
1376    _context: Rc<AppContext>,
1377}
1378
1379#[cfg(test)]
1380pub(crate) fn app_context_test_scope() -> TestAppContextScope {
1381    let context = AppContext::new();
1382    let scope = context.enter_scope();
1383    context.enter(reset_render_state_for_tests);
1384    TestAppContextScope {
1385        _scope: scope,
1386        _context: context,
1387    }
1388}
1389
1390#[cfg(test)]
1391pub(crate) struct RenderStateTestGuard {
1392    _app_scope: TestAppContextScope,
1393    _lock: std::sync::MutexGuard<'static, ()>,
1394}
1395
1396#[cfg(test)]
1397pub(crate) fn render_state_test_guard() -> RenderStateTestGuard {
1398    static TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
1399    let lock = match TEST_LOCK.get_or_init(|| Mutex::new(())).lock() {
1400        Ok(guard) => guard,
1401        Err(poisoned) => poisoned.into_inner(),
1402    };
1403    RenderStateTestGuard {
1404        _app_scope: app_context_test_scope(),
1405        _lock: lock,
1406    }
1407}
1408
1409#[cfg(test)]
1410#[path = "tests/render_state_tests.rs"]
1411mod tests;