Skip to main content

cranpose_ui/
render_state.rs

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