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