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 with_draw_observer(|observer| {
76 observer.observe_reads(
77 scope,
78 |scope| {
79 schedule_draw_repass(scope.node_id);
80 },
81 block,
82 )
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.text.set_measurer(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 }
190
191 #[doc(hidden)]
192 pub fn downgrade(&self) -> Weak<Self> {
193 self.self_weak.borrow().clone()
194 }
195}
196
197impl Drop for AppContext {
198 fn drop(&mut self) {
199 let id = self.id;
200 let _ = APP_CONTEXTS.try_with(|contexts| {
201 contexts.borrow_mut().remove(&id);
202 });
203 }
204}
205
206fn app_context_by_id(id: AppContextId) -> Option<Rc<AppContext>> {
207 APP_CONTEXTS
208 .try_with(|contexts| {
209 let context = contexts.borrow().get(&id).cloned()?;
210 let Some(context) = context.upgrade() else {
211 contexts.borrow_mut().remove(&id);
212 return None;
213 };
214 Some(context)
215 })
216 .ok()
217 .flatten()
218}
219
220#[cfg(test)]
221fn app_context_registry_entry_count() -> usize {
222 APP_CONTEXTS
223 .try_with(|contexts| contexts.borrow().len())
224 .unwrap_or_default()
225}
226
227fn with_app_context_by_id<R>(id: AppContextId, f: impl FnOnce(&Rc<AppContext>) -> R) -> Option<R> {
228 app_context_by_id(id).map(|context| f(&context))
229}
230
231pub(crate) fn current_app_context_id() -> AppContextId {
232 require_current_app_context("app context identity access").id
233}
234
235pub(crate) fn with_layout_node_registry_by_app_context<R>(
236 id: AppContextId,
237 f: impl FnOnce(&crate::widgets::nodes::layout_node::LayoutNodeRegistryState) -> R,
238) -> Option<R> {
239 with_app_context_by_id(id, |context| f(&context.layout_node_registry))
240}
241
242pub(crate) fn enter_app_context_by_id<R>(id: AppContextId, f: impl FnOnce() -> R) -> Option<R> {
243 with_app_context_by_id(id, |context| context.enter(f))
244}
245
246fn current_app_context() -> Option<Rc<AppContext>> {
247 CURRENT_APP_CONTEXT
248 .try_with(|stack| {
249 let mut stack = stack.borrow_mut();
250 loop {
251 let context = stack.last()?;
252 if let Some(context) = context.upgrade() {
253 return Some(context);
254 }
255 stack.pop();
256 }
257 })
258 .ok()
259 .flatten()
260}
261
262#[doc(hidden)]
263pub fn has_current_app_context() -> bool {
264 current_app_context().is_some()
265}
266
267fn require_current_app_context(operation: &str) -> Rc<AppContext> {
268 if let Some(context) = current_app_context() {
269 return context;
270 }
271 require_current_app_context_without_scope(operation)
272}
273
274fn require_current_app_context_without_scope(operation: &str) -> Rc<AppContext> {
275 panic!("{operation} requires an active AppContext")
276}
277
278fn with_render_state<R>(f: impl FnOnce(&RenderState) -> R) -> R {
279 let context = require_current_app_context("render state access");
280 f(&context.state)
281}
282
283fn normalize_density(density: f32) -> f32 {
284 if density.is_finite() && density > 0.0 {
285 density
286 } else {
287 1.0
288 }
289}
290
291pub(crate) fn with_text_measurer<R>(f: impl FnOnce(&dyn crate::text::TextMeasurer) -> R) -> R {
292 let context = require_current_app_context("text measurer access");
293 context.text.with_measurer(f)
294}
295
296pub(crate) fn with_text_service<R>(f: impl FnOnce(&crate::text::measure::TextService) -> R) -> R {
297 let context = require_current_app_context("text service access");
298 f(&context.text)
299}
300
301pub(crate) fn set_current_text_measurer(measurer: Rc<dyn crate::text::TextMeasurer>) {
302 let Some(context) = current_app_context() else {
303 panic!("set_text_measurer requires an active AppContext");
304 };
305 context.text.set_measurer(measurer);
306}
307
308pub(crate) fn set_modifier_chain_trace(callback: Arc<ModifierChainTraceCallback>) -> AppContextId {
309 let context = require_current_app_context("modifier chain trace installation");
310 *context.modifier_chain_trace.borrow_mut() = Some(callback);
311 context.id
312}
313
314pub(crate) fn clear_modifier_chain_trace(context_id: AppContextId) {
315 let _ = with_app_context_by_id(context_id, |context| {
316 *context.modifier_chain_trace.borrow_mut() = None;
317 });
318}
319
320pub(crate) fn emit_modifier_chain_trace(nodes: &[crate::modifier::ModifierChainInspectorNode]) {
321 let Some(context) = current_app_context() else {
322 return;
323 };
324 let callback = context.modifier_chain_trace.borrow().clone();
325 if let Some(callback) = callback {
326 callback(nodes);
327 }
328}
329
330pub(crate) fn take_layout_frame_arena() -> crate::layout::FrameLayoutArena {
331 let context = require_current_app_context("layout frame arena access");
332 let arena = std::mem::take(&mut *context.layout_frame_arena.borrow_mut());
333 arena
334}
335
336pub(crate) fn replace_layout_frame_arena(arena: crate::layout::FrameLayoutArena) {
337 let context = require_current_app_context("layout frame arena access");
338 *context.layout_frame_arena.borrow_mut() = arena;
339}
340
341pub(crate) fn invalidate_layout_cache_epoch() {
342 let context = require_current_app_context("layout cache epoch access");
343 context.layout_cache_epoch.fetch_add(1, Ordering::Relaxed);
344}
345
346pub(crate) fn next_layout_cache_epoch() -> u64 {
347 let context = require_current_app_context("layout cache epoch access");
348 context.layout_cache_epoch.fetch_add(1, Ordering::Relaxed)
349}
350
351pub(crate) fn current_layout_cache_epoch() -> u64 {
352 let context = require_current_app_context("layout cache epoch access");
353 context.layout_cache_epoch.load(Ordering::Relaxed)
354}
355
356pub(crate) fn record_last_fling_velocity(velocity: f32) {
357 if let Some(context) = current_app_context() {
358 context
359 .last_fling_velocity_bits
360 .store(velocity.to_bits(), Ordering::Relaxed);
361 }
362}
363
364#[doc(hidden)]
365pub fn debug_last_fling_velocity() -> f32 {
366 let context = require_current_app_context("fling velocity diagnostics access");
367 f32::from_bits(context.last_fling_velocity_bits.load(Ordering::Relaxed))
368}
369
370#[doc(hidden)]
371pub fn debug_reset_last_fling_velocity() {
372 let context = require_current_app_context("fling velocity diagnostics access");
373 context
374 .last_fling_velocity_bits
375 .store(0.0f32.to_bits(), Ordering::Relaxed);
376}
377
378pub(crate) fn with_scroll_motion_context_store<R>(
379 f: impl FnOnce(&crate::scroll::ScrollMotionContextStore) -> R,
380) -> R {
381 let context = require_current_app_context("scroll motion context access");
382 f(&context.scroll_motion_contexts)
383}
384
385#[cfg(test)]
386pub(crate) fn layout_frame_arena_placement_scratch_count() -> usize {
387 let context = require_current_app_context("layout frame arena access");
388 let count = context
389 .layout_frame_arena
390 .borrow()
391 .available_placement_scratch_count();
392 count
393}
394
395pub(crate) fn with_layout_node_registry<R>(
396 f: impl FnOnce(&crate::widgets::nodes::layout_node::LayoutNodeRegistryState) -> R,
397) -> R {
398 let context = require_current_app_context("layout node registry access");
399 f(&context.layout_node_registry)
400}
401
402pub(crate) fn with_pointer_dispatch<R>(
403 f: impl FnOnce(&crate::pointer_dispatch::PointerDispatchState) -> R,
404) -> R {
405 let context = require_current_app_context("pointer dispatch access");
406 f(&context.pointer_dispatch)
407}
408
409pub(crate) fn with_focus_dispatch<R>(
410 f: impl FnOnce(&crate::focus_dispatch::FocusInvalidationState) -> R,
411) -> R {
412 let context = require_current_app_context("focus dispatch access");
413 f(&context.focus_dispatch)
414}
415
416pub(crate) fn with_cursor_animation<R>(
417 f: impl FnOnce(&crate::cursor_animation::CursorAnimationState) -> R,
418) -> R {
419 let context = require_current_app_context("cursor animation access");
420 f(&context.cursor_animation)
421}
422
423pub(crate) fn with_text_field_focus<R>(
424 f: impl FnOnce(&crate::text_field_focus::TextFieldFocusState) -> R,
425) -> R {
426 let context = require_current_app_context("text field focus access");
427 f(&context.text_field_focus)
428}
429
430pub(crate) fn register_pointer_input_task(
431 task_id: u64,
432 task: Rc<crate::modifier::pointer_input::PointerInputTaskInner>,
433) -> crate::modifier::pointer_input::PointerInputTaskOwner {
434 let context = require_current_app_context("pointer input task registration");
435 context.pointer_input_tasks.insert(task_id, task);
436 crate::modifier::pointer_input::PointerInputTaskOwner::App(context.id)
437}
438
439pub(crate) fn remove_pointer_input_task(
440 owner: crate::modifier::pointer_input::PointerInputTaskOwner,
441 task_id: u64,
442) {
443 match owner {
444 crate::modifier::pointer_input::PointerInputTaskOwner::App(context_id) => {
445 let _ = with_app_context_by_id(context_id, |context| {
446 context.pointer_input_tasks.remove(task_id);
447 });
448 }
449 }
450}
451
452pub(crate) fn request_pointer_input_task_poll(
453 owner: crate::modifier::pointer_input::PointerInputTaskOwner,
454 task_id: u64,
455) {
456 match owner {
457 crate::modifier::pointer_input::PointerInputTaskOwner::App(context_id) => {
458 let _ = with_app_context_by_id(context_id, |context| {
459 context.enter(|| {
460 context.pointer_input_tasks.request_poll(task_id, owner);
461 });
462 });
463 }
464 }
465}
466
467fn with_draw_observer<R>(f: impl FnOnce(&SnapshotStateObserver) -> R) -> R {
468 let context = require_current_app_context("draw observer access");
469 f(&context.draw_observer)
470}
471
472struct LayoutRepassManager {
477 dirty_nodes: HashSet<NodeId>,
478}
479
480impl LayoutRepassManager {
481 fn new() -> Self {
482 Self {
483 dirty_nodes: HashSet::new(),
484 }
485 }
486
487 fn schedule_repass(&mut self, node_id: NodeId) {
488 self.dirty_nodes.insert(node_id);
489 }
490
491 fn has_pending_repass(&self) -> bool {
492 !self.dirty_nodes.is_empty()
493 }
494
495 fn take_dirty_nodes(&mut self) -> Vec<NodeId> {
496 self.dirty_nodes.drain().collect()
497 }
498}
499
500struct DrawRepassManager {
502 dirty_nodes: HashSet<NodeId>,
503}
504
505impl DrawRepassManager {
506 fn new() -> Self {
507 Self {
508 dirty_nodes: HashSet::new(),
509 }
510 }
511
512 fn schedule_repass(&mut self, node_id: NodeId) {
513 self.dirty_nodes.insert(node_id);
514 }
515
516 fn has_pending_repass(&self) -> bool {
517 !self.dirty_nodes.is_empty()
518 }
519
520 fn take_dirty_nodes(&mut self) -> Vec<NodeId> {
521 self.dirty_nodes.drain().collect()
522 }
523}
524
525fn lock_repass_manager<T>(manager: &Mutex<T>) -> MutexGuard<'_, T> {
526 manager
527 .lock()
528 .unwrap_or_else(|poisoned| poisoned.into_inner())
529}
530
531pub fn schedule_layout_repass(node_id: NodeId) {
550 with_render_state(|state| {
551 lock_repass_manager(&state.layout_repasses).schedule_repass(node_id);
552 state.layout_invalidated.store(true, Ordering::Relaxed);
553 });
554 request_render_invalidation();
561}
562
563pub(crate) fn schedule_modifier_slices_repass(node_id: NodeId) {
564 with_render_state(|state| {
565 lock_repass_manager(&state.modifier_slice_repasses).schedule_repass(node_id);
566 });
567 schedule_layout_repass(node_id);
568}
569
570pub fn schedule_draw_repass(node_id: NodeId) {
575 with_render_state(|state| {
576 lock_repass_manager(&state.draw_repasses).schedule_repass(node_id);
577 });
578 request_render_invalidation();
579}
580
581pub fn has_pending_draw_repasses() -> bool {
583 with_render_state(|state| lock_repass_manager(&state.draw_repasses).has_pending_repass())
584}
585
586pub fn take_draw_repass_nodes() -> Vec<NodeId> {
588 with_render_state(|state| lock_repass_manager(&state.draw_repasses).take_dirty_nodes())
589}
590
591pub fn has_pending_layout_repasses() -> bool {
593 with_render_state(|state| lock_repass_manager(&state.layout_repasses).has_pending_repass())
594}
595
596pub fn take_layout_repass_nodes() -> Vec<NodeId> {
600 with_render_state(|state| lock_repass_manager(&state.layout_repasses).take_dirty_nodes())
601}
602
603pub(crate) fn take_modifier_slice_repass_nodes() -> Vec<NodeId> {
604 with_render_state(|state| {
605 lock_repass_manager(&state.modifier_slice_repasses).take_dirty_nodes()
606 })
607}
608
609pub fn current_density() -> f32 {
611 with_render_state(|state| f32::from_bits(state.density_bits.load(Ordering::Relaxed)))
612}
613
614pub fn set_density(density: f32) {
619 let normalized = normalize_density(density);
620 let new_bits = normalized.to_bits();
621 with_render_state(|state| {
622 let old_bits = state.density_bits.swap(new_bits, Ordering::Relaxed);
623 if old_bits != new_bits {
624 state.layout_invalidated.store(true, Ordering::Relaxed);
625 }
626 });
627}
628
629pub fn request_render_invalidation() {
631 with_render_state(|state| state.render_invalidated.store(true, Ordering::Relaxed));
632}
633
634pub fn take_render_invalidation() -> bool {
636 with_render_state(|state| state.render_invalidated.swap(false, Ordering::Relaxed))
637}
638
639pub fn peek_render_invalidation() -> bool {
641 with_render_state(|state| state.render_invalidated.load(Ordering::Relaxed))
642}
643
644pub fn request_pointer_invalidation() {
646 with_render_state(|state| state.pointer_invalidated.store(true, Ordering::Relaxed));
647}
648
649pub fn take_pointer_invalidation() -> bool {
651 with_render_state(|state| state.pointer_invalidated.swap(false, Ordering::Relaxed))
652}
653
654pub fn peek_pointer_invalidation() -> bool {
656 with_render_state(|state| state.pointer_invalidated.load(Ordering::Relaxed))
657}
658
659pub fn request_focus_invalidation() {
661 with_render_state(|state| state.focus_invalidated.store(true, Ordering::Relaxed));
662}
663
664pub fn take_focus_invalidation() -> bool {
666 with_render_state(|state| state.focus_invalidated.swap(false, Ordering::Relaxed))
667}
668
669pub fn peek_focus_invalidation() -> bool {
671 with_render_state(|state| state.focus_invalidated.load(Ordering::Relaxed))
672}
673
674pub fn request_layout_invalidation() {
701 with_render_state(|state| state.layout_invalidated.store(true, Ordering::Relaxed));
702}
703
704pub fn take_layout_invalidation() -> bool {
706 with_render_state(|state| state.layout_invalidated.swap(false, Ordering::Relaxed))
707}
708
709pub fn peek_layout_invalidation() -> bool {
711 with_render_state(|state| state.layout_invalidated.load(Ordering::Relaxed))
712}
713
714#[cfg(any(test, feature = "test-helpers"))]
715#[doc(hidden)]
716pub fn reset_render_state_for_tests() {
717 let _ = take_draw_repass_nodes();
718 let _ = take_layout_repass_nodes();
719 let _ = take_modifier_slice_repass_nodes();
720 let _ = take_render_invalidation();
721 let _ = take_pointer_invalidation();
722 let _ = take_focus_invalidation();
723 let _ = take_layout_invalidation();
724 debug_reset_last_fling_velocity();
725 set_density(1.0);
726 let _ = take_layout_invalidation();
727}
728
729#[cfg(test)]
730pub(crate) struct TestAppContextScope {
731 _scope: AppContextScope,
732 _context: Rc<AppContext>,
733}
734
735#[cfg(test)]
736pub(crate) fn app_context_test_scope() -> TestAppContextScope {
737 let context = AppContext::new();
738 let scope = context.enter_scope();
739 context.enter(reset_render_state_for_tests);
740 TestAppContextScope {
741 _scope: scope,
742 _context: context,
743 }
744}
745
746#[cfg(test)]
747pub(crate) struct RenderStateTestGuard {
748 _app_scope: TestAppContextScope,
749 _lock: std::sync::MutexGuard<'static, ()>,
750}
751
752#[cfg(test)]
753pub(crate) fn render_state_test_guard() -> RenderStateTestGuard {
754 static TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
755 let lock = match TEST_LOCK.get_or_init(|| Mutex::new(())).lock() {
756 Ok(guard) => guard,
757 Err(poisoned) => poisoned.into_inner(),
758 };
759 RenderStateTestGuard {
760 _app_scope: app_context_test_scope(),
761 _lock: lock,
762 }
763}
764
765#[cfg(test)]
766mod tests {
767 use super::*;
768 use crate::text::{AnnotatedString, TextLayoutResult, TextMeasurer, TextMetrics, TextStyle};
769 use std::sync::{mpsc, Arc};
770
771 struct TestTextMeasurer;
772
773 impl TextMeasurer for TestTextMeasurer {
774 fn measure(&self, text: &AnnotatedString, _style: &TextStyle) -> TextMetrics {
775 TextMetrics {
776 width: text.text.len() as f32,
777 height: 1.0,
778 line_height: 1.0,
779 line_count: 1,
780 }
781 }
782
783 fn get_offset_for_position(
784 &self,
785 text: &AnnotatedString,
786 _style: &TextStyle,
787 x: f32,
788 _y: f32,
789 ) -> usize {
790 x.round().max(0.0) as usize % text.text.len().max(1)
791 }
792
793 fn get_cursor_x_for_offset(
794 &self,
795 _text: &AnnotatedString,
796 _style: &TextStyle,
797 offset: usize,
798 ) -> f32 {
799 offset as f32
800 }
801
802 fn layout(&self, text: &AnnotatedString, _style: &TextStyle) -> TextLayoutResult {
803 TextLayoutResult::monospaced(&text.text, 1.0, 1.0)
804 }
805 }
806
807 #[test]
808 fn app_context_ids_do_not_use_process_global_counter() {
809 let source = include_str!("render_state.rs");
810 assert!(!source.contains(concat!("NEXT_", "APP_CONTEXT_ID: Atomic")));
811 }
812
813 #[test]
814 fn app_context_ids_are_unique_within_thread_registry() {
815 let first = AppContext::new();
816 let second = AppContext::new();
817
818 assert_ne!(first.id, second.id);
819 assert!(app_context_by_id(first.id).is_some());
820 assert!(app_context_by_id(second.id).is_some());
821 }
822
823 #[test]
824 fn set_text_measurer_requires_active_app_context() {
825 let result = std::panic::catch_unwind(|| {
826 crate::text::set_text_measurer(TestTextMeasurer);
827 });
828 assert!(result.is_err());
829
830 let context = AppContext::new();
831 context.enter(|| {
832 crate::text::set_text_measurer(TestTextMeasurer);
833 });
834 }
835
836 #[test]
837 fn invalidation_flags_are_shared_across_threads() {
838 let state = Arc::new(RenderState::new_with_density(1.0));
839 let (tx, rx) = mpsc::channel();
840 let worker_state = Arc::clone(&state);
841
842 let handle = std::thread::spawn(move || {
843 worker_state
844 .render_invalidated
845 .store(true, Ordering::Relaxed);
846 worker_state
847 .pointer_invalidated
848 .store(true, Ordering::Relaxed);
849 worker_state
850 .focus_invalidated
851 .store(true, Ordering::Relaxed);
852 worker_state
853 .layout_invalidated
854 .store(true, Ordering::Relaxed);
855 worker_state
856 .density_bits
857 .store(f32::to_bits(2.0), Ordering::Relaxed);
858 tx.send(()).expect("signal invalidation setup");
859
860 f32::from_bits(worker_state.density_bits.load(Ordering::Relaxed))
861 });
862
863 rx.recv().expect("wait for worker invalidation setup");
864 assert!(state.render_invalidated.load(Ordering::Relaxed));
865 assert!(state.pointer_invalidated.load(Ordering::Relaxed));
866 assert!(state.focus_invalidated.load(Ordering::Relaxed));
867 assert!(state.layout_invalidated.load(Ordering::Relaxed));
868 assert_eq!(
869 f32::from_bits(state.density_bits.load(Ordering::Relaxed)),
870 2.0
871 );
872 assert!(state.render_invalidated.swap(false, Ordering::Relaxed));
873 assert!(state.pointer_invalidated.swap(false, Ordering::Relaxed));
874 assert!(state.focus_invalidated.swap(false, Ordering::Relaxed));
875 assert!(state.layout_invalidated.swap(false, Ordering::Relaxed));
876
877 let density = handle.join().expect("worker invalidation snapshot");
878 assert_eq!(density, 2.0);
879 assert!(!state.render_invalidated.load(Ordering::Relaxed));
880 assert!(!state.pointer_invalidated.load(Ordering::Relaxed));
881 assert!(!state.focus_invalidated.load(Ordering::Relaxed));
882 assert!(!state.layout_invalidated.load(Ordering::Relaxed));
883 }
884
885 #[test]
886 fn app_contexts_keep_density_and_invalidations_isolated() {
887 let first = AppContext::new_with_density(1.0);
888 let second = AppContext::new_with_density(1.0);
889
890 first.enter(|| {
891 set_density(2.0);
892 request_render_invalidation();
893 request_pointer_invalidation();
894 schedule_layout_repass(11);
895 schedule_draw_repass(12);
896 });
897
898 second.enter(|| {
899 assert_eq!(current_density(), 1.0);
900 assert!(!peek_render_invalidation());
901 assert!(!peek_pointer_invalidation());
902 assert!(!peek_layout_invalidation());
903 assert!(!has_pending_layout_repasses());
904 assert!(!has_pending_draw_repasses());
905 });
906
907 first.enter(|| {
908 assert_eq!(current_density(), 2.0);
909 assert!(peek_render_invalidation());
910 assert!(peek_pointer_invalidation());
911 assert!(peek_layout_invalidation());
912 assert!(has_pending_layout_repasses());
913 assert!(has_pending_draw_repasses());
914 assert_eq!(take_layout_repass_nodes(), vec![11]);
915 assert_eq!(take_draw_repass_nodes(), vec![12]);
916 assert!(take_render_invalidation());
917 assert!(take_pointer_invalidation());
918 assert!(take_layout_invalidation());
919 });
920 }
921
922 #[test]
923 fn app_contexts_keep_fling_velocity_diagnostics_isolated() {
924 let first = AppContext::new_with_density(1.0);
925 let second = AppContext::new_with_density(1.0);
926
927 first.enter(|| {
928 record_last_fling_velocity(1200.0);
929 assert_eq!(debug_last_fling_velocity(), 1200.0);
930 });
931
932 second.enter(|| {
933 assert_eq!(debug_last_fling_velocity(), 0.0);
934 record_last_fling_velocity(-450.0);
935 assert_eq!(debug_last_fling_velocity(), -450.0);
936 });
937
938 first.enter(|| {
939 assert_eq!(debug_last_fling_velocity(), 1200.0);
940 debug_reset_last_fling_velocity();
941 assert_eq!(debug_last_fling_velocity(), 0.0);
942 });
943
944 second.enter(|| {
945 assert_eq!(debug_last_fling_velocity(), -450.0);
946 });
947 }
948
949 #[test]
950 fn app_context_new_uses_independent_density() {
951 let outer = AppContext::new_with_density(2.0);
952 let context = AppContext::new();
953 context.enter(|| {
954 assert_eq!(current_density(), 1.0);
955 });
956 outer.enter(|| {
957 assert_eq!(current_density(), 2.0);
958 });
959 }
960
961 #[test]
962 fn runtime_state_access_requires_explicit_app_context_even_in_tests() {
963 let result = std::panic::catch_unwind(|| {
964 request_render_invalidation();
965 });
966 assert!(result.is_err());
967 }
968
969 #[test]
970 fn app_contexts_keep_layout_frame_arenas_isolated() {
971 let first = AppContext::new_with_density(1.0);
972 let second = AppContext::new_with_density(1.0);
973
974 first.enter(|| {
975 assert_eq!(layout_frame_arena_placement_scratch_count(), 0);
976 let mut arena = take_layout_frame_arena();
977 arena.seed_placement_scratch_for_test();
978 replace_layout_frame_arena(arena);
979 assert_eq!(layout_frame_arena_placement_scratch_count(), 1);
980 });
981
982 second.enter(|| {
983 assert_eq!(layout_frame_arena_placement_scratch_count(), 0);
984 });
985
986 first.enter(|| {
987 assert_eq!(layout_frame_arena_placement_scratch_count(), 1);
988 });
989 }
990
991 #[test]
992 fn current_app_context_scope_does_not_extend_context_lifetime() {
993 let weak = {
994 let context = AppContext::new_with_density(1.0);
995 let weak = Rc::downgrade(&context);
996 context.enter(|| {
997 assert!(current_app_context().is_some());
998 });
999 weak
1000 };
1001
1002 assert!(weak.upgrade().is_none());
1003 assert!(current_app_context().is_none());
1004 }
1005
1006 #[test]
1007 fn dropped_app_context_unregisters_from_thread_lookup_registry() {
1008 let start_count = app_context_registry_entry_count();
1009
1010 let id = {
1011 let context = AppContext::new_with_density(1.0);
1012 let id = context.id;
1013 assert!(app_context_by_id(id).is_some());
1014 id
1015 };
1016
1017 assert_eq!(
1018 app_context_registry_entry_count(),
1019 start_count,
1020 "dropped AppContexts must remove their weak registry entry"
1021 );
1022 assert!(app_context_by_id(id).is_none());
1023 }
1024}