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