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.
483pub(crate) fn begin_text_layout_pass() {
484    if let Some(context) = current_app_context() {
485        context.text.begin_layout_pass();
486    }
487}
488
489pub(crate) fn with_text_service<R>(f: impl FnOnce(&crate::text::measure::TextService) -> R) -> R {
490    let context = require_current_app_context("text service access");
491    f(&context.text)
492}
493
494pub(crate) fn set_current_text_measurer(measurer: Rc<dyn crate::text::TextMeasurer>) {
495    let Some(context) = current_app_context() else {
496        panic!("set_text_measurer requires an active AppContext");
497    };
498    context.set_text_measurer_rc(measurer);
499}
500
501pub(crate) fn set_modifier_chain_trace(callback: Arc<ModifierChainTraceCallback>) -> AppContextId {
502    let context = require_current_app_context("modifier chain trace installation");
503    *context.modifier_chain_trace.borrow_mut() = Some(callback);
504    context.id
505}
506
507pub(crate) fn clear_modifier_chain_trace(context_id: AppContextId) {
508    let _ = with_app_context_by_id(context_id, |context| {
509        *context.modifier_chain_trace.borrow_mut() = None;
510    });
511}
512
513pub(crate) fn emit_modifier_chain_trace(nodes: &[crate::modifier::ModifierChainInspectorNode]) {
514    let Some(context) = current_app_context() else {
515        return;
516    };
517    let callback = context.modifier_chain_trace.borrow().clone();
518    if let Some(callback) = callback {
519        callback(nodes);
520    }
521}
522
523pub(crate) fn take_layout_frame_arena() -> crate::layout::FrameLayoutArena {
524    let context = require_current_app_context("layout frame arena access");
525
526    std::mem::take(&mut *context.layout_frame_arena.borrow_mut())
527}
528
529/// The host for a layout pass: the one the last pass gave back, or a new one
530/// when a pass is still running or none ran yet.
531pub(crate) fn take_layout_host() -> crate::layout::LayoutHost {
532    let context = require_current_app_context("layout host access");
533    context.layout_host.borrow_mut().take().unwrap_or_default()
534}
535
536pub(crate) fn replace_layout_host(host: crate::layout::LayoutHost) {
537    if let Some(context) = current_app_context() {
538        *context.layout_host.borrow_mut() = Some(host);
539    }
540}
541
542pub(crate) fn replace_layout_frame_arena(arena: crate::layout::FrameLayoutArena) {
543    let context = require_current_app_context("layout frame arena access");
544    *context.layout_frame_arena.borrow_mut() = arena;
545}
546
547pub(crate) fn invalidate_layout_cache_epoch() {
548    require_current_app_context("layout cache epoch access").invalidate_layout_caches();
549}
550
551pub(crate) fn next_layout_cache_epoch() -> u64 {
552    let context = require_current_app_context("layout cache epoch access");
553    let epoch = context.layout_cache_epoch.get();
554    context.layout_cache_epoch.set(epoch + 1);
555    epoch
556}
557
558pub(crate) fn current_layout_cache_epoch() -> u64 {
559    let context = require_current_app_context("layout cache epoch access");
560    context.layout_cache_epoch.get()
561}
562
563pub(crate) fn layout_cache_floor() -> u64 {
564    let context = require_current_app_context("layout cache epoch access");
565    context.layout_cache_floor.get()
566}
567
568pub(crate) fn record_last_fling_velocity(velocity: f32) {
569    if let Some(context) = current_app_context() {
570        context.last_fling_velocity_bits.set(velocity.to_bits());
571    }
572}
573
574#[doc(hidden)]
575pub fn debug_last_fling_velocity() -> f32 {
576    let context = require_current_app_context("fling velocity diagnostics access");
577    f32::from_bits(context.last_fling_velocity_bits.get())
578}
579
580#[doc(hidden)]
581pub fn debug_reset_last_fling_velocity() {
582    let context = require_current_app_context("fling velocity diagnostics access");
583    context.last_fling_velocity_bits.set(0.0f32.to_bits());
584}
585
586impl AppContext {
587    pub(crate) fn lazy_prefetch(&self) -> &crate::lazy_prefetch::LazyPrefetchState {
588        &self.lazy_prefetch
589    }
590
591    /// Whether the next frame has work in this context: a render, pointer,
592    /// focus or layout invalidation, or a layout, measure or layer-property
593    /// repass. Reads the context directly, without looking up the current
594    /// one.
595    pub fn has_pending_frame_work(&self) -> bool {
596        let state = &self.state;
597        state.render_invalidated.get()
598            || state.pointer_invalidated.get()
599            || state.focus_invalidated.get()
600            || state.layout_invalidated.get()
601            || state.layout_repasses.borrow_mut().has_pending_repass()
602            || state.measure_repasses.borrow_mut().has_pending_repass()
603            || state
604                .layer_property_repasses
605                .borrow_mut()
606                .has_pending_repass()
607    }
608
609    /// [`Self::has_pending_frame_work`], or a pending draw or pointer repass
610    /// or focus invalidation.
611    pub fn has_stale_work(&self) -> bool {
612        self.has_pending_frame_work()
613            || self.state.draw_repasses.borrow_mut().has_pending_repass()
614            || self.pointer_dispatch.has_pending_repass()
615            || self.focus_dispatch.has_pending_invalidation()
616    }
617}
618
619pub(crate) fn with_scroll_motion_context_store<R>(
620    f: impl FnOnce(&crate::scroll::ScrollMotionContextStore) -> R,
621) -> R {
622    let context = require_current_app_context("scroll motion context access");
623    f(&context.scroll_motion_contexts)
624}
625
626#[doc(hidden)]
627pub fn clear_transient_scroll_motion_contexts() {
628    let Some(context) = current_app_context() else {
629        return;
630    };
631    context.scroll_motion_contexts.clear_transient_after_frame();
632}
633
634#[cfg(test)]
635pub(crate) fn layout_frame_arena_placement_scratch_count() -> usize {
636    let context = require_current_app_context("layout frame arena access");
637
638    context
639        .layout_frame_arena
640        .borrow()
641        .available_placement_scratch_count()
642}
643
644/// Runs `f` on the current app context's layout node registry and returns
645/// that context's id.
646pub(crate) fn with_current_layout_node_registry(
647    f: impl FnOnce(&crate::widgets::nodes::layout_node::LayoutNodeRegistryState),
648) -> AppContextId {
649    let context = require_current_app_context("layout node registry access");
650    f(&context.layout_node_registry);
651    context.id
652}
653
654pub(crate) fn with_layout_node_registry<R>(
655    f: impl FnOnce(&crate::widgets::nodes::layout_node::LayoutNodeRegistryState) -> R,
656) -> R {
657    let context = require_current_app_context("layout node registry access");
658    f(&context.layout_node_registry)
659}
660
661pub(crate) fn with_pointer_dispatch<R>(
662    f: impl FnOnce(&crate::pointer_dispatch::PointerDispatchState) -> R,
663) -> R {
664    let context = require_current_app_context("pointer dispatch access");
665    f(&context.pointer_dispatch)
666}
667
668pub(crate) fn with_focus_dispatch<R>(
669    f: impl FnOnce(&crate::focus_dispatch::FocusInvalidationState) -> R,
670) -> R {
671    let context = require_current_app_context("focus dispatch access");
672    f(&context.focus_dispatch)
673}
674
675pub(crate) fn with_focus_dispatch_by_app_context<R>(
676    id: AppContextId,
677    f: impl FnOnce(&crate::focus_dispatch::FocusInvalidationState) -> R,
678) -> Option<R> {
679    with_app_context_by_id(id, |context| context.enter(|| f(&context.focus_dispatch)))
680}
681
682pub(crate) fn with_modal_state<R>(f: impl FnOnce(&crate::modal::ModalState) -> R) -> R {
683    let context = require_current_app_context("modal state access");
684    f(&context.modal)
685}
686
687pub(crate) fn with_current_modal_nodes<R>(
688    f: impl FnOnce(&crate::modal_nodes::ModalNodes) -> R,
689) -> Option<R> {
690    current_app_context().map(|context| f(&context.modal_nodes))
691}
692
693pub(crate) fn with_current_semantics_layout_log<R>(
694    f: impl FnOnce(&crate::semantics_layout_log::SemanticsLayoutLog) -> R,
695) -> Option<R> {
696    current_app_context().map(|context| f(&context.semantics_layout))
697}
698
699pub(crate) fn with_hosted_popup_registries<R>(
700    f: impl FnOnce(&crate::widgets::popup::HostedPopupRegistries) -> R,
701) -> R {
702    let context = require_current_app_context("hosted popup registries access");
703    f(&context.hosted_popups)
704}
705
706pub(crate) fn with_semantics_dispatch<R>(
707    f: impl FnOnce(&crate::semantics_dispatch::SemanticsInvalidationState) -> R,
708) -> R {
709    let context = require_current_app_context("semantics dispatch access");
710    f(&context.semantics_dispatch)
711}
712
713pub(crate) fn with_semantics_dispatch_by_app_context(
714    id: AppContextId,
715    f: impl FnOnce(&crate::semantics_dispatch::SemanticsInvalidationState),
716) {
717    with_app_context_by_id(id, |context| f(&context.semantics_dispatch));
718}
719
720pub(crate) fn current_app_context_id_opt() -> Option<AppContextId> {
721    current_app_context().map(|context| context.id)
722}
723
724pub(crate) fn with_cursor_animation<R>(
725    f: impl FnOnce(&crate::cursor_animation::CursorAnimationState) -> R,
726) -> R {
727    let context = require_current_app_context("cursor animation access");
728    f(&context.cursor_animation)
729}
730
731pub(crate) fn with_text_field_focus<R>(
732    f: impl FnOnce(&crate::text_field_focus::TextFieldFocusState) -> R,
733) -> R {
734    let context = require_current_app_context("text field focus access");
735    f(&context.text_field_focus)
736}
737
738pub(crate) fn with_text_input_session<R>(
739    f: impl FnOnce(&crate::text_input_session::PlatformTextInputState) -> R,
740) -> R {
741    let context = require_current_app_context("platform text input session access");
742    f(&context.text_input_session)
743}
744
745pub(crate) fn with_clipboard_session<R>(
746    f: impl FnOnce(&crate::clipboard_session::ClipboardSessionState) -> R,
747) -> R {
748    let context = require_current_app_context("clipboard session access");
749    f(&context.clipboard_session)
750}
751
752pub(crate) fn with_pointer_icon_session<R>(
753    f: impl FnOnce(&crate::pointer_icon_session::PointerIconState) -> R,
754) -> R {
755    let context = require_current_app_context("pointer icon session access");
756    f(&context.pointer_icon)
757}
758
759pub(crate) fn register_pointer_input_task(
760    task_id: u64,
761    task: Rc<crate::modifier::pointer_input::PointerInputTaskInner>,
762) -> crate::modifier::pointer_input::PointerInputTaskOwner {
763    let context = require_current_app_context("pointer input task registration");
764    context.pointer_input_tasks.insert(task_id, task);
765    crate::modifier::pointer_input::PointerInputTaskOwner::App(context.id)
766}
767
768pub(crate) fn remove_pointer_input_task(
769    owner: crate::modifier::pointer_input::PointerInputTaskOwner,
770    task_id: u64,
771) {
772    match owner {
773        crate::modifier::pointer_input::PointerInputTaskOwner::App(context_id) => {
774            let _ = with_app_context_by_id(context_id, |context| {
775                context.pointer_input_tasks.remove(task_id);
776            });
777        }
778    }
779}
780
781pub(crate) fn request_pointer_input_task_poll(
782    owner: crate::modifier::pointer_input::PointerInputTaskOwner,
783    task_id: u64,
784) {
785    match owner {
786        crate::modifier::pointer_input::PointerInputTaskOwner::App(context_id) => {
787            let _ = with_app_context_by_id(context_id, |context| {
788                context.enter(|| {
789                    context.pointer_input_tasks.request_poll(task_id, owner);
790                });
791            });
792        }
793    }
794}
795
796fn with_draw_observer<R>(f: impl FnOnce(&SnapshotStateObserver) -> R) -> R {
797    let context = require_current_app_context("draw observer access");
798    f(&context.draw_observer)
799}
800
801struct DirtyNodeSet {
802    dirty_nodes: HashSet<NodeId>,
803}
804
805impl DirtyNodeSet {
806    fn new() -> Self {
807        Self {
808            dirty_nodes: HashSet::default(),
809        }
810    }
811
812    fn schedule_repass(&mut self, node_id: NodeId) {
813        self.dirty_nodes.insert(node_id);
814    }
815
816    fn has_pending_repass(&self) -> bool {
817        !self.dirty_nodes.is_empty()
818    }
819
820    fn take_dirty_nodes(&mut self) -> Vec<NodeId> {
821        self.dirty_nodes.drain().collect()
822    }
823
824    fn take_dirty_nodes_into(&mut self, output: &mut Vec<NodeId>) {
825        output.clear();
826        output.reserve(self.dirty_nodes.len());
827        output.extend(self.dirty_nodes.drain());
828    }
829
830    fn dirty_nodes_snapshot(&self) -> Vec<NodeId> {
831        let mut nodes = self.dirty_nodes.iter().copied().collect::<Vec<_>>();
832        nodes.sort_unstable();
833        nodes
834    }
835}
836
837/// Schedules a layout repass for a specific node.
838///
839/// **This is the preferred way to invalidate layout for local changes** (e.g., scroll, single-node mutations).
840///
841/// The app shell will call `take_layout_repass_nodes()` and bubble dirty flags up the tree
842/// via `bubble_layout_dirty`. This gives you **O(subtree) performance** - only the affected
843/// subtree is remeasured, and layout caches for other parts of the app remain valid.
844///
845/// A scoped repass never invalidates the whole tree, and it never cancels a global
846/// invalidation requested for the same frame.
847///
848/// # For Global Invalidation
849///
850/// For rare global events (window resize, global scale changes), use `request_layout_invalidation()` instead.
851#[track_caller]
852pub fn schedule_layout_repass(node_id: NodeId) {
853    if layout_repass_schedule_diagnostics_enabled_for(node_id) {
854        let caller = std::panic::Location::caller();
855        log::warn!(
856            "[layout-repass-schedule] node={} caller={}:{}:{}",
857            node_id,
858            caller.file(),
859            caller.line(),
860            caller.column()
861        );
862    }
863    with_render_state(|state| {
864        state.layout_repasses.borrow_mut().schedule_repass(node_id);
865    });
866    request_render_invalidation();
867}
868
869#[derive(Clone, Copy)]
870enum LayoutRepassScheduleDiag {
871    Disabled,
872    All,
873    Node(NodeId),
874}
875
876fn layout_repass_schedule_diagnostics_enabled_for(node_id: NodeId) -> bool {
877    static MODE: std::sync::OnceLock<LayoutRepassScheduleDiag> = std::sync::OnceLock::new();
878    match *MODE.get_or_init(|| {
879        let Some(value) = std::env::var_os("CRANPOSE_LAYOUT_REPASS_SCHEDULE_DIAG") else {
880            return LayoutRepassScheduleDiag::Disabled;
881        };
882        if value == "all" {
883            return LayoutRepassScheduleDiag::All;
884        }
885        value.to_string_lossy().parse::<NodeId>().map_or(
886            LayoutRepassScheduleDiag::Disabled,
887            LayoutRepassScheduleDiag::Node,
888        )
889    }) {
890        LayoutRepassScheduleDiag::Disabled => false,
891        LayoutRepassScheduleDiag::All => true,
892        LayoutRepassScheduleDiag::Node(target) => target == node_id,
893    }
894}
895
896pub(crate) fn schedule_modifier_slices_repass(node_id: NodeId) {
897    with_render_state(|state| {
898        state
899            .modifier_slice_repasses
900            .borrow_mut()
901            .schedule_repass(node_id);
902    });
903    schedule_draw_repass(node_id);
904}
905
906/// Schedules a draw-only repass for a specific node.
907///
908/// This ensures draw/pointer data stays in sync when modifier updates do not
909/// require a layout pass (e.g., draw-only modifier changes).
910pub fn schedule_draw_repass(node_id: NodeId) {
911    let context = require_current_app_context("render state access");
912    schedule_draw_repass_in_context(&context, node_id);
913}
914
915fn schedule_draw_repass_for_app_context(context_id: AppContextId, node_id: NodeId) {
916    let _ = with_app_context_by_id(context_id, |context| {
917        schedule_draw_repass_in_context(context, node_id);
918    });
919}
920
921fn schedule_draw_repass_in_context(context: &AppContext, node_id: NodeId) {
922    context
923        .state
924        .draw_repasses
925        .borrow_mut()
926        .schedule_repass(node_id);
927    context.state.render_invalidated.set(true);
928}
929
930fn schedule_layer_property_repass_for_app_context(context_id: AppContextId, node_id: NodeId) {
931    let _ = with_app_context_by_id(context_id, |context| {
932        schedule_layer_property_repass_in_context(context, node_id);
933    });
934}
935
936fn schedule_layer_property_repass_in_context(context: &AppContext, node_id: NodeId) {
937    context
938        .state
939        .layer_property_repasses
940        .borrow_mut()
941        .schedule_repass(node_id);
942    context.state.render_invalidated.set(true);
943}
944
945/// Returns true if any draw repasses are pending.
946pub fn has_pending_draw_repasses() -> bool {
947    with_render_state(|state| state.draw_repasses.borrow_mut().has_pending_repass())
948}
949
950/// Takes all pending draw repass node IDs.
951pub fn take_draw_repass_nodes() -> Vec<NodeId> {
952    with_render_state(|state| state.draw_repasses.borrow_mut().take_dirty_nodes())
953}
954
955/// Takes pending draw repasses into reusable caller-owned storage.
956///
957/// ```
958/// let context = cranpose_ui::AppContext::new();
959/// context.enter(|| {
960///     let mut nodes = Vec::new();
961///     cranpose_ui::take_draw_repass_nodes_into(&mut nodes);
962///     assert!(nodes.is_empty());
963/// });
964/// ```
965pub fn take_draw_repass_nodes_into(output: &mut Vec<NodeId>) {
966    with_render_state(|state| {
967        state
968            .draw_repasses
969            .borrow_mut()
970            .take_dirty_nodes_into(output);
971    });
972}
973
974/// Records that `node_id`'s z index changed. The frame rebuilds the layer of
975/// its parent, which holds the order the parent's children draw and take
976/// taps in.
977pub fn schedule_z_order_change(node_id: NodeId) {
978    with_render_state(|state| {
979        state.z_order_changes.borrow_mut().schedule_repass(node_id);
980        state.render_invalidated.set(true);
981    });
982}
983
984/// Appends the nodes whose z index changed since the last call to `output`,
985/// after what it already holds.
986pub fn append_z_order_changes(output: &mut Vec<NodeId>) {
987    with_render_state(|state| {
988        output.extend(state.z_order_changes.borrow_mut().dirty_nodes.drain());
989    });
990}
991
992/// Returns true if graphics-layer property updates are pending.
993#[doc(hidden)]
994pub fn has_pending_layer_property_repasses() -> bool {
995    with_render_state(|state| {
996        state
997            .layer_property_repasses
998            .borrow_mut()
999            .has_pending_repass()
1000    })
1001}
1002
1003/// Takes pending graphics-layer property updates into reusable storage.
1004#[doc(hidden)]
1005pub fn take_layer_property_repass_nodes_into(output: &mut Vec<NodeId>) {
1006    with_render_state(|state| {
1007        state
1008            .layer_property_repasses
1009            .borrow_mut()
1010            .take_dirty_nodes_into(output);
1011    });
1012}
1013
1014/// Returns true if any layout repasses are pending.
1015pub fn has_pending_layout_repasses() -> bool {
1016    with_render_state(|state| state.layout_repasses.borrow_mut().has_pending_repass())
1017}
1018
1019/// Returns a stable snapshot of pending layout repass node IDs without consuming them.
1020pub fn pending_layout_repass_nodes_snapshot() -> Vec<NodeId> {
1021    with_render_state(|state| state.layout_repasses.borrow_mut().dirty_nodes_snapshot())
1022}
1023
1024/// Takes all pending layout repass node IDs.
1025///
1026/// The caller should iterate over these and call `bubble_layout_dirty` for each.
1027pub fn take_layout_repass_nodes() -> Vec<NodeId> {
1028    with_render_state(|state| state.layout_repasses.borrow_mut().take_dirty_nodes())
1029}
1030
1031/// Schedules a scoped re-*measure* of `node_id` on the next frame.
1032///
1033/// Like [`schedule_layout_repass`], but processing bubbles *measure* dirtiness
1034/// (not just layout/placement) up the tree, so the node and its ancestors are
1035/// re-measured. Use this when a node's own measured size changes off a frame
1036/// callback (e.g. a row collapsing after a swipe dismiss): a plain layout
1037/// repass would leave the node's `needs_measure` flag unset, and an enclosing
1038/// `LazyColumn` would reuse its cached, full-height item slot.
1039pub fn schedule_measure_repass(node_id: NodeId) {
1040    with_render_state(|state| {
1041        state.measure_repasses.borrow_mut().schedule_repass(node_id);
1042    });
1043    request_render_invalidation();
1044}
1045
1046/// Returns true if any measure repasses are pending.
1047pub fn has_pending_measure_repasses() -> bool {
1048    with_render_state(|state| state.measure_repasses.borrow_mut().has_pending_repass())
1049}
1050
1051/// Returns a stable snapshot of pending measure repass node IDs without
1052/// consuming them.
1053///
1054/// The layout pass takes these ids to bubble measure dirtiness; the scene phase
1055/// needs the same ids *before* that happens, to scope its graph update to the
1056/// subtree that moved. Without the snapshot a measure repass reaches the scene
1057/// phase as "something changed, but nothing says where", which is
1058/// indistinguishable from a full invalidation.
1059pub fn pending_measure_repass_nodes_snapshot() -> Vec<NodeId> {
1060    with_render_state(|state| state.measure_repasses.borrow_mut().dirty_nodes_snapshot())
1061}
1062
1063/// Takes all pending measure repass node IDs.
1064///
1065/// The caller should iterate over these and call `bubble_measure_dirty` for each.
1066pub fn take_measure_repass_nodes() -> Vec<NodeId> {
1067    with_render_state(|state| state.measure_repasses.borrow_mut().take_dirty_nodes())
1068}
1069
1070pub(crate) fn take_modifier_slice_repass_nodes() -> Vec<NodeId> {
1071    with_render_state(|state| {
1072        state
1073            .modifier_slice_repasses
1074            .borrow_mut()
1075            .take_dirty_nodes()
1076    })
1077}
1078
1079pub(crate) fn record_geometry_scene_node(node_id: NodeId) {
1080    with_render_state(|state| {
1081        state
1082            .geometry_scene_nodes
1083            .borrow_mut()
1084            .schedule_repass(node_id);
1085    });
1086}
1087
1088pub(crate) fn record_moved_scene_node(node_id: NodeId) {
1089    with_render_state(|state| {
1090        state
1091            .moved_scene_nodes
1092            .borrow_mut()
1093            .schedule_repass(node_id);
1094    });
1095}
1096
1097/// The nodes whose geometry a layout pass changed, by what the scene redoes
1098/// for them.
1099#[derive(Clone, Debug, Default, PartialEq, Eq)]
1100pub struct GeometrySceneNodes {
1101    /// Nodes that changed size or were placed anew: the scene draws them
1102    /// again.
1103    pub reshaped: Vec<NodeId>,
1104    /// Nodes that moved in their parent: the scene moves what it drew for
1105    /// them, unless they are also reshaped or drawn again.
1106    pub moved: Vec<NodeId>,
1107}
1108
1109impl GeometrySceneNodes {
1110    /// Whether the pass changed no node's geometry.
1111    pub fn is_empty(&self) -> bool {
1112        self.reshaped.is_empty() && self.moved.is_empty()
1113    }
1114}
1115
1116/// Takes the nodes whose geometry the last layout pass actually changed.
1117///
1118/// The scene phase merges these into its scoped update scope. Consuming them
1119/// is mandatory whenever layout ran: geometry recorded by one pass is
1120/// meaningless to the next.
1121pub fn take_geometry_scene_nodes() -> GeometrySceneNodes {
1122    let mut nodes = GeometrySceneNodes::default();
1123    take_geometry_scene_nodes_into(&mut nodes);
1124    nodes
1125}
1126
1127/// Takes changed geometry nodes into reusable caller-owned storage.
1128///
1129/// ```
1130/// let context = cranpose_ui::AppContext::new();
1131/// context.enter(|| {
1132///     let mut nodes = cranpose_ui::GeometrySceneNodes::default();
1133///     cranpose_ui::take_geometry_scene_nodes_into(&mut nodes);
1134///     assert!(nodes.is_empty());
1135/// });
1136/// ```
1137pub fn take_geometry_scene_nodes_into(output: &mut GeometrySceneNodes) {
1138    with_render_state(|state| {
1139        state
1140            .geometry_scene_nodes
1141            .borrow_mut()
1142            .take_dirty_nodes_into(&mut output.reshaped);
1143        state
1144            .moved_scene_nodes
1145            .borrow_mut()
1146            .take_dirty_nodes_into(&mut output.moved);
1147    });
1148}
1149
1150/// Returns the current density scale factor (logical px per dp).
1151pub fn current_density() -> f32 {
1152    with_render_state(|state| f32::from_bits(state.density_bits.get()))
1153}
1154
1155/// Updates the current density scale factor.
1156///
1157/// This triggers a global layout invalidation when the value changes because
1158/// density impacts layout, text measurement, and input thresholds.
1159pub fn set_density(density: f32) {
1160    let normalized = normalize_density(density);
1161    let new_bits = normalized.to_bits();
1162    with_render_state(|state| {
1163        let old_bits = state.density_bits.replace(new_bits);
1164        if old_bits != new_bits {
1165            state.layout_invalidated.set(true);
1166        }
1167    });
1168}
1169
1170/// Returns the system font scale — the multiplier the user chose in the
1171/// platform's font-size setting, `1.0` when they left it alone.
1172///
1173/// This is the setting `Sp` is defined against, so text follows it while
1174/// everything measured in `Dp` does not. A platform that does not report one
1175/// leaves it at `1.0`.
1176///
1177/// It is the number to *report*, not the number to multiply by: what a size in
1178/// `Sp` comes to is [`scale_sp`], and on Android 14 and up the two are not the
1179/// same arithmetic. See [`crate::font_scale`].
1180pub fn current_font_scale() -> f32 {
1181    current_font_scale_curve().scale()
1182}
1183
1184/// Returns the conversion the platform performs for a size in `Sp`.
1185///
1186/// The setting is not a multiplier on every platform — see
1187/// [`crate::font_scale`] — so this, and not [`current_font_scale`], is what a
1188/// size in `Sp` is resolved through. The scalar remains the thing to *report*.
1189pub fn current_font_scale_curve() -> crate::font_scale::FontScaleCurve {
1190    with_render_state(|state| state.font_scale.get())
1191}
1192
1193/// The running app's font scale curve, or `None` when it leaves every size as
1194/// it is, which the caller then need not read.
1195pub(crate) fn current_scaling_font_scale_curve() -> Option<crate::font_scale::FontScaleCurve> {
1196    with_render_state(|state| (!state.font_scale_identity.get()).then(|| state.font_scale.get()))
1197}
1198
1199/// What a prepared text layout is valid for: the text service's generation
1200/// and the font scale curve's fingerprint, read in one app context access.
1201pub(crate) fn text_layout_stamp() -> (u64, u32) {
1202    let context = require_current_app_context("text layout stamp access");
1203    (
1204        context.text.generation(),
1205        context.state.font_scale_fingerprint.get(),
1206    )
1207}
1208
1209/// A size in scale-independent pixels, in dp, through the running app's curve.
1210pub fn scale_sp(sp: f32) -> f32 {
1211    current_font_scale_curve().sp_to_dp(sp)
1212}
1213
1214/// Updates the system font scale, taking it as a plain multiplier.
1215///
1216/// Hosts call this when the platform reports the setting, and again whenever it
1217/// changes while the app is running — on Android that is a configuration
1218/// change, which arrives without the process restarting. Like density it
1219/// invalidates layout, because every `Sp` size on screen has just changed.
1220///
1221/// A host whose platform converts `Sp` through a table of its own calls
1222/// [`set_font_scale_curve`] instead.
1223pub fn set_font_scale(scale: f32) {
1224    set_font_scale_curve(crate::font_scale::FontScaleCurve::linear(scale));
1225}
1226
1227/// Updates the system font scale and the conversion behind it.
1228///
1229/// Hosts that can read the platform's real `Sp` conversion call this instead of
1230/// [`set_font_scale`], which is the same thing with no table behind it. A curve
1231/// whose scale is not a value a platform could report is refused the same way a
1232/// bare scalar is, and refusing it drops the table with it: knots sampled at a
1233/// scale that was rejected describe a conversion the app is not going to use.
1234pub fn set_font_scale_curve(curve: crate::font_scale::FontScaleCurve) {
1235    let normalized = normalize_font_scale(curve.scale());
1236    let curve = if (normalized - curve.scale()).abs() <= f32::EPSILON {
1237        curve
1238    } else {
1239        crate::font_scale::FontScaleCurve::linear(normalized)
1240    };
1241    with_render_state(|state| {
1242        if state.font_scale.get() != curve {
1243            state.font_scale.set(curve);
1244            state.font_scale_identity.set(curve.is_identity());
1245            state.font_scale_fingerprint.set(curve.fingerprint());
1246            state.layout_invalidated.set(true);
1247        }
1248    });
1249}
1250
1251/// Requests that the renderer rebuild the current scene.
1252pub fn request_render_invalidation() {
1253    with_render_state(|state| state.render_invalidated.set(true));
1254}
1255
1256/// Returns true if a render invalidation was pending and clears the flag.
1257pub fn take_render_invalidation() -> bool {
1258    with_render_state(|state| state.render_invalidated.replace(false))
1259}
1260
1261/// Returns true if a render invalidation is pending without clearing it.
1262pub fn peek_render_invalidation() -> bool {
1263    with_render_state(|state| state.render_invalidated.get())
1264}
1265
1266/// Requests a new pointer-input pass without touching layout or draw dirties.
1267pub fn request_pointer_invalidation() {
1268    with_render_state(|state| state.pointer_invalidated.set(true));
1269}
1270
1271/// Returns true if a pointer invalidation was pending and clears the flag.
1272pub fn take_pointer_invalidation() -> bool {
1273    with_render_state(|state| state.pointer_invalidated.replace(false))
1274}
1275
1276/// Returns true if a pointer invalidation is pending without clearing it.
1277pub fn peek_pointer_invalidation() -> bool {
1278    with_render_state(|state| state.pointer_invalidated.get())
1279}
1280
1281/// Requests a focus recomposition without affecting layout/draw dirties.
1282pub fn request_focus_invalidation() {
1283    with_render_state(|state| state.focus_invalidated.set(true));
1284}
1285
1286/// Returns true if a focus invalidation was pending and clears the flag.
1287pub fn take_focus_invalidation() -> bool {
1288    with_render_state(|state| state.focus_invalidated.replace(false))
1289}
1290
1291/// Returns true if a focus invalidation is pending without clearing it.
1292pub fn peek_focus_invalidation() -> bool {
1293    with_render_state(|state| state.focus_invalidated.get())
1294}
1295
1296/// Requests a **global** layout re-run.
1297///
1298/// # ⚠️ WARNING: Extremely Expensive - O(entire app size)
1299///
1300/// This triggers internal cache invalidation that forces **every node** in the app
1301/// to re-measure, even if nothing changed. This is a performance footgun!
1302///
1303/// ## Valid Use Cases (rare!)
1304///
1305/// Only use this for **true global changes** that affect layout computation everywhere:
1306/// - Window/viewport resize
1307/// - Global font scale or density changes
1308/// - System-wide theme changes that affect layout
1309/// - Debug toggles that change layout behavior globally
1310///
1311/// ## For Local Changes - DO NOT USE THIS
1312///
1313/// **If you're invalidating layout for scroll, a single widget update, or any local change,
1314/// you MUST use the scoped repass mechanism instead:**
1315///
1316/// ```text
1317/// cranpose_ui::schedule_layout_repass(node_id);
1318/// ```
1319///
1320/// Scoped repasses give you O(subtree) performance instead of O(app), and they don't
1321/// invalidate caches across the entire app.
1322pub fn request_layout_invalidation() {
1323    with_render_state(|state| state.layout_invalidated.set(true));
1324}
1325
1326/// Returns true if a layout invalidation was pending and clears the flag.
1327pub fn take_layout_invalidation() -> bool {
1328    with_render_state(|state| state.layout_invalidated.replace(false))
1329}
1330
1331/// Returns true if a layout invalidation is pending without clearing it.
1332pub fn peek_layout_invalidation() -> bool {
1333    with_render_state(|state| state.layout_invalidated.get())
1334}
1335
1336#[cfg(any(test, feature = "test-helpers"))]
1337#[doc(hidden)]
1338pub fn reset_render_state_for_tests() {
1339    let _ = take_draw_repass_nodes();
1340    let mut layer_nodes = Vec::new();
1341    take_layer_property_repass_nodes_into(&mut layer_nodes);
1342    let _ = take_layout_repass_nodes();
1343    let _ = take_modifier_slice_repass_nodes();
1344    append_z_order_changes(&mut layer_nodes);
1345    let _ = take_render_invalidation();
1346    let _ = take_pointer_invalidation();
1347    let _ = take_focus_invalidation();
1348    let _ = take_layout_invalidation();
1349    debug_reset_last_fling_velocity();
1350    set_density(1.0);
1351    set_font_scale(1.0);
1352    let _ = take_layout_invalidation();
1353}
1354
1355#[cfg(test)]
1356pub(crate) struct TestAppContextScope {
1357    _scope: AppContextScope,
1358    _context: Rc<AppContext>,
1359}
1360
1361#[cfg(test)]
1362pub(crate) fn app_context_test_scope() -> TestAppContextScope {
1363    let context = AppContext::new();
1364    let scope = context.enter_scope();
1365    context.enter(reset_render_state_for_tests);
1366    TestAppContextScope {
1367        _scope: scope,
1368        _context: context,
1369    }
1370}
1371
1372#[cfg(test)]
1373pub(crate) struct RenderStateTestGuard {
1374    _app_scope: TestAppContextScope,
1375    _lock: std::sync::MutexGuard<'static, ()>,
1376}
1377
1378#[cfg(test)]
1379pub(crate) fn render_state_test_guard() -> RenderStateTestGuard {
1380    static TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
1381    let lock = match TEST_LOCK.get_or_init(|| Mutex::new(())).lock() {
1382        Ok(guard) => guard,
1383        Err(poisoned) => poisoned.into_inner(),
1384    };
1385    RenderStateTestGuard {
1386        _app_scope: app_context_test_scope(),
1387        _lock: lock,
1388    }
1389}
1390
1391#[cfg(test)]
1392#[path = "tests/render_state_tests.rs"]
1393mod tests;