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