Skip to main content

cranpose_ui/
render_state.rs

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