Skip to main content

cranpose_ui/
render_state.rs

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