1use cranpose_core::{current_runtime_handle, NodeId, SnapshotStateObserver};
2use std::cell::{Cell, RefCell};
3use std::collections::{HashMap, HashSet};
4use std::rc::{Rc, Weak};
5use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
6#[cfg(test)]
7use std::sync::OnceLock;
8use std::sync::{Arc, Mutex, MutexGuard};
9
10pub(crate) type ModifierChainTraceCallback =
11 dyn Fn(&[crate::modifier::ModifierChainInspectorNode]) + Send + Sync + 'static;
12
13struct RenderState {
14 layout_repasses: Mutex<LayoutRepassManager>,
15 draw_repasses: Mutex<DrawRepassManager>,
16 modifier_slice_repasses: Mutex<LayoutRepassManager>,
17 render_invalidated: AtomicBool,
18 pointer_invalidated: AtomicBool,
19 focus_invalidated: AtomicBool,
20 layout_invalidated: AtomicBool,
21 density_bits: AtomicU32,
22}
23
24#[doc(hidden)]
25pub struct AppContext {
26 id: AppContextId,
27 self_weak: RefCell<Weak<AppContext>>,
28 state: RenderState,
29 draw_observer: SnapshotStateObserver,
30 text: crate::text::measure::TextService,
31 layout_frame_arena: RefCell<crate::layout::FrameLayoutArena>,
32 layout_cache_epoch: AtomicU64,
33 last_fling_velocity_bits: AtomicU32,
34 scroll_motion_contexts: crate::scroll::ScrollMotionContextStore,
35 layout_node_registry: crate::widgets::nodes::layout_node::LayoutNodeRegistryState,
36 pointer_dispatch: crate::pointer_dispatch::PointerDispatchState,
37 focus_dispatch: crate::focus_dispatch::FocusInvalidationState,
38 cursor_animation: crate::cursor_animation::CursorAnimationState,
39 text_field_focus: crate::text_field_focus::TextFieldFocusState,
40 pointer_input_tasks: crate::modifier::pointer_input::PointerInputTaskRegistry,
41 modifier_chain_trace: RefCell<Option<Arc<ModifierChainTraceCallback>>>,
42}
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
45pub(crate) struct AppContextId(u64);
46
47#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
48pub(crate) struct DrawObservationScope {
49 node_id: NodeId,
50 command_index: usize,
51}
52
53impl DrawObservationScope {
54 pub(crate) fn new(node_id: NodeId, command_index: usize) -> Self {
55 Self {
56 node_id,
57 command_index,
58 }
59 }
60}
61
62fn new_draw_observer() -> SnapshotStateObserver {
63 let observer = SnapshotStateObserver::new(|callback| {
64 if let Some(runtime) = current_runtime_handle() {
65 runtime.enqueue_ui_task(callback);
66 } else {
67 callback();
68 }
69 });
70 observer.start();
71 observer
72}
73
74pub(crate) fn observe_draw_reads<R>(scope: DrawObservationScope, block: impl FnOnce() -> R) -> R {
75 let context = require_current_app_context("draw observer access");
76 let context_id = context.id;
77 context.draw_observer.observe_reads(
78 scope,
79 move |scope| {
80 schedule_draw_repass_for_app_context(context_id, scope.node_id);
81 },
82 block,
83 )
84}
85
86pub(crate) fn clear_draw_observations_for_node(node_id: NodeId) {
87 with_draw_observer(|observer| {
88 observer.clear_if(|scope| {
89 scope
90 .downcast_ref::<DrawObservationScope>()
91 .is_some_and(|scope| scope.node_id == node_id)
92 });
93 });
94}
95
96impl RenderState {
97 fn new_with_density(density: f32) -> Self {
98 Self {
99 layout_repasses: Mutex::new(LayoutRepassManager::new()),
100 draw_repasses: Mutex::new(DrawRepassManager::new()),
101 modifier_slice_repasses: Mutex::new(LayoutRepassManager::new()),
102 render_invalidated: AtomicBool::new(false),
103 pointer_invalidated: AtomicBool::new(false),
104 focus_invalidated: AtomicBool::new(false),
105 layout_invalidated: AtomicBool::new(false),
106 density_bits: AtomicU32::new(normalize_density(density).to_bits()),
107 }
108 }
109}
110
111std::thread_local! {
112 static NEXT_APP_CONTEXT_ID: Cell<u64> = const { Cell::new(1) };
113 static CURRENT_APP_CONTEXT: RefCell<Vec<Weak<AppContext>>> = const { RefCell::new(Vec::new()) };
114 static APP_CONTEXTS: RefCell<HashMap<AppContextId, Weak<AppContext>>> = RefCell::new(HashMap::new());
115}
116
117fn next_app_context_id() -> AppContextId {
118 NEXT_APP_CONTEXT_ID.with(|next| {
119 let id = next.get();
120 next.set(id.wrapping_add(1));
121 AppContextId(id)
122 })
123}
124
125#[doc(hidden)]
126pub struct AppContextScope;
127
128impl Drop for AppContextScope {
129 fn drop(&mut self) {
130 CURRENT_APP_CONTEXT.with(|stack| {
131 stack.borrow_mut().pop();
132 });
133 }
134}
135
136impl AppContext {
137 pub fn new() -> Rc<Self> {
138 Self::new_with_density(1.0)
139 }
140
141 pub fn new_with_density(density: f32) -> Rc<Self> {
142 let context = Rc::new(Self {
143 id: next_app_context_id(),
144 self_weak: RefCell::new(Weak::new()),
145 state: RenderState::new_with_density(density),
146 draw_observer: new_draw_observer(),
147 text: crate::text::measure::TextService::new(),
148 layout_frame_arena: RefCell::new(crate::layout::FrameLayoutArena::default()),
149 layout_cache_epoch: AtomicU64::new(1),
150 last_fling_velocity_bits: AtomicU32::new(0.0f32.to_bits()),
151 scroll_motion_contexts: crate::scroll::ScrollMotionContextStore::new(),
152 layout_node_registry: crate::widgets::nodes::layout_node::LayoutNodeRegistryState::new(
153 ),
154 pointer_dispatch: crate::pointer_dispatch::PointerDispatchState::new(),
155 focus_dispatch: crate::focus_dispatch::FocusInvalidationState::new(),
156 cursor_animation: crate::cursor_animation::CursorAnimationState::new(),
157 text_field_focus: crate::text_field_focus::TextFieldFocusState::new(),
158 pointer_input_tasks: crate::modifier::pointer_input::PointerInputTaskRegistry::new(),
159 modifier_chain_trace: RefCell::new(None),
160 });
161 *context.self_weak.borrow_mut() = Rc::downgrade(&context);
162 APP_CONTEXTS.with(|contexts| {
163 contexts
164 .borrow_mut()
165 .insert(context.id, Rc::downgrade(&context));
166 });
167 context
168 }
169
170 pub fn enter<R>(self: &Rc<Self>, block: impl FnOnce() -> R) -> R {
171 let _scope = self.enter_scope();
172 block()
173 }
174
175 #[doc(hidden)]
176 pub fn enter_scope(self: &Rc<Self>) -> AppContextScope {
177 CURRENT_APP_CONTEXT.with(|stack| {
178 stack.borrow_mut().push(Rc::downgrade(self));
179 });
180 AppContextScope
181 }
182
183 pub fn set_text_measurer<M: crate::text::TextMeasurer>(&self, measurer: M) {
184 self.set_text_measurer_rc(Rc::new(measurer));
185 }
186
187 pub fn set_text_measurer_rc(&self, measurer: Rc<dyn crate::text::TextMeasurer>) {
188 self.text.set_measurer(measurer);
189 self.layout_cache_epoch.fetch_add(1, Ordering::Relaxed);
190 self.state.layout_invalidated.store(true, Ordering::Relaxed);
191 self.state.render_invalidated.store(true, Ordering::Relaxed);
192 }
193
194 #[doc(hidden)]
195 pub fn downgrade(&self) -> Weak<Self> {
196 self.self_weak.borrow().clone()
197 }
198}
199
200impl Drop for AppContext {
201 fn drop(&mut self) {
202 let id = self.id;
203 let _ = APP_CONTEXTS.try_with(|contexts| {
204 contexts.borrow_mut().remove(&id);
205 });
206 }
207}
208
209fn app_context_by_id(id: AppContextId) -> Option<Rc<AppContext>> {
210 APP_CONTEXTS
211 .try_with(|contexts| {
212 let context = contexts.borrow().get(&id).cloned()?;
213 let Some(context) = context.upgrade() else {
214 contexts.borrow_mut().remove(&id);
215 return None;
216 };
217 Some(context)
218 })
219 .ok()
220 .flatten()
221}
222
223#[cfg(test)]
224fn app_context_registry_entry_count() -> usize {
225 APP_CONTEXTS
226 .try_with(|contexts| contexts.borrow().len())
227 .unwrap_or_default()
228}
229
230fn with_app_context_by_id<R>(id: AppContextId, f: impl FnOnce(&Rc<AppContext>) -> R) -> Option<R> {
231 app_context_by_id(id).map(|context| f(&context))
232}
233
234pub(crate) fn current_app_context_id() -> AppContextId {
235 require_current_app_context("app context identity access").id
236}
237
238pub(crate) fn with_layout_node_registry_by_app_context<R>(
239 id: AppContextId,
240 f: impl FnOnce(&crate::widgets::nodes::layout_node::LayoutNodeRegistryState) -> R,
241) -> Option<R> {
242 with_app_context_by_id(id, |context| f(&context.layout_node_registry))
243}
244
245pub(crate) fn enter_app_context_by_id<R>(id: AppContextId, f: impl FnOnce() -> R) -> Option<R> {
246 with_app_context_by_id(id, |context| context.enter(f))
247}
248
249fn current_app_context() -> Option<Rc<AppContext>> {
250 CURRENT_APP_CONTEXT
251 .try_with(|stack| {
252 let mut stack = stack.borrow_mut();
253 loop {
254 let context = stack.last()?;
255 if let Some(context) = context.upgrade() {
256 return Some(context);
257 }
258 stack.pop();
259 }
260 })
261 .ok()
262 .flatten()
263}
264
265#[doc(hidden)]
266pub fn has_current_app_context() -> bool {
267 current_app_context().is_some()
268}
269
270fn require_current_app_context(operation: &str) -> Rc<AppContext> {
271 if let Some(context) = current_app_context() {
272 return context;
273 }
274 require_current_app_context_without_scope(operation)
275}
276
277fn require_current_app_context_without_scope(operation: &str) -> Rc<AppContext> {
278 panic!("{operation} requires an active AppContext")
279}
280
281fn with_render_state<R>(f: impl FnOnce(&RenderState) -> R) -> R {
282 let context = require_current_app_context("render state access");
283 f(&context.state)
284}
285
286fn normalize_density(density: f32) -> f32 {
287 if density.is_finite() && density > 0.0 {
288 density
289 } else {
290 1.0
291 }
292}
293
294pub(crate) fn with_text_measurer<R>(f: impl FnOnce(&dyn crate::text::TextMeasurer) -> R) -> R {
295 let context = require_current_app_context("text measurer access");
296 context.text.with_measurer(f)
297}
298
299pub(crate) fn with_text_service<R>(f: impl FnOnce(&crate::text::measure::TextService) -> R) -> R {
300 let context = require_current_app_context("text service access");
301 f(&context.text)
302}
303
304pub(crate) fn set_current_text_measurer(measurer: Rc<dyn crate::text::TextMeasurer>) {
305 let Some(context) = current_app_context() else {
306 panic!("set_text_measurer requires an active AppContext");
307 };
308 context.set_text_measurer_rc(measurer);
309}
310
311pub(crate) fn set_modifier_chain_trace(callback: Arc<ModifierChainTraceCallback>) -> AppContextId {
312 let context = require_current_app_context("modifier chain trace installation");
313 *context.modifier_chain_trace.borrow_mut() = Some(callback);
314 context.id
315}
316
317pub(crate) fn clear_modifier_chain_trace(context_id: AppContextId) {
318 let _ = with_app_context_by_id(context_id, |context| {
319 *context.modifier_chain_trace.borrow_mut() = None;
320 });
321}
322
323pub(crate) fn emit_modifier_chain_trace(nodes: &[crate::modifier::ModifierChainInspectorNode]) {
324 let Some(context) = current_app_context() else {
325 return;
326 };
327 let callback = context.modifier_chain_trace.borrow().clone();
328 if let Some(callback) = callback {
329 callback(nodes);
330 }
331}
332
333pub(crate) fn take_layout_frame_arena() -> crate::layout::FrameLayoutArena {
334 let context = require_current_app_context("layout frame arena access");
335 let arena = std::mem::take(&mut *context.layout_frame_arena.borrow_mut());
336 arena
337}
338
339pub(crate) fn replace_layout_frame_arena(arena: crate::layout::FrameLayoutArena) {
340 let context = require_current_app_context("layout frame arena access");
341 *context.layout_frame_arena.borrow_mut() = arena;
342}
343
344pub(crate) fn invalidate_layout_cache_epoch() {
345 let context = require_current_app_context("layout cache epoch access");
346 context.layout_cache_epoch.fetch_add(1, Ordering::Relaxed);
347}
348
349pub(crate) fn next_layout_cache_epoch() -> u64 {
350 let context = require_current_app_context("layout cache epoch access");
351 context.layout_cache_epoch.fetch_add(1, Ordering::Relaxed)
352}
353
354pub(crate) fn current_layout_cache_epoch() -> u64 {
355 let context = require_current_app_context("layout cache epoch access");
356 context.layout_cache_epoch.load(Ordering::Relaxed)
357}
358
359pub(crate) fn record_last_fling_velocity(velocity: f32) {
360 if let Some(context) = current_app_context() {
361 context
362 .last_fling_velocity_bits
363 .store(velocity.to_bits(), Ordering::Relaxed);
364 }
365}
366
367#[doc(hidden)]
368pub fn debug_last_fling_velocity() -> f32 {
369 let context = require_current_app_context("fling velocity diagnostics access");
370 f32::from_bits(context.last_fling_velocity_bits.load(Ordering::Relaxed))
371}
372
373#[doc(hidden)]
374pub fn debug_reset_last_fling_velocity() {
375 let context = require_current_app_context("fling velocity diagnostics access");
376 context
377 .last_fling_velocity_bits
378 .store(0.0f32.to_bits(), Ordering::Relaxed);
379}
380
381pub(crate) fn with_scroll_motion_context_store<R>(
382 f: impl FnOnce(&crate::scroll::ScrollMotionContextStore) -> R,
383) -> R {
384 let context = require_current_app_context("scroll motion context access");
385 f(&context.scroll_motion_contexts)
386}
387
388#[doc(hidden)]
389pub fn clear_transient_scroll_motion_contexts() {
390 let Some(context) = current_app_context() else {
391 return;
392 };
393 context.scroll_motion_contexts.clear_transient_after_frame();
394}
395
396#[cfg(test)]
397pub(crate) fn layout_frame_arena_placement_scratch_count() -> usize {
398 let context = require_current_app_context("layout frame arena access");
399 let count = context
400 .layout_frame_arena
401 .borrow()
402 .available_placement_scratch_count();
403 count
404}
405
406pub(crate) fn with_layout_node_registry<R>(
407 f: impl FnOnce(&crate::widgets::nodes::layout_node::LayoutNodeRegistryState) -> R,
408) -> R {
409 let context = require_current_app_context("layout node registry access");
410 f(&context.layout_node_registry)
411}
412
413pub(crate) fn with_pointer_dispatch<R>(
414 f: impl FnOnce(&crate::pointer_dispatch::PointerDispatchState) -> R,
415) -> R {
416 let context = require_current_app_context("pointer dispatch access");
417 f(&context.pointer_dispatch)
418}
419
420pub(crate) fn with_focus_dispatch<R>(
421 f: impl FnOnce(&crate::focus_dispatch::FocusInvalidationState) -> R,
422) -> R {
423 let context = require_current_app_context("focus dispatch access");
424 f(&context.focus_dispatch)
425}
426
427pub(crate) fn with_cursor_animation<R>(
428 f: impl FnOnce(&crate::cursor_animation::CursorAnimationState) -> R,
429) -> R {
430 let context = require_current_app_context("cursor animation access");
431 f(&context.cursor_animation)
432}
433
434pub(crate) fn with_text_field_focus<R>(
435 f: impl FnOnce(&crate::text_field_focus::TextFieldFocusState) -> R,
436) -> R {
437 let context = require_current_app_context("text field focus access");
438 f(&context.text_field_focus)
439}
440
441pub(crate) fn register_pointer_input_task(
442 task_id: u64,
443 task: Rc<crate::modifier::pointer_input::PointerInputTaskInner>,
444) -> crate::modifier::pointer_input::PointerInputTaskOwner {
445 let context = require_current_app_context("pointer input task registration");
446 context.pointer_input_tasks.insert(task_id, task);
447 crate::modifier::pointer_input::PointerInputTaskOwner::App(context.id)
448}
449
450pub(crate) fn remove_pointer_input_task(
451 owner: crate::modifier::pointer_input::PointerInputTaskOwner,
452 task_id: u64,
453) {
454 match owner {
455 crate::modifier::pointer_input::PointerInputTaskOwner::App(context_id) => {
456 let _ = with_app_context_by_id(context_id, |context| {
457 context.pointer_input_tasks.remove(task_id);
458 });
459 }
460 }
461}
462
463pub(crate) fn request_pointer_input_task_poll(
464 owner: crate::modifier::pointer_input::PointerInputTaskOwner,
465 task_id: u64,
466) {
467 match owner {
468 crate::modifier::pointer_input::PointerInputTaskOwner::App(context_id) => {
469 let _ = with_app_context_by_id(context_id, |context| {
470 context.enter(|| {
471 context.pointer_input_tasks.request_poll(task_id, owner);
472 });
473 });
474 }
475 }
476}
477
478fn with_draw_observer<R>(f: impl FnOnce(&SnapshotStateObserver) -> R) -> R {
479 let context = require_current_app_context("draw observer access");
480 f(&context.draw_observer)
481}
482
483struct LayoutRepassManager {
488 dirty_nodes: HashSet<NodeId>,
489}
490
491impl LayoutRepassManager {
492 fn new() -> Self {
493 Self {
494 dirty_nodes: HashSet::new(),
495 }
496 }
497
498 fn schedule_repass(&mut self, node_id: NodeId) {
499 self.dirty_nodes.insert(node_id);
500 }
501
502 fn has_pending_repass(&self) -> bool {
503 !self.dirty_nodes.is_empty()
504 }
505
506 fn take_dirty_nodes(&mut self) -> Vec<NodeId> {
507 self.dirty_nodes.drain().collect()
508 }
509
510 fn dirty_nodes_snapshot(&self) -> Vec<NodeId> {
511 let mut nodes = self.dirty_nodes.iter().copied().collect::<Vec<_>>();
512 nodes.sort_unstable();
513 nodes
514 }
515}
516
517struct DrawRepassManager {
519 dirty_nodes: HashSet<NodeId>,
520}
521
522impl DrawRepassManager {
523 fn new() -> Self {
524 Self {
525 dirty_nodes: HashSet::new(),
526 }
527 }
528
529 fn schedule_repass(&mut self, node_id: NodeId) {
530 self.dirty_nodes.insert(node_id);
531 }
532
533 fn has_pending_repass(&self) -> bool {
534 !self.dirty_nodes.is_empty()
535 }
536
537 fn take_dirty_nodes(&mut self) -> Vec<NodeId> {
538 self.dirty_nodes.drain().collect()
539 }
540}
541
542fn lock_repass_manager<T>(manager: &Mutex<T>) -> MutexGuard<'_, T> {
543 manager
544 .lock()
545 .unwrap_or_else(|poisoned| poisoned.into_inner())
546}
547
548#[track_caller]
567pub fn schedule_layout_repass(node_id: NodeId) {
568 if layout_repass_schedule_diagnostics_enabled_for(node_id) {
569 let caller = std::panic::Location::caller();
570 log::warn!(
571 "[layout-repass-schedule] node={} caller={}:{}:{}",
572 node_id,
573 caller.file(),
574 caller.line(),
575 caller.column()
576 );
577 }
578 with_render_state(|state| {
579 lock_repass_manager(&state.layout_repasses).schedule_repass(node_id);
580 state.layout_invalidated.store(true, Ordering::Relaxed);
581 });
582 request_render_invalidation();
589}
590
591#[derive(Clone, Copy)]
592enum LayoutRepassScheduleDiag {
593 Disabled,
594 All,
595 Node(NodeId),
596}
597
598fn layout_repass_schedule_diagnostics_enabled_for(node_id: NodeId) -> bool {
599 static MODE: std::sync::OnceLock<LayoutRepassScheduleDiag> = std::sync::OnceLock::new();
600 match *MODE.get_or_init(|| {
601 let Some(value) = std::env::var_os("CRANPOSE_LAYOUT_REPASS_SCHEDULE_DIAG") else {
602 return LayoutRepassScheduleDiag::Disabled;
603 };
604 if value == "all" {
605 return LayoutRepassScheduleDiag::All;
606 }
607 value
608 .to_string_lossy()
609 .parse::<NodeId>()
610 .map(LayoutRepassScheduleDiag::Node)
611 .unwrap_or(LayoutRepassScheduleDiag::Disabled)
612 }) {
613 LayoutRepassScheduleDiag::Disabled => false,
614 LayoutRepassScheduleDiag::All => true,
615 LayoutRepassScheduleDiag::Node(target) => target == node_id,
616 }
617}
618
619pub(crate) fn schedule_modifier_slices_repass(node_id: NodeId) {
620 with_render_state(|state| {
621 lock_repass_manager(&state.modifier_slice_repasses).schedule_repass(node_id);
622 });
623 schedule_draw_repass(node_id);
624}
625
626pub fn schedule_draw_repass(node_id: NodeId) {
631 let context = require_current_app_context("render state access");
632 schedule_draw_repass_in_context(&context, node_id);
633}
634
635fn schedule_draw_repass_for_app_context(context_id: AppContextId, node_id: NodeId) {
636 let _ = with_app_context_by_id(context_id, |context| {
637 schedule_draw_repass_in_context(context, node_id);
638 });
639}
640
641fn schedule_draw_repass_in_context(context: &AppContext, node_id: NodeId) {
642 lock_repass_manager(&context.state.draw_repasses).schedule_repass(node_id);
643 context
644 .state
645 .render_invalidated
646 .store(true, Ordering::Relaxed);
647}
648
649pub fn has_pending_draw_repasses() -> bool {
651 with_render_state(|state| lock_repass_manager(&state.draw_repasses).has_pending_repass())
652}
653
654pub fn take_draw_repass_nodes() -> Vec<NodeId> {
656 with_render_state(|state| lock_repass_manager(&state.draw_repasses).take_dirty_nodes())
657}
658
659pub fn has_pending_layout_repasses() -> bool {
661 with_render_state(|state| lock_repass_manager(&state.layout_repasses).has_pending_repass())
662}
663
664pub fn pending_layout_repass_nodes_snapshot() -> Vec<NodeId> {
666 with_render_state(|state| lock_repass_manager(&state.layout_repasses).dirty_nodes_snapshot())
667}
668
669pub fn take_layout_repass_nodes() -> Vec<NodeId> {
673 with_render_state(|state| lock_repass_manager(&state.layout_repasses).take_dirty_nodes())
674}
675
676pub(crate) fn take_modifier_slice_repass_nodes() -> Vec<NodeId> {
677 with_render_state(|state| {
678 lock_repass_manager(&state.modifier_slice_repasses).take_dirty_nodes()
679 })
680}
681
682pub fn current_density() -> f32 {
684 with_render_state(|state| f32::from_bits(state.density_bits.load(Ordering::Relaxed)))
685}
686
687pub fn set_density(density: f32) {
692 let normalized = normalize_density(density);
693 let new_bits = normalized.to_bits();
694 with_render_state(|state| {
695 let old_bits = state.density_bits.swap(new_bits, Ordering::Relaxed);
696 if old_bits != new_bits {
697 state.layout_invalidated.store(true, Ordering::Relaxed);
698 }
699 });
700}
701
702pub fn request_render_invalidation() {
704 with_render_state(|state| state.render_invalidated.store(true, Ordering::Relaxed));
705}
706
707pub fn take_render_invalidation() -> bool {
709 with_render_state(|state| state.render_invalidated.swap(false, Ordering::Relaxed))
710}
711
712pub fn peek_render_invalidation() -> bool {
714 with_render_state(|state| state.render_invalidated.load(Ordering::Relaxed))
715}
716
717pub fn request_pointer_invalidation() {
719 with_render_state(|state| state.pointer_invalidated.store(true, Ordering::Relaxed));
720}
721
722pub fn take_pointer_invalidation() -> bool {
724 with_render_state(|state| state.pointer_invalidated.swap(false, Ordering::Relaxed))
725}
726
727pub fn peek_pointer_invalidation() -> bool {
729 with_render_state(|state| state.pointer_invalidated.load(Ordering::Relaxed))
730}
731
732pub fn request_focus_invalidation() {
734 with_render_state(|state| state.focus_invalidated.store(true, Ordering::Relaxed));
735}
736
737pub fn take_focus_invalidation() -> bool {
739 with_render_state(|state| state.focus_invalidated.swap(false, Ordering::Relaxed))
740}
741
742pub fn peek_focus_invalidation() -> bool {
744 with_render_state(|state| state.focus_invalidated.load(Ordering::Relaxed))
745}
746
747pub fn request_layout_invalidation() {
774 with_render_state(|state| state.layout_invalidated.store(true, Ordering::Relaxed));
775}
776
777pub fn take_layout_invalidation() -> bool {
779 with_render_state(|state| state.layout_invalidated.swap(false, Ordering::Relaxed))
780}
781
782pub fn peek_layout_invalidation() -> bool {
784 with_render_state(|state| state.layout_invalidated.load(Ordering::Relaxed))
785}
786
787#[cfg(any(test, feature = "test-helpers"))]
788#[doc(hidden)]
789pub fn reset_render_state_for_tests() {
790 let _ = take_draw_repass_nodes();
791 let _ = take_layout_repass_nodes();
792 let _ = take_modifier_slice_repass_nodes();
793 let _ = take_render_invalidation();
794 let _ = take_pointer_invalidation();
795 let _ = take_focus_invalidation();
796 let _ = take_layout_invalidation();
797 debug_reset_last_fling_velocity();
798 set_density(1.0);
799 let _ = take_layout_invalidation();
800}
801
802#[cfg(test)]
803pub(crate) struct TestAppContextScope {
804 _scope: AppContextScope,
805 _context: Rc<AppContext>,
806}
807
808#[cfg(test)]
809pub(crate) fn app_context_test_scope() -> TestAppContextScope {
810 let context = AppContext::new();
811 let scope = context.enter_scope();
812 context.enter(reset_render_state_for_tests);
813 TestAppContextScope {
814 _scope: scope,
815 _context: context,
816 }
817}
818
819#[cfg(test)]
820pub(crate) struct RenderStateTestGuard {
821 _app_scope: TestAppContextScope,
822 _lock: std::sync::MutexGuard<'static, ()>,
823}
824
825#[cfg(test)]
826pub(crate) fn render_state_test_guard() -> RenderStateTestGuard {
827 static TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
828 let lock = match TEST_LOCK.get_or_init(|| Mutex::new(())).lock() {
829 Ok(guard) => guard,
830 Err(poisoned) => poisoned.into_inner(),
831 };
832 RenderStateTestGuard {
833 _app_scope: app_context_test_scope(),
834 _lock: lock,
835 }
836}
837
838#[cfg(test)]
839mod tests {
840 use super::*;
841 use crate::text::{AnnotatedString, TextLayoutResult, TextMeasurer, TextMetrics, TextStyle};
842 use std::sync::{mpsc, Arc};
843
844 struct TestTextMeasurer;
845
846 impl TextMeasurer for TestTextMeasurer {
847 fn measure(&self, text: &AnnotatedString, _style: &TextStyle) -> TextMetrics {
848 TextMetrics {
849 width: text.text.len() as f32,
850 height: 1.0,
851 line_height: 1.0,
852 line_count: 1,
853 }
854 }
855
856 fn get_offset_for_position(
857 &self,
858 text: &AnnotatedString,
859 _style: &TextStyle,
860 x: f32,
861 _y: f32,
862 ) -> usize {
863 x.round().max(0.0) as usize % text.text.len().max(1)
864 }
865
866 fn get_cursor_x_for_offset(
867 &self,
868 _text: &AnnotatedString,
869 _style: &TextStyle,
870 offset: usize,
871 ) -> f32 {
872 offset as f32
873 }
874
875 fn layout(&self, text: &AnnotatedString, _style: &TextStyle) -> TextLayoutResult {
876 TextLayoutResult::monospaced(&text.text, 1.0, 1.0)
877 }
878 }
879
880 #[test]
881 fn app_context_ids_do_not_use_process_global_counter() {
882 let source = include_str!("render_state.rs");
883 assert!(!source.contains(concat!("NEXT_", "APP_CONTEXT_ID: Atomic")));
884 }
885
886 #[test]
887 fn app_context_ids_are_unique_within_thread_registry() {
888 let first = AppContext::new();
889 let second = AppContext::new();
890
891 assert_ne!(first.id, second.id);
892 assert!(app_context_by_id(first.id).is_some());
893 assert!(app_context_by_id(second.id).is_some());
894 }
895
896 #[test]
897 fn set_text_measurer_requires_active_app_context() {
898 let result = std::panic::catch_unwind(|| {
899 crate::text::set_text_measurer(TestTextMeasurer);
900 });
901 assert!(result.is_err());
902
903 let context = AppContext::new();
904 context.enter(|| {
905 crate::text::set_text_measurer(TestTextMeasurer);
906 });
907 }
908
909 #[test]
910 fn invalidation_flags_are_shared_across_threads() {
911 let state = Arc::new(RenderState::new_with_density(1.0));
912 let (tx, rx) = mpsc::channel();
913 let worker_state = Arc::clone(&state);
914
915 let handle = std::thread::spawn(move || {
916 worker_state
917 .render_invalidated
918 .store(true, Ordering::Relaxed);
919 worker_state
920 .pointer_invalidated
921 .store(true, Ordering::Relaxed);
922 worker_state
923 .focus_invalidated
924 .store(true, Ordering::Relaxed);
925 worker_state
926 .layout_invalidated
927 .store(true, Ordering::Relaxed);
928 worker_state
929 .density_bits
930 .store(f32::to_bits(2.0), Ordering::Relaxed);
931 tx.send(()).expect("signal invalidation setup");
932
933 f32::from_bits(worker_state.density_bits.load(Ordering::Relaxed))
934 });
935
936 rx.recv().expect("wait for worker invalidation setup");
937 assert!(state.render_invalidated.load(Ordering::Relaxed));
938 assert!(state.pointer_invalidated.load(Ordering::Relaxed));
939 assert!(state.focus_invalidated.load(Ordering::Relaxed));
940 assert!(state.layout_invalidated.load(Ordering::Relaxed));
941 assert_eq!(
942 f32::from_bits(state.density_bits.load(Ordering::Relaxed)),
943 2.0
944 );
945 assert!(state.render_invalidated.swap(false, Ordering::Relaxed));
946 assert!(state.pointer_invalidated.swap(false, Ordering::Relaxed));
947 assert!(state.focus_invalidated.swap(false, Ordering::Relaxed));
948 assert!(state.layout_invalidated.swap(false, Ordering::Relaxed));
949
950 let density = handle.join().expect("worker invalidation snapshot");
951 assert_eq!(density, 2.0);
952 assert!(!state.render_invalidated.load(Ordering::Relaxed));
953 assert!(!state.pointer_invalidated.load(Ordering::Relaxed));
954 assert!(!state.focus_invalidated.load(Ordering::Relaxed));
955 assert!(!state.layout_invalidated.load(Ordering::Relaxed));
956 }
957
958 #[test]
959 fn app_contexts_keep_density_and_invalidations_isolated() {
960 let first = AppContext::new_with_density(1.0);
961 let second = AppContext::new_with_density(1.0);
962
963 first.enter(|| {
964 set_density(2.0);
965 request_render_invalidation();
966 request_pointer_invalidation();
967 schedule_layout_repass(11);
968 schedule_draw_repass(12);
969 });
970
971 second.enter(|| {
972 assert_eq!(current_density(), 1.0);
973 assert!(!peek_render_invalidation());
974 assert!(!peek_pointer_invalidation());
975 assert!(!peek_layout_invalidation());
976 assert!(!has_pending_layout_repasses());
977 assert!(!has_pending_draw_repasses());
978 });
979
980 first.enter(|| {
981 assert_eq!(current_density(), 2.0);
982 assert!(peek_render_invalidation());
983 assert!(peek_pointer_invalidation());
984 assert!(peek_layout_invalidation());
985 assert!(has_pending_layout_repasses());
986 assert!(has_pending_draw_repasses());
987 assert_eq!(take_layout_repass_nodes(), vec![11]);
988 assert_eq!(take_draw_repass_nodes(), vec![12]);
989 assert!(take_render_invalidation());
990 assert!(take_pointer_invalidation());
991 assert!(take_layout_invalidation());
992 });
993 }
994
995 #[test]
996 fn app_contexts_keep_fling_velocity_diagnostics_isolated() {
997 let first = AppContext::new_with_density(1.0);
998 let second = AppContext::new_with_density(1.0);
999
1000 first.enter(|| {
1001 record_last_fling_velocity(1200.0);
1002 assert_eq!(debug_last_fling_velocity(), 1200.0);
1003 });
1004
1005 second.enter(|| {
1006 assert_eq!(debug_last_fling_velocity(), 0.0);
1007 record_last_fling_velocity(-450.0);
1008 assert_eq!(debug_last_fling_velocity(), -450.0);
1009 });
1010
1011 first.enter(|| {
1012 assert_eq!(debug_last_fling_velocity(), 1200.0);
1013 debug_reset_last_fling_velocity();
1014 assert_eq!(debug_last_fling_velocity(), 0.0);
1015 });
1016
1017 second.enter(|| {
1018 assert_eq!(debug_last_fling_velocity(), -450.0);
1019 });
1020 }
1021
1022 #[test]
1023 fn app_context_new_uses_independent_density() {
1024 let outer = AppContext::new_with_density(2.0);
1025 let context = AppContext::new();
1026 context.enter(|| {
1027 assert_eq!(current_density(), 1.0);
1028 });
1029 outer.enter(|| {
1030 assert_eq!(current_density(), 2.0);
1031 });
1032 }
1033
1034 #[test]
1035 fn runtime_state_access_requires_explicit_app_context_even_in_tests() {
1036 let result = std::panic::catch_unwind(|| {
1037 request_render_invalidation();
1038 });
1039 assert!(result.is_err());
1040 }
1041
1042 #[test]
1043 fn app_contexts_keep_layout_frame_arenas_isolated() {
1044 let first = AppContext::new_with_density(1.0);
1045 let second = AppContext::new_with_density(1.0);
1046
1047 first.enter(|| {
1048 assert_eq!(layout_frame_arena_placement_scratch_count(), 0);
1049 let mut arena = take_layout_frame_arena();
1050 arena.seed_placement_scratch_for_test();
1051 replace_layout_frame_arena(arena);
1052 assert_eq!(layout_frame_arena_placement_scratch_count(), 1);
1053 });
1054
1055 second.enter(|| {
1056 assert_eq!(layout_frame_arena_placement_scratch_count(), 0);
1057 });
1058
1059 first.enter(|| {
1060 assert_eq!(layout_frame_arena_placement_scratch_count(), 1);
1061 });
1062 }
1063
1064 #[test]
1065 fn current_app_context_scope_does_not_extend_context_lifetime() {
1066 let weak = {
1067 let context = AppContext::new_with_density(1.0);
1068 let weak = Rc::downgrade(&context);
1069 context.enter(|| {
1070 assert!(current_app_context().is_some());
1071 });
1072 weak
1073 };
1074
1075 assert!(weak.upgrade().is_none());
1076 assert!(current_app_context().is_none());
1077 }
1078
1079 #[test]
1080 fn dropped_app_context_unregisters_from_thread_lookup_registry() {
1081 let start_count = app_context_registry_entry_count();
1082
1083 let id = {
1084 let context = AppContext::new_with_density(1.0);
1085 let id = context.id;
1086 assert!(app_context_by_id(id).is_some());
1087 id
1088 };
1089
1090 assert_eq!(
1091 app_context_registry_entry_count(),
1092 start_count,
1093 "dropped AppContexts must remove their weak registry entry"
1094 );
1095 assert!(app_context_by_id(id).is_none());
1096 }
1097}