Skip to main content

cranpose_ui/
render_state.rs

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