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