1#[cfg(test)]
2use std::sync::OnceLock;
3use std::{
4 cell::{Cell, RefCell},
5 collections::{HashMap, HashSet},
6 rc::{Rc, Weak},
7 sync::{
8 Arc, Mutex, MutexGuard,
9 atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering},
10 },
11};
12
13use cranpose_core::{NodeId, SnapshotStateObserver, current_runtime_handle};
14
15pub(crate) type ModifierChainTraceCallback =
16 dyn Fn(&[crate::modifier::ModifierChainInspectorNode]) + Send + Sync + 'static;
17
18struct RenderState {
19 layout_repasses: Mutex<LayoutRepassManager>,
20 measure_repasses: Mutex<LayoutRepassManager>,
21 draw_repasses: Mutex<DrawRepassManager>,
22 modifier_slice_repasses: Mutex<LayoutRepassManager>,
23 geometry_scene_nodes: Mutex<LayoutRepassManager>,
24 render_invalidated: AtomicBool,
25 pointer_invalidated: AtomicBool,
26 focus_invalidated: AtomicBool,
27 layout_invalidated: AtomicBool,
28 density_bits: AtomicU32,
29 font_scale: Mutex<crate::font_scale::FontScaleCurve>,
30}
31
32#[doc(hidden)]
33pub struct AppContext {
34 id: AppContextId,
35 self_weak: RefCell<Weak<AppContext>>,
36 state: RenderState,
37 draw_observer: SnapshotStateObserver,
38 text: crate::text::measure::TextService,
39 layout_frame_arena: RefCell<crate::layout::FrameLayoutArena>,
40 layout_cache_epoch: AtomicU64,
41 last_fling_velocity_bits: AtomicU32,
42 scroll_motion_contexts: crate::scroll::ScrollMotionContextStore,
43 layout_node_registry: crate::widgets::nodes::layout_node::LayoutNodeRegistryState,
44 pointer_dispatch: crate::pointer_dispatch::PointerDispatchState,
45 focus_dispatch: crate::focus_dispatch::FocusInvalidationState,
46 semantics_dispatch: crate::semantics_dispatch::SemanticsInvalidationState,
47 cursor_animation: crate::cursor_animation::CursorAnimationState,
48 text_field_focus: crate::text_field_focus::TextFieldFocusState,
49 text_input_session: crate::text_input_session::PlatformTextInputState,
50 clipboard_session: crate::clipboard_session::ClipboardSessionState,
51 pointer_input_tasks: crate::modifier::pointer_input::PointerInputTaskRegistry,
52 modifier_chain_trace: RefCell<Option<Arc<ModifierChainTraceCallback>>>,
53}
54
55#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
56pub(crate) struct AppContextId(u64);
57
58#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
59pub(crate) struct DrawObservationScope {
60 node_id: NodeId,
61 command_index: usize,
62}
63
64impl DrawObservationScope {
65 pub(crate) fn new(node_id: NodeId, command_index: usize) -> Self {
66 Self {
67 node_id,
68 command_index,
69 }
70 }
71}
72
73fn new_draw_observer() -> SnapshotStateObserver {
74 let observer = SnapshotStateObserver::new(|callback| {
75 if let Some(runtime) = current_runtime_handle() {
76 runtime.enqueue_ui_task(callback);
77 } else {
78 callback();
79 }
80 });
81 observer.start();
82 observer
83}
84
85thread_local! {
86 static CURRENT_DRAW_NODE: std::cell::Cell<Option<NodeId>> = const { std::cell::Cell::new(None) };
87}
88
89struct CurrentDrawNodeGuard {
90 previous: Option<NodeId>,
91}
92
93impl Drop for CurrentDrawNodeGuard {
94 fn drop(&mut self) {
95 CURRENT_DRAW_NODE.with(|current| current.set(self.previous));
96 }
97}
98
99pub(crate) fn observe_draw_reads<R>(scope: DrawObservationScope, block: impl FnOnce() -> R) -> R {
100 let context = require_current_app_context("draw observer access");
101 let context_id = context.id;
102 let _guard = CurrentDrawNodeGuard {
103 previous: CURRENT_DRAW_NODE.with(|current| current.replace(Some(scope.node_id))),
104 };
105 context.draw_observer.observe_reads(
106 scope,
107 move |scope| {
108 schedule_draw_repass_for_app_context(context_id, scope.node_id);
109 },
110 block,
111 )
112}
113
114pub fn request_current_draw_redraw() {
121 if let Some(node_id) = CURRENT_DRAW_NODE.with(std::cell::Cell::get) {
122 schedule_draw_repass(node_id);
123 }
124 request_render_invalidation();
125}
126
127pub(crate) fn clear_draw_observations_for_node(node_id: NodeId) {
128 with_draw_observer(|observer| {
129 observer.clear_if(|scope| {
130 scope
131 .downcast_ref::<DrawObservationScope>()
132 .is_some_and(|scope| scope.node_id == node_id)
133 });
134 });
135}
136
137pub fn prune_draw_observations_to_nodes(retained: &HashSet<NodeId>) {
138 with_draw_observer(|observer| {
139 observer.clear_if(|scope| {
140 scope
141 .downcast_ref::<DrawObservationScope>()
142 .is_some_and(|scope| !retained.contains(&scope.node_id))
143 });
144 });
145}
146
147impl RenderState {
148 fn new_with_density(density: f32) -> Self {
149 Self {
150 layout_repasses: Mutex::new(LayoutRepassManager::new()),
151 measure_repasses: Mutex::new(LayoutRepassManager::new()),
152 draw_repasses: Mutex::new(DrawRepassManager::new()),
153 modifier_slice_repasses: Mutex::new(LayoutRepassManager::new()),
154 geometry_scene_nodes: Mutex::new(LayoutRepassManager::new()),
155 render_invalidated: AtomicBool::new(false),
156 pointer_invalidated: AtomicBool::new(false),
157 focus_invalidated: AtomicBool::new(false),
158 layout_invalidated: AtomicBool::new(false),
159 density_bits: AtomicU32::new(normalize_density(density).to_bits()),
160 font_scale: Mutex::new(crate::font_scale::FontScaleCurve::linear(1.0)),
161 }
162 }
163}
164
165std::thread_local! {
166 static NEXT_APP_CONTEXT_ID: Cell<u64> = const { Cell::new(1) };
167 static CURRENT_APP_CONTEXT: RefCell<Vec<Weak<AppContext>>> = const { RefCell::new(Vec::new()) };
168 static APP_CONTEXTS: RefCell<HashMap<AppContextId, Weak<AppContext>>> = RefCell::new(HashMap::new());
169}
170
171fn next_app_context_id() -> AppContextId {
172 NEXT_APP_CONTEXT_ID.with(|next| {
173 let id = next.get();
174 next.set(id.wrapping_add(1));
175 AppContextId(id)
176 })
177}
178
179#[doc(hidden)]
180pub struct AppContextScope;
181
182impl Drop for AppContextScope {
183 fn drop(&mut self) {
184 CURRENT_APP_CONTEXT.with(|stack| {
185 stack.borrow_mut().pop();
186 });
187 }
188}
189
190impl AppContext {
191 pub fn new() -> Rc<Self> {
192 Self::new_with_density(1.0)
193 }
194
195 pub fn new_with_density(density: f32) -> Rc<Self> {
196 let context = Rc::new(Self {
197 id: next_app_context_id(),
198 self_weak: RefCell::new(Weak::new()),
199 state: RenderState::new_with_density(density),
200 draw_observer: new_draw_observer(),
201 text: crate::text::measure::TextService::new(),
202 layout_frame_arena: RefCell::new(crate::layout::FrameLayoutArena::default()),
203 layout_cache_epoch: AtomicU64::new(1),
204 last_fling_velocity_bits: AtomicU32::new(0.0f32.to_bits()),
205 scroll_motion_contexts: crate::scroll::ScrollMotionContextStore::new(),
206 layout_node_registry: crate::widgets::nodes::layout_node::LayoutNodeRegistryState::new(
207 ),
208 pointer_dispatch: crate::pointer_dispatch::PointerDispatchState::new(),
209 focus_dispatch: crate::focus_dispatch::FocusInvalidationState::new(),
210 semantics_dispatch: crate::semantics_dispatch::SemanticsInvalidationState::new(),
211 cursor_animation: crate::cursor_animation::CursorAnimationState::new(),
212 text_field_focus: crate::text_field_focus::TextFieldFocusState::new(),
213 text_input_session: crate::text_input_session::PlatformTextInputState::new(),
214 clipboard_session: crate::clipboard_session::ClipboardSessionState::new(),
215 pointer_input_tasks: crate::modifier::pointer_input::PointerInputTaskRegistry::new(),
216 modifier_chain_trace: RefCell::new(None),
217 });
218 *context.self_weak.borrow_mut() = Rc::downgrade(&context);
219 APP_CONTEXTS.with(|contexts| {
220 contexts
221 .borrow_mut()
222 .insert(context.id, Rc::downgrade(&context));
223 });
224 context
225 }
226
227 pub fn enter<R>(self: &Rc<Self>, block: impl FnOnce() -> R) -> R {
228 let _scope = self.enter_scope();
229 block()
230 }
231
232 #[doc(hidden)]
233 pub fn enter_scope(self: &Rc<Self>) -> AppContextScope {
234 CURRENT_APP_CONTEXT.with(|stack| {
235 stack.borrow_mut().push(Rc::downgrade(self));
236 });
237 AppContextScope
238 }
239
240 pub fn set_text_measurer<M: crate::text::TextMeasurer>(&self, measurer: M) {
241 self.set_text_measurer_rc(Rc::new(measurer));
242 }
243
244 pub fn set_text_measurer_rc(&self, measurer: Rc<dyn crate::text::TextMeasurer>) {
245 self.text.set_measurer(measurer);
246 self.layout_cache_epoch.fetch_add(1, Ordering::Relaxed);
247 self.state.layout_invalidated.store(true, Ordering::Relaxed);
248 self.state.render_invalidated.store(true, Ordering::Relaxed);
249 }
250
251 #[doc(hidden)]
252 pub fn downgrade(&self) -> Weak<Self> {
253 self.self_weak.borrow().clone()
254 }
255}
256
257impl Drop for AppContext {
258 fn drop(&mut self) {
259 let id = self.id;
260 let _ = APP_CONTEXTS.try_with(|contexts| {
261 contexts.borrow_mut().remove(&id);
262 });
263 }
264}
265
266fn app_context_by_id(id: AppContextId) -> Option<Rc<AppContext>> {
267 APP_CONTEXTS
268 .try_with(|contexts| {
269 let context = contexts.borrow().get(&id).cloned()?;
270 let Some(context) = context.upgrade() else {
271 contexts.borrow_mut().remove(&id);
272 return None;
273 };
274 Some(context)
275 })
276 .ok()
277 .flatten()
278}
279
280#[cfg(test)]
281fn app_context_registry_entry_count() -> usize {
282 APP_CONTEXTS
283 .try_with(|contexts| contexts.borrow().len())
284 .unwrap_or_default()
285}
286
287fn with_app_context_by_id<R>(id: AppContextId, f: impl FnOnce(&Rc<AppContext>) -> R) -> Option<R> {
288 app_context_by_id(id).map(|context| f(&context))
289}
290
291pub(crate) fn current_app_context_id() -> AppContextId {
292 require_current_app_context("app context identity access").id
293}
294
295pub(crate) fn with_layout_node_registry_by_app_context<R>(
296 id: AppContextId,
297 f: impl FnOnce(&crate::widgets::nodes::layout_node::LayoutNodeRegistryState) -> R,
298) -> Option<R> {
299 with_app_context_by_id(id, |context| f(&context.layout_node_registry))
300}
301
302pub(crate) fn enter_app_context_by_id<R>(id: AppContextId, f: impl FnOnce() -> R) -> Option<R> {
303 with_app_context_by_id(id, |context| context.enter(f))
304}
305
306pub(crate) fn current_app_context() -> Option<Rc<AppContext>> {
307 CURRENT_APP_CONTEXT
308 .try_with(|stack| {
309 let mut stack = stack.borrow_mut();
310 loop {
311 let context = stack.last()?;
312 if let Some(context) = context.upgrade() {
313 return Some(context);
314 }
315 stack.pop();
316 }
317 })
318 .ok()
319 .flatten()
320}
321
322#[doc(hidden)]
323pub fn has_current_app_context() -> bool {
324 current_app_context().is_some()
325}
326
327fn require_current_app_context(operation: &str) -> Rc<AppContext> {
328 if let Some(context) = current_app_context() {
329 return context;
330 }
331 require_current_app_context_without_scope(operation)
332}
333
334fn require_current_app_context_without_scope(operation: &str) -> Rc<AppContext> {
335 panic!("{operation} requires an active AppContext")
336}
337
338fn with_render_state<R>(f: impl FnOnce(&RenderState) -> R) -> R {
339 let context = require_current_app_context("render state access");
340 f(&context.state)
341}
342
343fn normalize_density(density: f32) -> f32 {
344 if density.is_finite() && density > 0.0 {
345 density
346 } else {
347 1.0
348 }
349}
350
351fn normalize_font_scale(scale: f32) -> f32 {
352 if scale.is_finite() && scale > 0.0 {
353 scale.clamp(MIN_FONT_SCALE, MAX_FONT_SCALE)
354 } else {
355 1.0
356 }
357}
358
359pub const MIN_FONT_SCALE: f32 = 0.5;
362pub const MAX_FONT_SCALE: f32 = 3.0;
365
366pub(crate) fn with_text_measurer<R>(f: impl FnOnce(&dyn crate::text::TextMeasurer) -> R) -> R {
367 let context = require_current_app_context("text measurer access");
368 context.text.with_measurer(f)
369}
370
371pub(crate) fn with_text_service<R>(f: impl FnOnce(&crate::text::measure::TextService) -> R) -> R {
372 let context = require_current_app_context("text service access");
373 f(&context.text)
374}
375
376pub(crate) fn set_current_text_measurer(measurer: Rc<dyn crate::text::TextMeasurer>) {
377 let Some(context) = current_app_context() else {
378 panic!("set_text_measurer requires an active AppContext");
379 };
380 context.set_text_measurer_rc(measurer);
381}
382
383pub(crate) fn set_modifier_chain_trace(callback: Arc<ModifierChainTraceCallback>) -> AppContextId {
384 let context = require_current_app_context("modifier chain trace installation");
385 *context.modifier_chain_trace.borrow_mut() = Some(callback);
386 context.id
387}
388
389pub(crate) fn clear_modifier_chain_trace(context_id: AppContextId) {
390 let _ = with_app_context_by_id(context_id, |context| {
391 *context.modifier_chain_trace.borrow_mut() = None;
392 });
393}
394
395pub(crate) fn emit_modifier_chain_trace(nodes: &[crate::modifier::ModifierChainInspectorNode]) {
396 let Some(context) = current_app_context() else {
397 return;
398 };
399 let callback = context.modifier_chain_trace.borrow().clone();
400 if let Some(callback) = callback {
401 callback(nodes);
402 }
403}
404
405pub(crate) fn take_layout_frame_arena() -> crate::layout::FrameLayoutArena {
406 let context = require_current_app_context("layout frame arena access");
407
408 std::mem::take(&mut *context.layout_frame_arena.borrow_mut())
409}
410
411pub(crate) fn replace_layout_frame_arena(arena: crate::layout::FrameLayoutArena) {
412 let context = require_current_app_context("layout frame arena access");
413 *context.layout_frame_arena.borrow_mut() = arena;
414}
415
416pub(crate) fn invalidate_layout_cache_epoch() {
417 let context = require_current_app_context("layout cache epoch access");
418 context.layout_cache_epoch.fetch_add(1, Ordering::Relaxed);
419}
420
421pub(crate) fn next_layout_cache_epoch() -> u64 {
422 let context = require_current_app_context("layout cache epoch access");
423 context.layout_cache_epoch.fetch_add(1, Ordering::Relaxed)
424}
425
426pub(crate) fn current_layout_cache_epoch() -> u64 {
427 let context = require_current_app_context("layout cache epoch access");
428 context.layout_cache_epoch.load(Ordering::Relaxed)
429}
430
431pub(crate) fn record_last_fling_velocity(velocity: f32) {
432 if let Some(context) = current_app_context() {
433 context
434 .last_fling_velocity_bits
435 .store(velocity.to_bits(), Ordering::Relaxed);
436 }
437}
438
439#[doc(hidden)]
440pub fn debug_last_fling_velocity() -> f32 {
441 let context = require_current_app_context("fling velocity diagnostics access");
442 f32::from_bits(context.last_fling_velocity_bits.load(Ordering::Relaxed))
443}
444
445#[doc(hidden)]
446pub fn debug_reset_last_fling_velocity() {
447 let context = require_current_app_context("fling velocity diagnostics access");
448 context
449 .last_fling_velocity_bits
450 .store(0.0f32.to_bits(), Ordering::Relaxed);
451}
452
453pub(crate) fn with_scroll_motion_context_store<R>(
454 f: impl FnOnce(&crate::scroll::ScrollMotionContextStore) -> R,
455) -> R {
456 let context = require_current_app_context("scroll motion context access");
457 f(&context.scroll_motion_contexts)
458}
459
460#[doc(hidden)]
461pub fn clear_transient_scroll_motion_contexts() {
462 let Some(context) = current_app_context() else {
463 return;
464 };
465 context.scroll_motion_contexts.clear_transient_after_frame();
466}
467
468#[cfg(test)]
469pub(crate) fn layout_frame_arena_placement_scratch_count() -> usize {
470 let context = require_current_app_context("layout frame arena access");
471
472 context
473 .layout_frame_arena
474 .borrow()
475 .available_placement_scratch_count()
476}
477
478pub(crate) fn with_layout_node_registry<R>(
479 f: impl FnOnce(&crate::widgets::nodes::layout_node::LayoutNodeRegistryState) -> R,
480) -> R {
481 let context = require_current_app_context("layout node registry access");
482 f(&context.layout_node_registry)
483}
484
485pub(crate) fn with_pointer_dispatch<R>(
486 f: impl FnOnce(&crate::pointer_dispatch::PointerDispatchState) -> R,
487) -> R {
488 let context = require_current_app_context("pointer dispatch access");
489 f(&context.pointer_dispatch)
490}
491
492pub(crate) fn with_focus_dispatch<R>(
493 f: impl FnOnce(&crate::focus_dispatch::FocusInvalidationState) -> R,
494) -> R {
495 let context = require_current_app_context("focus dispatch access");
496 f(&context.focus_dispatch)
497}
498
499pub(crate) fn with_focus_dispatch_by_app_context<R>(
500 id: AppContextId,
501 f: impl FnOnce(&crate::focus_dispatch::FocusInvalidationState) -> R,
502) -> Option<R> {
503 with_app_context_by_id(id, |context| f(&context.focus_dispatch))
504}
505
506pub(crate) fn with_semantics_dispatch<R>(
507 f: impl FnOnce(&crate::semantics_dispatch::SemanticsInvalidationState) -> R,
508) -> R {
509 let context = require_current_app_context("semantics dispatch access");
510 f(&context.semantics_dispatch)
511}
512
513pub(crate) fn with_semantics_dispatch_by_app_context(
514 id: AppContextId,
515 f: impl FnOnce(&crate::semantics_dispatch::SemanticsInvalidationState),
516) {
517 with_app_context_by_id(id, |context| f(&context.semantics_dispatch));
518}
519
520pub(crate) fn current_app_context_id_opt() -> Option<AppContextId> {
521 current_app_context().map(|context| context.id)
522}
523
524pub(crate) fn with_cursor_animation<R>(
525 f: impl FnOnce(&crate::cursor_animation::CursorAnimationState) -> R,
526) -> R {
527 let context = require_current_app_context("cursor animation access");
528 f(&context.cursor_animation)
529}
530
531pub(crate) fn with_text_field_focus<R>(
532 f: impl FnOnce(&crate::text_field_focus::TextFieldFocusState) -> R,
533) -> R {
534 let context = require_current_app_context("text field focus access");
535 f(&context.text_field_focus)
536}
537
538pub(crate) fn with_text_input_session<R>(
539 f: impl FnOnce(&crate::text_input_session::PlatformTextInputState) -> R,
540) -> R {
541 let context = require_current_app_context("platform text input session access");
542 f(&context.text_input_session)
543}
544
545pub(crate) fn with_clipboard_session<R>(
546 f: impl FnOnce(&crate::clipboard_session::ClipboardSessionState) -> R,
547) -> R {
548 let context = require_current_app_context("clipboard session access");
549 f(&context.clipboard_session)
550}
551
552pub(crate) fn register_pointer_input_task(
553 task_id: u64,
554 task: Rc<crate::modifier::pointer_input::PointerInputTaskInner>,
555) -> crate::modifier::pointer_input::PointerInputTaskOwner {
556 let context = require_current_app_context("pointer input task registration");
557 context.pointer_input_tasks.insert(task_id, task);
558 crate::modifier::pointer_input::PointerInputTaskOwner::App(context.id)
559}
560
561pub(crate) fn remove_pointer_input_task(
562 owner: crate::modifier::pointer_input::PointerInputTaskOwner,
563 task_id: u64,
564) {
565 match owner {
566 crate::modifier::pointer_input::PointerInputTaskOwner::App(context_id) => {
567 let _ = with_app_context_by_id(context_id, |context| {
568 context.pointer_input_tasks.remove(task_id);
569 });
570 }
571 }
572}
573
574pub(crate) fn request_pointer_input_task_poll(
575 owner: crate::modifier::pointer_input::PointerInputTaskOwner,
576 task_id: u64,
577) {
578 match owner {
579 crate::modifier::pointer_input::PointerInputTaskOwner::App(context_id) => {
580 let _ = with_app_context_by_id(context_id, |context| {
581 context.enter(|| {
582 context.pointer_input_tasks.request_poll(task_id, owner);
583 });
584 });
585 }
586 }
587}
588
589fn with_draw_observer<R>(f: impl FnOnce(&SnapshotStateObserver) -> R) -> R {
590 let context = require_current_app_context("draw observer access");
591 f(&context.draw_observer)
592}
593
594struct LayoutRepassManager {
595 dirty_nodes: HashSet<NodeId>,
596}
597
598impl LayoutRepassManager {
599 fn new() -> Self {
600 Self {
601 dirty_nodes: HashSet::new(),
602 }
603 }
604
605 fn schedule_repass(&mut self, node_id: NodeId) {
606 self.dirty_nodes.insert(node_id);
607 }
608
609 fn has_pending_repass(&self) -> bool {
610 !self.dirty_nodes.is_empty()
611 }
612
613 fn take_dirty_nodes(&mut self) -> Vec<NodeId> {
614 self.dirty_nodes.drain().collect()
615 }
616
617 fn dirty_nodes_snapshot(&self) -> Vec<NodeId> {
618 let mut nodes = self.dirty_nodes.iter().copied().collect::<Vec<_>>();
619 nodes.sort_unstable();
620 nodes
621 }
622}
623
624struct DrawRepassManager {
625 dirty_nodes: HashSet<NodeId>,
626}
627
628impl DrawRepassManager {
629 fn new() -> Self {
630 Self {
631 dirty_nodes: HashSet::new(),
632 }
633 }
634
635 fn schedule_repass(&mut self, node_id: NodeId) {
636 self.dirty_nodes.insert(node_id);
637 }
638
639 fn has_pending_repass(&self) -> bool {
640 !self.dirty_nodes.is_empty()
641 }
642
643 fn take_dirty_nodes(&mut self) -> Vec<NodeId> {
644 self.dirty_nodes.drain().collect()
645 }
646}
647
648fn lock_repass_manager<T>(manager: &Mutex<T>) -> MutexGuard<'_, T> {
649 manager
650 .lock()
651 .unwrap_or_else(|poisoned| poisoned.into_inner())
652}
653
654#[track_caller]
673pub fn schedule_layout_repass(node_id: NodeId) {
674 if layout_repass_schedule_diagnostics_enabled_for(node_id) {
675 let caller = std::panic::Location::caller();
676 log::warn!(
677 "[layout-repass-schedule] node={} caller={}:{}:{}",
678 node_id,
679 caller.file(),
680 caller.line(),
681 caller.column()
682 );
683 }
684 with_render_state(|state| {
685 lock_repass_manager(&state.layout_repasses).schedule_repass(node_id);
686 state.layout_invalidated.store(true, Ordering::Relaxed);
687 });
688 request_render_invalidation();
689}
690
691#[derive(Clone, Copy)]
692enum LayoutRepassScheduleDiag {
693 Disabled,
694 All,
695 Node(NodeId),
696}
697
698fn layout_repass_schedule_diagnostics_enabled_for(node_id: NodeId) -> bool {
699 static MODE: std::sync::OnceLock<LayoutRepassScheduleDiag> = std::sync::OnceLock::new();
700 match *MODE.get_or_init(|| {
701 let Some(value) = std::env::var_os("CRANPOSE_LAYOUT_REPASS_SCHEDULE_DIAG") else {
702 return LayoutRepassScheduleDiag::Disabled;
703 };
704 if value == "all" {
705 return LayoutRepassScheduleDiag::All;
706 }
707 value
708 .to_string_lossy()
709 .parse::<NodeId>()
710 .map(LayoutRepassScheduleDiag::Node)
711 .unwrap_or(LayoutRepassScheduleDiag::Disabled)
712 }) {
713 LayoutRepassScheduleDiag::Disabled => false,
714 LayoutRepassScheduleDiag::All => true,
715 LayoutRepassScheduleDiag::Node(target) => target == node_id,
716 }
717}
718
719pub(crate) fn schedule_modifier_slices_repass(node_id: NodeId) {
720 with_render_state(|state| {
721 lock_repass_manager(&state.modifier_slice_repasses).schedule_repass(node_id);
722 });
723 schedule_draw_repass(node_id);
724}
725
726pub fn schedule_draw_repass(node_id: NodeId) {
731 let context = require_current_app_context("render state access");
732 schedule_draw_repass_in_context(&context, node_id);
733}
734
735fn schedule_draw_repass_for_app_context(context_id: AppContextId, node_id: NodeId) {
736 let _ = with_app_context_by_id(context_id, |context| {
737 schedule_draw_repass_in_context(context, node_id);
738 });
739}
740
741fn schedule_draw_repass_in_context(context: &AppContext, node_id: NodeId) {
742 lock_repass_manager(&context.state.draw_repasses).schedule_repass(node_id);
743 context
744 .state
745 .render_invalidated
746 .store(true, Ordering::Relaxed);
747}
748
749pub fn has_pending_draw_repasses() -> bool {
751 with_render_state(|state| lock_repass_manager(&state.draw_repasses).has_pending_repass())
752}
753
754pub fn take_draw_repass_nodes() -> Vec<NodeId> {
756 with_render_state(|state| lock_repass_manager(&state.draw_repasses).take_dirty_nodes())
757}
758
759pub fn has_pending_layout_repasses() -> bool {
761 with_render_state(|state| lock_repass_manager(&state.layout_repasses).has_pending_repass())
762}
763
764pub fn pending_layout_repass_nodes_snapshot() -> Vec<NodeId> {
766 with_render_state(|state| lock_repass_manager(&state.layout_repasses).dirty_nodes_snapshot())
767}
768
769pub fn take_layout_repass_nodes() -> Vec<NodeId> {
773 with_render_state(|state| lock_repass_manager(&state.layout_repasses).take_dirty_nodes())
774}
775
776pub fn schedule_measure_repass(node_id: NodeId) {
785 with_render_state(|state| {
786 lock_repass_manager(&state.measure_repasses).schedule_repass(node_id);
787 state.layout_invalidated.store(true, Ordering::Relaxed);
788 });
789 request_render_invalidation();
790}
791
792pub fn has_pending_measure_repasses() -> bool {
794 with_render_state(|state| lock_repass_manager(&state.measure_repasses).has_pending_repass())
795}
796
797pub fn pending_measure_repass_nodes_snapshot() -> Vec<NodeId> {
806 with_render_state(|state| lock_repass_manager(&state.measure_repasses).dirty_nodes_snapshot())
807}
808
809pub fn take_measure_repass_nodes() -> Vec<NodeId> {
813 with_render_state(|state| lock_repass_manager(&state.measure_repasses).take_dirty_nodes())
814}
815
816pub(crate) fn take_modifier_slice_repass_nodes() -> Vec<NodeId> {
817 with_render_state(|state| {
818 lock_repass_manager(&state.modifier_slice_repasses).take_dirty_nodes()
819 })
820}
821
822pub(crate) fn record_geometry_scene_node(node_id: NodeId) {
823 with_render_state(|state| {
824 lock_repass_manager(&state.geometry_scene_nodes).schedule_repass(node_id);
825 });
826}
827
828pub fn take_geometry_scene_nodes() -> Vec<NodeId> {
834 with_render_state(|state| lock_repass_manager(&state.geometry_scene_nodes).take_dirty_nodes())
835}
836
837pub fn current_density() -> f32 {
839 with_render_state(|state| f32::from_bits(state.density_bits.load(Ordering::Relaxed)))
840}
841
842pub fn set_density(density: f32) {
847 let normalized = normalize_density(density);
848 let new_bits = normalized.to_bits();
849 with_render_state(|state| {
850 let old_bits = state.density_bits.swap(new_bits, Ordering::Relaxed);
851 if old_bits != new_bits {
852 state.layout_invalidated.store(true, Ordering::Relaxed);
853 }
854 });
855}
856
857pub fn current_font_scale() -> f32 {
868 current_font_scale_curve().scale()
869}
870
871pub fn current_font_scale_curve() -> crate::font_scale::FontScaleCurve {
877 with_render_state(|state| *lock_font_scale(&state.font_scale))
878}
879
880pub fn scale_sp(sp: f32) -> f32 {
882 current_font_scale_curve().sp_to_dp(sp)
883}
884
885pub fn set_font_scale(scale: f32) {
895 set_font_scale_curve(crate::font_scale::FontScaleCurve::linear(scale));
896}
897
898pub fn set_font_scale_curve(curve: crate::font_scale::FontScaleCurve) {
906 let normalized = normalize_font_scale(curve.scale());
907 let curve = if (normalized - curve.scale()).abs() <= f32::EPSILON {
908 curve
909 } else {
910 crate::font_scale::FontScaleCurve::linear(normalized)
911 };
912 with_render_state(|state| {
913 let mut current = lock_font_scale(&state.font_scale);
914 if *current != curve {
915 *current = curve;
916 state.layout_invalidated.store(true, Ordering::Relaxed);
917 }
918 });
919}
920
921fn lock_font_scale(
922 slot: &Mutex<crate::font_scale::FontScaleCurve>,
923) -> MutexGuard<'_, crate::font_scale::FontScaleCurve> {
924 match slot.lock() {
925 Ok(guard) => guard,
926 Err(poisoned) => poisoned.into_inner(),
927 }
928}
929
930pub fn request_render_invalidation() {
932 with_render_state(|state| state.render_invalidated.store(true, Ordering::Relaxed));
933}
934
935pub fn take_render_invalidation() -> bool {
937 with_render_state(|state| state.render_invalidated.swap(false, Ordering::Relaxed))
938}
939
940pub fn peek_render_invalidation() -> bool {
942 with_render_state(|state| state.render_invalidated.load(Ordering::Relaxed))
943}
944
945pub fn request_pointer_invalidation() {
947 with_render_state(|state| state.pointer_invalidated.store(true, Ordering::Relaxed));
948}
949
950pub fn take_pointer_invalidation() -> bool {
952 with_render_state(|state| state.pointer_invalidated.swap(false, Ordering::Relaxed))
953}
954
955pub fn peek_pointer_invalidation() -> bool {
957 with_render_state(|state| state.pointer_invalidated.load(Ordering::Relaxed))
958}
959
960pub fn request_focus_invalidation() {
962 with_render_state(|state| state.focus_invalidated.store(true, Ordering::Relaxed));
963}
964
965pub fn take_focus_invalidation() -> bool {
967 with_render_state(|state| state.focus_invalidated.swap(false, Ordering::Relaxed))
968}
969
970pub fn peek_focus_invalidation() -> bool {
972 with_render_state(|state| state.focus_invalidated.load(Ordering::Relaxed))
973}
974
975pub fn request_layout_invalidation() {
1002 with_render_state(|state| state.layout_invalidated.store(true, Ordering::Relaxed));
1003}
1004
1005pub fn take_layout_invalidation() -> bool {
1007 with_render_state(|state| state.layout_invalidated.swap(false, Ordering::Relaxed))
1008}
1009
1010pub fn peek_layout_invalidation() -> bool {
1012 with_render_state(|state| state.layout_invalidated.load(Ordering::Relaxed))
1013}
1014
1015#[cfg(any(test, feature = "test-helpers"))]
1016#[doc(hidden)]
1017pub fn reset_render_state_for_tests() {
1018 let _ = take_draw_repass_nodes();
1019 let _ = take_layout_repass_nodes();
1020 let _ = take_modifier_slice_repass_nodes();
1021 let _ = take_render_invalidation();
1022 let _ = take_pointer_invalidation();
1023 let _ = take_focus_invalidation();
1024 let _ = take_layout_invalidation();
1025 debug_reset_last_fling_velocity();
1026 set_density(1.0);
1027 set_font_scale(1.0);
1028 let _ = take_layout_invalidation();
1029}
1030
1031#[cfg(test)]
1032pub(crate) struct TestAppContextScope {
1033 _scope: AppContextScope,
1034 _context: Rc<AppContext>,
1035}
1036
1037#[cfg(test)]
1038pub(crate) fn app_context_test_scope() -> TestAppContextScope {
1039 let context = AppContext::new();
1040 let scope = context.enter_scope();
1041 context.enter(reset_render_state_for_tests);
1042 TestAppContextScope {
1043 _scope: scope,
1044 _context: context,
1045 }
1046}
1047
1048#[cfg(test)]
1049pub(crate) struct RenderStateTestGuard {
1050 _app_scope: TestAppContextScope,
1051 _lock: std::sync::MutexGuard<'static, ()>,
1052}
1053
1054#[cfg(test)]
1055pub(crate) fn render_state_test_guard() -> RenderStateTestGuard {
1056 static TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
1057 let lock = match TEST_LOCK.get_or_init(|| Mutex::new(())).lock() {
1058 Ok(guard) => guard,
1059 Err(poisoned) => poisoned.into_inner(),
1060 };
1061 RenderStateTestGuard {
1062 _app_scope: app_context_test_scope(),
1063 _lock: lock,
1064 }
1065}
1066
1067#[cfg(test)]
1068mod tests {
1069 use std::sync::{Arc, mpsc};
1070
1071 use super::*;
1072 use crate::text::{AnnotatedString, TextLayoutResult, TextMeasurer, TextMetrics, TextStyle};
1073
1074 #[test]
1075 fn a_draw_closure_can_name_its_own_node_for_the_next_frame() {
1076 let _scope = app_context_test_scope();
1077 let _ = take_draw_repass_nodes();
1078 let _ = take_render_invalidation();
1079
1080 observe_draw_reads(DrawObservationScope::new(33, 0), || {
1081 request_current_draw_redraw();
1082 });
1083 assert_eq!(
1084 take_draw_repass_nodes(),
1085 vec![33],
1086 "the recording node must be scheduled for re-record"
1087 );
1088 assert!(
1089 take_render_invalidation(),
1090 "the next frame must be requested"
1091 );
1092
1093 request_current_draw_redraw();
1094 assert!(take_draw_repass_nodes().is_empty());
1095 assert!(take_render_invalidation());
1096 }
1097
1098 struct TestTextMeasurer;
1099
1100 impl TextMeasurer for TestTextMeasurer {
1101 fn measure(&self, text: &AnnotatedString, _style: &TextStyle) -> TextMetrics {
1102 TextMetrics {
1103 width: text.text.len() as f32,
1104 height: 1.0,
1105 line_height: 1.0,
1106 line_count: 1,
1107 }
1108 }
1109
1110 fn get_offset_for_position(
1111 &self,
1112 text: &AnnotatedString,
1113 _style: &TextStyle,
1114 x: f32,
1115 _y: f32,
1116 ) -> usize {
1117 x.round().max(0.0) as usize % text.text.len().max(1)
1118 }
1119
1120 fn get_cursor_x_for_offset(
1121 &self,
1122 _text: &AnnotatedString,
1123 _style: &TextStyle,
1124 offset: usize,
1125 ) -> f32 {
1126 offset as f32
1127 }
1128
1129 fn layout(&self, text: &AnnotatedString, _style: &TextStyle) -> TextLayoutResult {
1130 TextLayoutResult::monospaced(&text.text, 1.0, 1.0)
1131 }
1132 }
1133
1134 #[test]
1135 fn app_context_ids_do_not_use_process_global_counter() {
1136 let source = include_str!("render_state.rs");
1137 assert!(!source.contains(concat!("NEXT_", "APP_CONTEXT_ID: Atomic")));
1138 }
1139
1140 #[test]
1141 fn app_context_ids_are_unique_within_thread_registry() {
1142 let first = AppContext::new();
1143 let second = AppContext::new();
1144
1145 assert_ne!(first.id, second.id);
1146 assert!(app_context_by_id(first.id).is_some());
1147 assert!(app_context_by_id(second.id).is_some());
1148 }
1149
1150 #[test]
1151 fn set_text_measurer_requires_active_app_context() {
1152 let result = std::panic::catch_unwind(|| {
1153 crate::text::set_text_measurer(TestTextMeasurer);
1154 });
1155 assert!(result.is_err());
1156
1157 let context = AppContext::new();
1158 context.enter(|| {
1159 crate::text::set_text_measurer(TestTextMeasurer);
1160 });
1161 }
1162
1163 #[test]
1164 fn the_font_scale_starts_at_one_and_invalidates_layout_when_it_moves() {
1165 let context = AppContext::new();
1166 context.enter(|| {
1167 assert_eq!(current_font_scale(), 1.0);
1168 let _ = take_layout_invalidation();
1169
1170 set_font_scale(1.3);
1171 assert_eq!(current_font_scale(), 1.3);
1172 assert!(
1173 take_layout_invalidation(),
1174 "every Sp on screen just changed size"
1175 );
1176
1177 set_font_scale(1.3);
1178 assert!(!take_layout_invalidation());
1179 });
1180 }
1181
1182 #[test]
1183 fn a_font_scale_no_platform_reports_is_refused() {
1184 let context = AppContext::new();
1185 context.enter(|| {
1186 for nonsense in [0.0, -1.0, f32::NAN, f32::INFINITY] {
1187 set_font_scale(1.0);
1188 set_font_scale(nonsense);
1189 assert_eq!(current_font_scale(), 1.0, "{nonsense} was let through");
1190 }
1191 set_font_scale(99.0);
1192 assert_eq!(current_font_scale(), MAX_FONT_SCALE);
1193 set_font_scale(0.01);
1194 assert_eq!(current_font_scale(), MIN_FONT_SCALE);
1195 });
1196 }
1197
1198 #[test]
1199 fn the_font_scale_is_per_app_context() {
1200 let first = AppContext::new();
1201 let second = AppContext::new();
1202 first.enter(|| set_font_scale(1.5));
1203 first.enter(|| assert_eq!(current_font_scale(), 1.5));
1204 second.enter(|| assert_eq!(current_font_scale(), 1.0));
1205 }
1206
1207 #[test]
1208 fn invalidation_flags_are_shared_across_threads() {
1209 let state = Arc::new(RenderState::new_with_density(1.0));
1210 let (tx, rx) = mpsc::channel();
1211 let worker_state = Arc::clone(&state);
1212
1213 let handle = std::thread::spawn(move || {
1214 worker_state
1215 .render_invalidated
1216 .store(true, Ordering::Relaxed);
1217 worker_state
1218 .pointer_invalidated
1219 .store(true, Ordering::Relaxed);
1220 worker_state
1221 .focus_invalidated
1222 .store(true, Ordering::Relaxed);
1223 worker_state
1224 .layout_invalidated
1225 .store(true, Ordering::Relaxed);
1226 worker_state
1227 .density_bits
1228 .store(f32::to_bits(2.0), Ordering::Relaxed);
1229 tx.send(()).expect("signal invalidation setup");
1230
1231 f32::from_bits(worker_state.density_bits.load(Ordering::Relaxed))
1232 });
1233
1234 rx.recv().expect("wait for worker invalidation setup");
1235 assert!(state.render_invalidated.load(Ordering::Relaxed));
1236 assert!(state.pointer_invalidated.load(Ordering::Relaxed));
1237 assert!(state.focus_invalidated.load(Ordering::Relaxed));
1238 assert!(state.layout_invalidated.load(Ordering::Relaxed));
1239 assert_eq!(
1240 f32::from_bits(state.density_bits.load(Ordering::Relaxed)),
1241 2.0
1242 );
1243 assert!(state.render_invalidated.swap(false, Ordering::Relaxed));
1244 assert!(state.pointer_invalidated.swap(false, Ordering::Relaxed));
1245 assert!(state.focus_invalidated.swap(false, Ordering::Relaxed));
1246 assert!(state.layout_invalidated.swap(false, Ordering::Relaxed));
1247
1248 let density = handle.join().expect("worker invalidation snapshot");
1249 assert_eq!(density, 2.0);
1250 assert!(!state.render_invalidated.load(Ordering::Relaxed));
1251 assert!(!state.pointer_invalidated.load(Ordering::Relaxed));
1252 assert!(!state.focus_invalidated.load(Ordering::Relaxed));
1253 assert!(!state.layout_invalidated.load(Ordering::Relaxed));
1254 }
1255
1256 #[test]
1257 fn app_contexts_keep_density_and_invalidations_isolated() {
1258 let first = AppContext::new_with_density(1.0);
1259 let second = AppContext::new_with_density(1.0);
1260
1261 first.enter(|| {
1262 set_density(2.0);
1263 request_render_invalidation();
1264 request_pointer_invalidation();
1265 schedule_layout_repass(11);
1266 schedule_draw_repass(12);
1267 });
1268
1269 second.enter(|| {
1270 assert_eq!(current_density(), 1.0);
1271 assert!(!peek_render_invalidation());
1272 assert!(!peek_pointer_invalidation());
1273 assert!(!peek_layout_invalidation());
1274 assert!(!has_pending_layout_repasses());
1275 assert!(!has_pending_draw_repasses());
1276 });
1277
1278 first.enter(|| {
1279 assert_eq!(current_density(), 2.0);
1280 assert!(peek_render_invalidation());
1281 assert!(peek_pointer_invalidation());
1282 assert!(peek_layout_invalidation());
1283 assert!(has_pending_layout_repasses());
1284 assert!(has_pending_draw_repasses());
1285 assert_eq!(take_layout_repass_nodes(), vec![11]);
1286 assert_eq!(take_draw_repass_nodes(), vec![12]);
1287 assert!(take_render_invalidation());
1288 assert!(take_pointer_invalidation());
1289 assert!(take_layout_invalidation());
1290 });
1291 }
1292
1293 #[test]
1294 fn app_contexts_keep_fling_velocity_diagnostics_isolated() {
1295 let first = AppContext::new_with_density(1.0);
1296 let second = AppContext::new_with_density(1.0);
1297
1298 first.enter(|| {
1299 record_last_fling_velocity(1200.0);
1300 assert_eq!(debug_last_fling_velocity(), 1200.0);
1301 });
1302
1303 second.enter(|| {
1304 assert_eq!(debug_last_fling_velocity(), 0.0);
1305 record_last_fling_velocity(-450.0);
1306 assert_eq!(debug_last_fling_velocity(), -450.0);
1307 });
1308
1309 first.enter(|| {
1310 assert_eq!(debug_last_fling_velocity(), 1200.0);
1311 debug_reset_last_fling_velocity();
1312 assert_eq!(debug_last_fling_velocity(), 0.0);
1313 });
1314
1315 second.enter(|| {
1316 assert_eq!(debug_last_fling_velocity(), -450.0);
1317 });
1318 }
1319
1320 #[test]
1321 fn app_context_new_uses_independent_density() {
1322 let outer = AppContext::new_with_density(2.0);
1323 let context = AppContext::new();
1324 context.enter(|| {
1325 assert_eq!(current_density(), 1.0);
1326 });
1327 outer.enter(|| {
1328 assert_eq!(current_density(), 2.0);
1329 });
1330 }
1331
1332 #[test]
1333 fn runtime_state_access_requires_explicit_app_context_even_in_tests() {
1334 let result = std::panic::catch_unwind(|| {
1335 request_render_invalidation();
1336 });
1337 assert!(result.is_err());
1338 }
1339
1340 #[test]
1341 fn app_contexts_keep_layout_frame_arenas_isolated() {
1342 let first = AppContext::new_with_density(1.0);
1343 let second = AppContext::new_with_density(1.0);
1344
1345 first.enter(|| {
1346 assert_eq!(layout_frame_arena_placement_scratch_count(), 0);
1347 let mut arena = take_layout_frame_arena();
1348 arena.seed_placement_scratch_for_test();
1349 replace_layout_frame_arena(arena);
1350 assert_eq!(layout_frame_arena_placement_scratch_count(), 1);
1351 });
1352
1353 second.enter(|| {
1354 assert_eq!(layout_frame_arena_placement_scratch_count(), 0);
1355 });
1356
1357 first.enter(|| {
1358 assert_eq!(layout_frame_arena_placement_scratch_count(), 1);
1359 });
1360 }
1361
1362 #[test]
1363 fn current_app_context_scope_does_not_extend_context_lifetime() {
1364 let weak = {
1365 let context = AppContext::new_with_density(1.0);
1366 let weak = Rc::downgrade(&context);
1367 context.enter(|| {
1368 assert!(current_app_context().is_some());
1369 });
1370 weak
1371 };
1372
1373 assert!(weak.upgrade().is_none());
1374 assert!(current_app_context().is_none());
1375 }
1376
1377 #[test]
1378 fn dropped_app_context_unregisters_from_thread_lookup_registry() {
1379 let start_count = app_context_registry_entry_count();
1380
1381 let id = {
1382 let context = AppContext::new_with_density(1.0);
1383 let id = context.id;
1384 assert!(app_context_by_id(id).is_some());
1385 id
1386 };
1387
1388 assert_eq!(
1389 app_context_registry_entry_count(),
1390 start_count,
1391 "dropped AppContexts must remove their weak registry entry"
1392 );
1393 assert!(app_context_by_id(id).is_none());
1394 }
1395}