1#[cfg(test)]
2use std::sync::OnceLock;
3use std::{
4 cell::{Cell, RefCell},
5 collections::{HashMap, HashSet},
6 rc::{Rc, Weak},
7 sync::{
8 atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering},
9 Arc, Mutex, MutexGuard,
10 },
11};
12
13use cranpose_core::{current_runtime_handle, NodeId, SnapshotStateObserver};
14
15pub(crate) type ModifierChainTraceCallback =
16 dyn Fn(&[crate::modifier::ModifierChainInspectorNode]) + Send + Sync + 'static;
17
18struct RenderState {
19 layout_repasses: Mutex<LayoutRepassManager>,
20 measure_repasses: Mutex<LayoutRepassManager>,
26 draw_repasses: Mutex<DrawRepassManager>,
27 modifier_slice_repasses: Mutex<LayoutRepassManager>,
28 render_invalidated: AtomicBool,
29 pointer_invalidated: AtomicBool,
30 focus_invalidated: AtomicBool,
31 layout_invalidated: AtomicBool,
32 density_bits: AtomicU32,
33 font_scale: Mutex<crate::font_scale::FontScaleCurve>,
34}
35
36#[doc(hidden)]
37pub struct AppContext {
38 id: AppContextId,
39 self_weak: RefCell<Weak<AppContext>>,
40 state: RenderState,
41 draw_observer: SnapshotStateObserver,
42 text: crate::text::measure::TextService,
43 layout_frame_arena: RefCell<crate::layout::FrameLayoutArena>,
44 layout_cache_epoch: AtomicU64,
45 last_fling_velocity_bits: AtomicU32,
46 scroll_motion_contexts: crate::scroll::ScrollMotionContextStore,
47 layout_node_registry: crate::widgets::nodes::layout_node::LayoutNodeRegistryState,
48 pointer_dispatch: crate::pointer_dispatch::PointerDispatchState,
49 focus_dispatch: crate::focus_dispatch::FocusInvalidationState,
50 semantics_dispatch: crate::semantics_dispatch::SemanticsInvalidationState,
51 cursor_animation: crate::cursor_animation::CursorAnimationState,
52 text_field_focus: crate::text_field_focus::TextFieldFocusState,
53 text_input_session: crate::text_input_session::PlatformTextInputState,
54 clipboard_session: crate::clipboard_session::ClipboardSessionState,
55 pointer_input_tasks: crate::modifier::pointer_input::PointerInputTaskRegistry,
56 modifier_chain_trace: RefCell<Option<Arc<ModifierChainTraceCallback>>>,
57}
58
59#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
60pub(crate) struct AppContextId(u64);
61
62#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
63pub(crate) struct DrawObservationScope {
64 node_id: NodeId,
65 command_index: usize,
66}
67
68impl DrawObservationScope {
69 pub(crate) fn new(node_id: NodeId, command_index: usize) -> Self {
70 Self {
71 node_id,
72 command_index,
73 }
74 }
75}
76
77fn new_draw_observer() -> SnapshotStateObserver {
78 let observer = SnapshotStateObserver::new(|callback| {
79 if let Some(runtime) = current_runtime_handle() {
80 runtime.enqueue_ui_task(callback);
81 } else {
82 callback();
83 }
84 });
85 observer.start();
86 observer
87}
88
89pub(crate) fn observe_draw_reads<R>(scope: DrawObservationScope, block: impl FnOnce() -> R) -> R {
90 let context = require_current_app_context("draw observer access");
91 let context_id = context.id;
92 context.draw_observer.observe_reads(
93 scope,
94 move |scope| {
95 schedule_draw_repass_for_app_context(context_id, scope.node_id);
96 },
97 block,
98 )
99}
100
101pub(crate) fn clear_draw_observations_for_node(node_id: NodeId) {
102 with_draw_observer(|observer| {
103 observer.clear_if(|scope| {
104 scope
105 .downcast_ref::<DrawObservationScope>()
106 .is_some_and(|scope| scope.node_id == node_id)
107 });
108 });
109}
110
111pub fn prune_draw_observations_to_nodes(retained: &HashSet<NodeId>) {
112 with_draw_observer(|observer| {
113 observer.clear_if(|scope| {
114 let remove = scope
115 .downcast_ref::<DrawObservationScope>()
116 .is_some_and(|scope| !retained.contains(&scope.node_id));
117 remove
118 });
119 });
120}
121
122impl RenderState {
123 fn new_with_density(density: f32) -> Self {
124 Self {
125 layout_repasses: Mutex::new(LayoutRepassManager::new()),
126 measure_repasses: Mutex::new(LayoutRepassManager::new()),
127 draw_repasses: Mutex::new(DrawRepassManager::new()),
128 modifier_slice_repasses: Mutex::new(LayoutRepassManager::new()),
129 render_invalidated: AtomicBool::new(false),
130 pointer_invalidated: AtomicBool::new(false),
131 focus_invalidated: AtomicBool::new(false),
132 layout_invalidated: AtomicBool::new(false),
133 density_bits: AtomicU32::new(normalize_density(density).to_bits()),
134 font_scale: Mutex::new(crate::font_scale::FontScaleCurve::linear(1.0)),
135 }
136 }
137}
138
139std::thread_local! {
140 static NEXT_APP_CONTEXT_ID: Cell<u64> = const { Cell::new(1) };
141 static CURRENT_APP_CONTEXT: RefCell<Vec<Weak<AppContext>>> = const { RefCell::new(Vec::new()) };
142 static APP_CONTEXTS: RefCell<HashMap<AppContextId, Weak<AppContext>>> = RefCell::new(HashMap::new());
143}
144
145fn next_app_context_id() -> AppContextId {
146 NEXT_APP_CONTEXT_ID.with(|next| {
147 let id = next.get();
148 next.set(id.wrapping_add(1));
149 AppContextId(id)
150 })
151}
152
153#[doc(hidden)]
154pub struct AppContextScope;
155
156impl Drop for AppContextScope {
157 fn drop(&mut self) {
158 CURRENT_APP_CONTEXT.with(|stack| {
159 stack.borrow_mut().pop();
160 });
161 }
162}
163
164impl AppContext {
165 pub fn new() -> Rc<Self> {
166 Self::new_with_density(1.0)
167 }
168
169 pub fn new_with_density(density: f32) -> Rc<Self> {
170 let context = Rc::new(Self {
171 id: next_app_context_id(),
172 self_weak: RefCell::new(Weak::new()),
173 state: RenderState::new_with_density(density),
174 draw_observer: new_draw_observer(),
175 text: crate::text::measure::TextService::new(),
176 layout_frame_arena: RefCell::new(crate::layout::FrameLayoutArena::default()),
177 layout_cache_epoch: AtomicU64::new(1),
178 last_fling_velocity_bits: AtomicU32::new(0.0f32.to_bits()),
179 scroll_motion_contexts: crate::scroll::ScrollMotionContextStore::new(),
180 layout_node_registry: crate::widgets::nodes::layout_node::LayoutNodeRegistryState::new(
181 ),
182 pointer_dispatch: crate::pointer_dispatch::PointerDispatchState::new(),
183 focus_dispatch: crate::focus_dispatch::FocusInvalidationState::new(),
184 semantics_dispatch: crate::semantics_dispatch::SemanticsInvalidationState::new(),
185 cursor_animation: crate::cursor_animation::CursorAnimationState::new(),
186 text_field_focus: crate::text_field_focus::TextFieldFocusState::new(),
187 text_input_session: crate::text_input_session::PlatformTextInputState::new(),
188 clipboard_session: crate::clipboard_session::ClipboardSessionState::new(),
189 pointer_input_tasks: crate::modifier::pointer_input::PointerInputTaskRegistry::new(),
190 modifier_chain_trace: RefCell::new(None),
191 });
192 *context.self_weak.borrow_mut() = Rc::downgrade(&context);
193 APP_CONTEXTS.with(|contexts| {
194 contexts
195 .borrow_mut()
196 .insert(context.id, Rc::downgrade(&context));
197 });
198 context
199 }
200
201 pub fn enter<R>(self: &Rc<Self>, block: impl FnOnce() -> R) -> R {
202 let _scope = self.enter_scope();
203 block()
204 }
205
206 #[doc(hidden)]
207 pub fn enter_scope(self: &Rc<Self>) -> AppContextScope {
208 CURRENT_APP_CONTEXT.with(|stack| {
209 stack.borrow_mut().push(Rc::downgrade(self));
210 });
211 AppContextScope
212 }
213
214 pub fn set_text_measurer<M: crate::text::TextMeasurer>(&self, measurer: M) {
215 self.set_text_measurer_rc(Rc::new(measurer));
216 }
217
218 pub fn set_text_measurer_rc(&self, measurer: Rc<dyn crate::text::TextMeasurer>) {
219 self.text.set_measurer(measurer);
220 self.layout_cache_epoch.fetch_add(1, Ordering::Relaxed);
221 self.state.layout_invalidated.store(true, Ordering::Relaxed);
222 self.state.render_invalidated.store(true, Ordering::Relaxed);
223 }
224
225 #[doc(hidden)]
226 pub fn downgrade(&self) -> Weak<Self> {
227 self.self_weak.borrow().clone()
228 }
229}
230
231impl Drop for AppContext {
232 fn drop(&mut self) {
233 let id = self.id;
234 let _ = APP_CONTEXTS.try_with(|contexts| {
235 contexts.borrow_mut().remove(&id);
236 });
237 }
238}
239
240fn app_context_by_id(id: AppContextId) -> Option<Rc<AppContext>> {
241 APP_CONTEXTS
242 .try_with(|contexts| {
243 let context = contexts.borrow().get(&id).cloned()?;
244 let Some(context) = context.upgrade() else {
245 contexts.borrow_mut().remove(&id);
246 return None;
247 };
248 Some(context)
249 })
250 .ok()
251 .flatten()
252}
253
254#[cfg(test)]
255fn app_context_registry_entry_count() -> usize {
256 APP_CONTEXTS
257 .try_with(|contexts| contexts.borrow().len())
258 .unwrap_or_default()
259}
260
261fn with_app_context_by_id<R>(id: AppContextId, f: impl FnOnce(&Rc<AppContext>) -> R) -> Option<R> {
262 app_context_by_id(id).map(|context| f(&context))
263}
264
265pub(crate) fn current_app_context_id() -> AppContextId {
266 require_current_app_context("app context identity access").id
267}
268
269pub(crate) fn with_layout_node_registry_by_app_context<R>(
270 id: AppContextId,
271 f: impl FnOnce(&crate::widgets::nodes::layout_node::LayoutNodeRegistryState) -> R,
272) -> Option<R> {
273 with_app_context_by_id(id, |context| f(&context.layout_node_registry))
274}
275
276pub(crate) fn enter_app_context_by_id<R>(id: AppContextId, f: impl FnOnce() -> R) -> Option<R> {
277 with_app_context_by_id(id, |context| context.enter(f))
278}
279
280pub(crate) fn current_app_context() -> Option<Rc<AppContext>> {
281 CURRENT_APP_CONTEXT
282 .try_with(|stack| {
283 let mut stack = stack.borrow_mut();
284 loop {
285 let context = stack.last()?;
286 if let Some(context) = context.upgrade() {
287 return Some(context);
288 }
289 stack.pop();
290 }
291 })
292 .ok()
293 .flatten()
294}
295
296#[doc(hidden)]
297pub fn has_current_app_context() -> bool {
298 current_app_context().is_some()
299}
300
301fn require_current_app_context(operation: &str) -> Rc<AppContext> {
302 if let Some(context) = current_app_context() {
303 return context;
304 }
305 require_current_app_context_without_scope(operation)
306}
307
308fn require_current_app_context_without_scope(operation: &str) -> Rc<AppContext> {
309 panic!("{operation} requires an active AppContext")
310}
311
312fn with_render_state<R>(f: impl FnOnce(&RenderState) -> R) -> R {
313 let context = require_current_app_context("render state access");
314 f(&context.state)
315}
316
317fn normalize_density(density: f32) -> f32 {
318 if density.is_finite() && density > 0.0 {
319 density
320 } else {
321 1.0
322 }
323}
324
325fn normalize_font_scale(scale: f32) -> f32 {
332 if scale.is_finite() && scale > 0.0 {
333 scale.clamp(MIN_FONT_SCALE, MAX_FONT_SCALE)
334 } else {
335 1.0
336 }
337}
338
339pub const MIN_FONT_SCALE: f32 = 0.5;
342pub const MAX_FONT_SCALE: f32 = 3.0;
345
346pub(crate) fn with_text_measurer<R>(f: impl FnOnce(&dyn crate::text::TextMeasurer) -> R) -> R {
347 let context = require_current_app_context("text measurer access");
348 context.text.with_measurer(f)
349}
350
351pub(crate) fn with_text_service<R>(f: impl FnOnce(&crate::text::measure::TextService) -> R) -> R {
352 let context = require_current_app_context("text service access");
353 f(&context.text)
354}
355
356pub(crate) fn set_current_text_measurer(measurer: Rc<dyn crate::text::TextMeasurer>) {
357 let Some(context) = current_app_context() else {
358 panic!("set_text_measurer requires an active AppContext");
359 };
360 context.set_text_measurer_rc(measurer);
361}
362
363pub(crate) fn set_modifier_chain_trace(callback: Arc<ModifierChainTraceCallback>) -> AppContextId {
364 let context = require_current_app_context("modifier chain trace installation");
365 *context.modifier_chain_trace.borrow_mut() = Some(callback);
366 context.id
367}
368
369pub(crate) fn clear_modifier_chain_trace(context_id: AppContextId) {
370 let _ = with_app_context_by_id(context_id, |context| {
371 *context.modifier_chain_trace.borrow_mut() = None;
372 });
373}
374
375pub(crate) fn emit_modifier_chain_trace(nodes: &[crate::modifier::ModifierChainInspectorNode]) {
376 let Some(context) = current_app_context() else {
377 return;
378 };
379 let callback = context.modifier_chain_trace.borrow().clone();
380 if let Some(callback) = callback {
381 callback(nodes);
382 }
383}
384
385pub(crate) fn take_layout_frame_arena() -> crate::layout::FrameLayoutArena {
386 let context = require_current_app_context("layout frame arena access");
387 let arena = std::mem::take(&mut *context.layout_frame_arena.borrow_mut());
388 arena
389}
390
391pub(crate) fn replace_layout_frame_arena(arena: crate::layout::FrameLayoutArena) {
392 let context = require_current_app_context("layout frame arena access");
393 *context.layout_frame_arena.borrow_mut() = arena;
394}
395
396pub(crate) fn invalidate_layout_cache_epoch() {
397 let context = require_current_app_context("layout cache epoch access");
398 context.layout_cache_epoch.fetch_add(1, Ordering::Relaxed);
399}
400
401pub(crate) fn next_layout_cache_epoch() -> u64 {
402 let context = require_current_app_context("layout cache epoch access");
403 context.layout_cache_epoch.fetch_add(1, Ordering::Relaxed)
404}
405
406pub(crate) fn current_layout_cache_epoch() -> u64 {
407 let context = require_current_app_context("layout cache epoch access");
408 context.layout_cache_epoch.load(Ordering::Relaxed)
409}
410
411pub(crate) fn record_last_fling_velocity(velocity: f32) {
412 if let Some(context) = current_app_context() {
413 context
414 .last_fling_velocity_bits
415 .store(velocity.to_bits(), Ordering::Relaxed);
416 }
417}
418
419#[doc(hidden)]
420pub fn debug_last_fling_velocity() -> f32 {
421 let context = require_current_app_context("fling velocity diagnostics access");
422 f32::from_bits(context.last_fling_velocity_bits.load(Ordering::Relaxed))
423}
424
425#[doc(hidden)]
426pub fn debug_reset_last_fling_velocity() {
427 let context = require_current_app_context("fling velocity diagnostics access");
428 context
429 .last_fling_velocity_bits
430 .store(0.0f32.to_bits(), Ordering::Relaxed);
431}
432
433pub(crate) fn with_scroll_motion_context_store<R>(
434 f: impl FnOnce(&crate::scroll::ScrollMotionContextStore) -> R,
435) -> R {
436 let context = require_current_app_context("scroll motion context access");
437 f(&context.scroll_motion_contexts)
438}
439
440#[doc(hidden)]
441pub fn clear_transient_scroll_motion_contexts() {
442 let Some(context) = current_app_context() else {
443 return;
444 };
445 context.scroll_motion_contexts.clear_transient_after_frame();
446}
447
448#[cfg(test)]
449pub(crate) fn layout_frame_arena_placement_scratch_count() -> usize {
450 let context = require_current_app_context("layout frame arena access");
451 let count = context
452 .layout_frame_arena
453 .borrow()
454 .available_placement_scratch_count();
455 count
456}
457
458pub(crate) fn with_layout_node_registry<R>(
459 f: impl FnOnce(&crate::widgets::nodes::layout_node::LayoutNodeRegistryState) -> R,
460) -> R {
461 let context = require_current_app_context("layout node registry access");
462 f(&context.layout_node_registry)
463}
464
465pub(crate) fn with_pointer_dispatch<R>(
466 f: impl FnOnce(&crate::pointer_dispatch::PointerDispatchState) -> R,
467) -> R {
468 let context = require_current_app_context("pointer dispatch access");
469 f(&context.pointer_dispatch)
470}
471
472pub(crate) fn with_focus_dispatch<R>(
473 f: impl FnOnce(&crate::focus_dispatch::FocusInvalidationState) -> R,
474) -> R {
475 let context = require_current_app_context("focus dispatch access");
476 f(&context.focus_dispatch)
477}
478
479pub(crate) fn with_semantics_dispatch<R>(
480 f: impl FnOnce(&crate::semantics_dispatch::SemanticsInvalidationState) -> R,
481) -> R {
482 let context = require_current_app_context("semantics dispatch access");
483 f(&context.semantics_dispatch)
484}
485
486pub(crate) fn with_semantics_dispatch_by_app_context(
495 id: AppContextId,
496 f: impl FnOnce(&crate::semantics_dispatch::SemanticsInvalidationState),
497) {
498 with_app_context_by_id(id, |context| f(&context.semantics_dispatch));
499}
500
501pub(crate) fn current_app_context_id_opt() -> Option<AppContextId> {
503 current_app_context().map(|context| context.id)
504}
505
506pub(crate) fn with_cursor_animation<R>(
507 f: impl FnOnce(&crate::cursor_animation::CursorAnimationState) -> R,
508) -> R {
509 let context = require_current_app_context("cursor animation access");
510 f(&context.cursor_animation)
511}
512
513pub(crate) fn with_text_field_focus<R>(
514 f: impl FnOnce(&crate::text_field_focus::TextFieldFocusState) -> R,
515) -> R {
516 let context = require_current_app_context("text field focus access");
517 f(&context.text_field_focus)
518}
519
520pub(crate) fn with_text_input_session<R>(
521 f: impl FnOnce(&crate::text_input_session::PlatformTextInputState) -> R,
522) -> R {
523 let context = require_current_app_context("platform text input session access");
524 f(&context.text_input_session)
525}
526
527pub(crate) fn with_clipboard_session<R>(
528 f: impl FnOnce(&crate::clipboard_session::ClipboardSessionState) -> R,
529) -> R {
530 let context = require_current_app_context("clipboard session access");
531 f(&context.clipboard_session)
532}
533
534pub(crate) fn register_pointer_input_task(
535 task_id: u64,
536 task: Rc<crate::modifier::pointer_input::PointerInputTaskInner>,
537) -> crate::modifier::pointer_input::PointerInputTaskOwner {
538 let context = require_current_app_context("pointer input task registration");
539 context.pointer_input_tasks.insert(task_id, task);
540 crate::modifier::pointer_input::PointerInputTaskOwner::App(context.id)
541}
542
543pub(crate) fn remove_pointer_input_task(
544 owner: crate::modifier::pointer_input::PointerInputTaskOwner,
545 task_id: u64,
546) {
547 match owner {
548 crate::modifier::pointer_input::PointerInputTaskOwner::App(context_id) => {
549 let _ = with_app_context_by_id(context_id, |context| {
550 context.pointer_input_tasks.remove(task_id);
551 });
552 }
553 }
554}
555
556pub(crate) fn request_pointer_input_task_poll(
557 owner: crate::modifier::pointer_input::PointerInputTaskOwner,
558 task_id: u64,
559) {
560 match owner {
561 crate::modifier::pointer_input::PointerInputTaskOwner::App(context_id) => {
562 let _ = with_app_context_by_id(context_id, |context| {
563 context.enter(|| {
564 context.pointer_input_tasks.request_poll(task_id, owner);
565 });
566 });
567 }
568 }
569}
570
571fn with_draw_observer<R>(f: impl FnOnce(&SnapshotStateObserver) -> R) -> R {
572 let context = require_current_app_context("draw observer access");
573 f(&context.draw_observer)
574}
575
576struct LayoutRepassManager {
581 dirty_nodes: HashSet<NodeId>,
582}
583
584impl LayoutRepassManager {
585 fn new() -> Self {
586 Self {
587 dirty_nodes: HashSet::new(),
588 }
589 }
590
591 fn schedule_repass(&mut self, node_id: NodeId) {
592 self.dirty_nodes.insert(node_id);
593 }
594
595 fn has_pending_repass(&self) -> bool {
596 !self.dirty_nodes.is_empty()
597 }
598
599 fn take_dirty_nodes(&mut self) -> Vec<NodeId> {
600 self.dirty_nodes.drain().collect()
601 }
602
603 fn dirty_nodes_snapshot(&self) -> Vec<NodeId> {
604 let mut nodes = self.dirty_nodes.iter().copied().collect::<Vec<_>>();
605 nodes.sort_unstable();
606 nodes
607 }
608}
609
610struct DrawRepassManager {
612 dirty_nodes: HashSet<NodeId>,
613}
614
615impl DrawRepassManager {
616 fn new() -> Self {
617 Self {
618 dirty_nodes: HashSet::new(),
619 }
620 }
621
622 fn schedule_repass(&mut self, node_id: NodeId) {
623 self.dirty_nodes.insert(node_id);
624 }
625
626 fn has_pending_repass(&self) -> bool {
627 !self.dirty_nodes.is_empty()
628 }
629
630 fn take_dirty_nodes(&mut self) -> Vec<NodeId> {
631 self.dirty_nodes.drain().collect()
632 }
633}
634
635fn lock_repass_manager<T>(manager: &Mutex<T>) -> MutexGuard<'_, T> {
636 manager
637 .lock()
638 .unwrap_or_else(|poisoned| poisoned.into_inner())
639}
640
641#[track_caller]
660pub fn schedule_layout_repass(node_id: NodeId) {
661 if layout_repass_schedule_diagnostics_enabled_for(node_id) {
662 let caller = std::panic::Location::caller();
663 log::warn!(
664 "[layout-repass-schedule] node={} caller={}:{}:{}",
665 node_id,
666 caller.file(),
667 caller.line(),
668 caller.column()
669 );
670 }
671 with_render_state(|state| {
672 lock_repass_manager(&state.layout_repasses).schedule_repass(node_id);
673 state.layout_invalidated.store(true, Ordering::Relaxed);
674 });
675 request_render_invalidation();
682}
683
684#[derive(Clone, Copy)]
685enum LayoutRepassScheduleDiag {
686 Disabled,
687 All,
688 Node(NodeId),
689}
690
691fn layout_repass_schedule_diagnostics_enabled_for(node_id: NodeId) -> bool {
692 static MODE: std::sync::OnceLock<LayoutRepassScheduleDiag> = std::sync::OnceLock::new();
693 match *MODE.get_or_init(|| {
694 let Some(value) = std::env::var_os("CRANPOSE_LAYOUT_REPASS_SCHEDULE_DIAG") else {
695 return LayoutRepassScheduleDiag::Disabled;
696 };
697 if value == "all" {
698 return LayoutRepassScheduleDiag::All;
699 }
700 value
701 .to_string_lossy()
702 .parse::<NodeId>()
703 .map(LayoutRepassScheduleDiag::Node)
704 .unwrap_or(LayoutRepassScheduleDiag::Disabled)
705 }) {
706 LayoutRepassScheduleDiag::Disabled => false,
707 LayoutRepassScheduleDiag::All => true,
708 LayoutRepassScheduleDiag::Node(target) => target == node_id,
709 }
710}
711
712pub(crate) fn schedule_modifier_slices_repass(node_id: NodeId) {
713 with_render_state(|state| {
714 lock_repass_manager(&state.modifier_slice_repasses).schedule_repass(node_id);
715 });
716 schedule_draw_repass(node_id);
717}
718
719pub fn schedule_draw_repass(node_id: NodeId) {
724 let context = require_current_app_context("render state access");
725 schedule_draw_repass_in_context(&context, node_id);
726}
727
728fn schedule_draw_repass_for_app_context(context_id: AppContextId, node_id: NodeId) {
729 let _ = with_app_context_by_id(context_id, |context| {
730 schedule_draw_repass_in_context(context, node_id);
731 });
732}
733
734fn schedule_draw_repass_in_context(context: &AppContext, node_id: NodeId) {
735 lock_repass_manager(&context.state.draw_repasses).schedule_repass(node_id);
736 context
737 .state
738 .render_invalidated
739 .store(true, Ordering::Relaxed);
740}
741
742pub fn has_pending_draw_repasses() -> bool {
744 with_render_state(|state| lock_repass_manager(&state.draw_repasses).has_pending_repass())
745}
746
747pub fn take_draw_repass_nodes() -> Vec<NodeId> {
749 with_render_state(|state| lock_repass_manager(&state.draw_repasses).take_dirty_nodes())
750}
751
752pub fn has_pending_layout_repasses() -> bool {
754 with_render_state(|state| lock_repass_manager(&state.layout_repasses).has_pending_repass())
755}
756
757pub fn pending_layout_repass_nodes_snapshot() -> Vec<NodeId> {
759 with_render_state(|state| lock_repass_manager(&state.layout_repasses).dirty_nodes_snapshot())
760}
761
762pub fn take_layout_repass_nodes() -> Vec<NodeId> {
766 with_render_state(|state| lock_repass_manager(&state.layout_repasses).take_dirty_nodes())
767}
768
769pub fn schedule_measure_repass(node_id: NodeId) {
778 with_render_state(|state| {
779 lock_repass_manager(&state.measure_repasses).schedule_repass(node_id);
780 state.layout_invalidated.store(true, Ordering::Relaxed);
781 });
782 request_render_invalidation();
783}
784
785pub fn has_pending_measure_repasses() -> bool {
787 with_render_state(|state| lock_repass_manager(&state.measure_repasses).has_pending_repass())
788}
789
790pub fn take_measure_repass_nodes() -> Vec<NodeId> {
794 with_render_state(|state| lock_repass_manager(&state.measure_repasses).take_dirty_nodes())
795}
796
797pub(crate) fn take_modifier_slice_repass_nodes() -> Vec<NodeId> {
798 with_render_state(|state| {
799 lock_repass_manager(&state.modifier_slice_repasses).take_dirty_nodes()
800 })
801}
802
803pub fn current_density() -> f32 {
805 with_render_state(|state| f32::from_bits(state.density_bits.load(Ordering::Relaxed)))
806}
807
808pub fn set_density(density: f32) {
813 let normalized = normalize_density(density);
814 let new_bits = normalized.to_bits();
815 with_render_state(|state| {
816 let old_bits = state.density_bits.swap(new_bits, Ordering::Relaxed);
817 if old_bits != new_bits {
818 state.layout_invalidated.store(true, Ordering::Relaxed);
819 }
820 });
821}
822
823pub fn current_font_scale() -> f32 {
834 current_font_scale_curve().scale()
835}
836
837pub fn current_font_scale_curve() -> crate::font_scale::FontScaleCurve {
843 with_render_state(|state| *lock_font_scale(&state.font_scale))
844}
845
846pub fn scale_sp(sp: f32) -> f32 {
848 current_font_scale_curve().sp_to_dp(sp)
849}
850
851pub fn set_font_scale(scale: f32) {
861 set_font_scale_curve(crate::font_scale::FontScaleCurve::linear(scale));
862}
863
864pub fn set_font_scale_curve(curve: crate::font_scale::FontScaleCurve) {
872 let normalized = normalize_font_scale(curve.scale());
873 let curve = if (normalized - curve.scale()).abs() <= f32::EPSILON {
874 curve
875 } else {
876 crate::font_scale::FontScaleCurve::linear(normalized)
877 };
878 with_render_state(|state| {
879 let mut current = lock_font_scale(&state.font_scale);
880 if *current != curve {
881 *current = curve;
882 state.layout_invalidated.store(true, Ordering::Relaxed);
883 }
884 });
885}
886
887fn lock_font_scale(
888 slot: &Mutex<crate::font_scale::FontScaleCurve>,
889) -> MutexGuard<'_, crate::font_scale::FontScaleCurve> {
890 match slot.lock() {
891 Ok(guard) => guard,
892 Err(poisoned) => poisoned.into_inner(),
893 }
894}
895
896pub fn request_render_invalidation() {
898 with_render_state(|state| state.render_invalidated.store(true, Ordering::Relaxed));
899}
900
901pub fn take_render_invalidation() -> bool {
903 with_render_state(|state| state.render_invalidated.swap(false, Ordering::Relaxed))
904}
905
906pub fn peek_render_invalidation() -> bool {
908 with_render_state(|state| state.render_invalidated.load(Ordering::Relaxed))
909}
910
911pub fn request_pointer_invalidation() {
913 with_render_state(|state| state.pointer_invalidated.store(true, Ordering::Relaxed));
914}
915
916pub fn take_pointer_invalidation() -> bool {
918 with_render_state(|state| state.pointer_invalidated.swap(false, Ordering::Relaxed))
919}
920
921pub fn peek_pointer_invalidation() -> bool {
923 with_render_state(|state| state.pointer_invalidated.load(Ordering::Relaxed))
924}
925
926pub fn request_focus_invalidation() {
928 with_render_state(|state| state.focus_invalidated.store(true, Ordering::Relaxed));
929}
930
931pub fn take_focus_invalidation() -> bool {
933 with_render_state(|state| state.focus_invalidated.swap(false, Ordering::Relaxed))
934}
935
936pub fn peek_focus_invalidation() -> bool {
938 with_render_state(|state| state.focus_invalidated.load(Ordering::Relaxed))
939}
940
941pub fn request_layout_invalidation() {
968 with_render_state(|state| state.layout_invalidated.store(true, Ordering::Relaxed));
969}
970
971pub fn take_layout_invalidation() -> bool {
973 with_render_state(|state| state.layout_invalidated.swap(false, Ordering::Relaxed))
974}
975
976pub fn peek_layout_invalidation() -> bool {
978 with_render_state(|state| state.layout_invalidated.load(Ordering::Relaxed))
979}
980
981#[cfg(any(test, feature = "test-helpers"))]
982#[doc(hidden)]
983pub fn reset_render_state_for_tests() {
984 let _ = take_draw_repass_nodes();
985 let _ = take_layout_repass_nodes();
986 let _ = take_modifier_slice_repass_nodes();
987 let _ = take_render_invalidation();
988 let _ = take_pointer_invalidation();
989 let _ = take_focus_invalidation();
990 let _ = take_layout_invalidation();
991 debug_reset_last_fling_velocity();
992 set_density(1.0);
993 set_font_scale(1.0);
994 let _ = take_layout_invalidation();
995}
996
997#[cfg(test)]
998pub(crate) struct TestAppContextScope {
999 _scope: AppContextScope,
1000 _context: Rc<AppContext>,
1001}
1002
1003#[cfg(test)]
1004pub(crate) fn app_context_test_scope() -> TestAppContextScope {
1005 let context = AppContext::new();
1006 let scope = context.enter_scope();
1007 context.enter(reset_render_state_for_tests);
1008 TestAppContextScope {
1009 _scope: scope,
1010 _context: context,
1011 }
1012}
1013
1014#[cfg(test)]
1015pub(crate) struct RenderStateTestGuard {
1016 _app_scope: TestAppContextScope,
1017 _lock: std::sync::MutexGuard<'static, ()>,
1018}
1019
1020#[cfg(test)]
1021pub(crate) fn render_state_test_guard() -> RenderStateTestGuard {
1022 static TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
1023 let lock = match TEST_LOCK.get_or_init(|| Mutex::new(())).lock() {
1024 Ok(guard) => guard,
1025 Err(poisoned) => poisoned.into_inner(),
1026 };
1027 RenderStateTestGuard {
1028 _app_scope: app_context_test_scope(),
1029 _lock: lock,
1030 }
1031}
1032
1033#[cfg(test)]
1034mod tests {
1035 use std::sync::{mpsc, Arc};
1036
1037 use super::*;
1038 use crate::text::{AnnotatedString, TextLayoutResult, TextMeasurer, TextMetrics, TextStyle};
1039
1040 struct TestTextMeasurer;
1041
1042 impl TextMeasurer for TestTextMeasurer {
1043 fn measure(&self, text: &AnnotatedString, _style: &TextStyle) -> TextMetrics {
1044 TextMetrics {
1045 width: text.text.len() as f32,
1046 height: 1.0,
1047 line_height: 1.0,
1048 line_count: 1,
1049 }
1050 }
1051
1052 fn get_offset_for_position(
1053 &self,
1054 text: &AnnotatedString,
1055 _style: &TextStyle,
1056 x: f32,
1057 _y: f32,
1058 ) -> usize {
1059 x.round().max(0.0) as usize % text.text.len().max(1)
1060 }
1061
1062 fn get_cursor_x_for_offset(
1063 &self,
1064 _text: &AnnotatedString,
1065 _style: &TextStyle,
1066 offset: usize,
1067 ) -> f32 {
1068 offset as f32
1069 }
1070
1071 fn layout(&self, text: &AnnotatedString, _style: &TextStyle) -> TextLayoutResult {
1072 TextLayoutResult::monospaced(&text.text, 1.0, 1.0)
1073 }
1074 }
1075
1076 #[test]
1077 fn app_context_ids_do_not_use_process_global_counter() {
1078 let source = include_str!("render_state.rs");
1079 assert!(!source.contains(concat!("NEXT_", "APP_CONTEXT_ID: Atomic")));
1080 }
1081
1082 #[test]
1083 fn app_context_ids_are_unique_within_thread_registry() {
1084 let first = AppContext::new();
1085 let second = AppContext::new();
1086
1087 assert_ne!(first.id, second.id);
1088 assert!(app_context_by_id(first.id).is_some());
1089 assert!(app_context_by_id(second.id).is_some());
1090 }
1091
1092 #[test]
1093 fn set_text_measurer_requires_active_app_context() {
1094 let result = std::panic::catch_unwind(|| {
1095 crate::text::set_text_measurer(TestTextMeasurer);
1096 });
1097 assert!(result.is_err());
1098
1099 let context = AppContext::new();
1100 context.enter(|| {
1101 crate::text::set_text_measurer(TestTextMeasurer);
1102 });
1103 }
1104
1105 #[test]
1106 fn the_font_scale_starts_at_one_and_invalidates_layout_when_it_moves() {
1107 let context = AppContext::new();
1108 context.enter(|| {
1109 assert_eq!(current_font_scale(), 1.0);
1110 let _ = take_layout_invalidation();
1111
1112 set_font_scale(1.3);
1113 assert_eq!(current_font_scale(), 1.3);
1114 assert!(
1115 take_layout_invalidation(),
1116 "every Sp on screen just changed size"
1117 );
1118
1119 set_font_scale(1.3);
1122 assert!(!take_layout_invalidation());
1123 });
1124 }
1125
1126 #[test]
1127 fn a_font_scale_no_platform_reports_is_refused() {
1128 let context = AppContext::new();
1129 context.enter(|| {
1130 for nonsense in [0.0, -1.0, f32::NAN, f32::INFINITY] {
1131 set_font_scale(1.0);
1132 set_font_scale(nonsense);
1133 assert_eq!(current_font_scale(), 1.0, "{nonsense} was let through");
1134 }
1135 set_font_scale(99.0);
1136 assert_eq!(current_font_scale(), MAX_FONT_SCALE);
1137 set_font_scale(0.01);
1138 assert_eq!(current_font_scale(), MIN_FONT_SCALE);
1139 });
1140 }
1141
1142 #[test]
1143 fn the_font_scale_is_per_app_context() {
1144 let first = AppContext::new();
1145 let second = AppContext::new();
1146 first.enter(|| set_font_scale(1.5));
1147 first.enter(|| assert_eq!(current_font_scale(), 1.5));
1148 second.enter(|| assert_eq!(current_font_scale(), 1.0));
1149 }
1150
1151 #[test]
1152 fn invalidation_flags_are_shared_across_threads() {
1153 let state = Arc::new(RenderState::new_with_density(1.0));
1154 let (tx, rx) = mpsc::channel();
1155 let worker_state = Arc::clone(&state);
1156
1157 let handle = std::thread::spawn(move || {
1158 worker_state
1159 .render_invalidated
1160 .store(true, Ordering::Relaxed);
1161 worker_state
1162 .pointer_invalidated
1163 .store(true, Ordering::Relaxed);
1164 worker_state
1165 .focus_invalidated
1166 .store(true, Ordering::Relaxed);
1167 worker_state
1168 .layout_invalidated
1169 .store(true, Ordering::Relaxed);
1170 worker_state
1171 .density_bits
1172 .store(f32::to_bits(2.0), Ordering::Relaxed);
1173 tx.send(()).expect("signal invalidation setup");
1174
1175 f32::from_bits(worker_state.density_bits.load(Ordering::Relaxed))
1176 });
1177
1178 rx.recv().expect("wait for worker invalidation setup");
1179 assert!(state.render_invalidated.load(Ordering::Relaxed));
1180 assert!(state.pointer_invalidated.load(Ordering::Relaxed));
1181 assert!(state.focus_invalidated.load(Ordering::Relaxed));
1182 assert!(state.layout_invalidated.load(Ordering::Relaxed));
1183 assert_eq!(
1184 f32::from_bits(state.density_bits.load(Ordering::Relaxed)),
1185 2.0
1186 );
1187 assert!(state.render_invalidated.swap(false, Ordering::Relaxed));
1188 assert!(state.pointer_invalidated.swap(false, Ordering::Relaxed));
1189 assert!(state.focus_invalidated.swap(false, Ordering::Relaxed));
1190 assert!(state.layout_invalidated.swap(false, Ordering::Relaxed));
1191
1192 let density = handle.join().expect("worker invalidation snapshot");
1193 assert_eq!(density, 2.0);
1194 assert!(!state.render_invalidated.load(Ordering::Relaxed));
1195 assert!(!state.pointer_invalidated.load(Ordering::Relaxed));
1196 assert!(!state.focus_invalidated.load(Ordering::Relaxed));
1197 assert!(!state.layout_invalidated.load(Ordering::Relaxed));
1198 }
1199
1200 #[test]
1201 fn app_contexts_keep_density_and_invalidations_isolated() {
1202 let first = AppContext::new_with_density(1.0);
1203 let second = AppContext::new_with_density(1.0);
1204
1205 first.enter(|| {
1206 set_density(2.0);
1207 request_render_invalidation();
1208 request_pointer_invalidation();
1209 schedule_layout_repass(11);
1210 schedule_draw_repass(12);
1211 });
1212
1213 second.enter(|| {
1214 assert_eq!(current_density(), 1.0);
1215 assert!(!peek_render_invalidation());
1216 assert!(!peek_pointer_invalidation());
1217 assert!(!peek_layout_invalidation());
1218 assert!(!has_pending_layout_repasses());
1219 assert!(!has_pending_draw_repasses());
1220 });
1221
1222 first.enter(|| {
1223 assert_eq!(current_density(), 2.0);
1224 assert!(peek_render_invalidation());
1225 assert!(peek_pointer_invalidation());
1226 assert!(peek_layout_invalidation());
1227 assert!(has_pending_layout_repasses());
1228 assert!(has_pending_draw_repasses());
1229 assert_eq!(take_layout_repass_nodes(), vec![11]);
1230 assert_eq!(take_draw_repass_nodes(), vec![12]);
1231 assert!(take_render_invalidation());
1232 assert!(take_pointer_invalidation());
1233 assert!(take_layout_invalidation());
1234 });
1235 }
1236
1237 #[test]
1238 fn app_contexts_keep_fling_velocity_diagnostics_isolated() {
1239 let first = AppContext::new_with_density(1.0);
1240 let second = AppContext::new_with_density(1.0);
1241
1242 first.enter(|| {
1243 record_last_fling_velocity(1200.0);
1244 assert_eq!(debug_last_fling_velocity(), 1200.0);
1245 });
1246
1247 second.enter(|| {
1248 assert_eq!(debug_last_fling_velocity(), 0.0);
1249 record_last_fling_velocity(-450.0);
1250 assert_eq!(debug_last_fling_velocity(), -450.0);
1251 });
1252
1253 first.enter(|| {
1254 assert_eq!(debug_last_fling_velocity(), 1200.0);
1255 debug_reset_last_fling_velocity();
1256 assert_eq!(debug_last_fling_velocity(), 0.0);
1257 });
1258
1259 second.enter(|| {
1260 assert_eq!(debug_last_fling_velocity(), -450.0);
1261 });
1262 }
1263
1264 #[test]
1265 fn app_context_new_uses_independent_density() {
1266 let outer = AppContext::new_with_density(2.0);
1267 let context = AppContext::new();
1268 context.enter(|| {
1269 assert_eq!(current_density(), 1.0);
1270 });
1271 outer.enter(|| {
1272 assert_eq!(current_density(), 2.0);
1273 });
1274 }
1275
1276 #[test]
1277 fn runtime_state_access_requires_explicit_app_context_even_in_tests() {
1278 let result = std::panic::catch_unwind(|| {
1279 request_render_invalidation();
1280 });
1281 assert!(result.is_err());
1282 }
1283
1284 #[test]
1285 fn app_contexts_keep_layout_frame_arenas_isolated() {
1286 let first = AppContext::new_with_density(1.0);
1287 let second = AppContext::new_with_density(1.0);
1288
1289 first.enter(|| {
1290 assert_eq!(layout_frame_arena_placement_scratch_count(), 0);
1291 let mut arena = take_layout_frame_arena();
1292 arena.seed_placement_scratch_for_test();
1293 replace_layout_frame_arena(arena);
1294 assert_eq!(layout_frame_arena_placement_scratch_count(), 1);
1295 });
1296
1297 second.enter(|| {
1298 assert_eq!(layout_frame_arena_placement_scratch_count(), 0);
1299 });
1300
1301 first.enter(|| {
1302 assert_eq!(layout_frame_arena_placement_scratch_count(), 1);
1303 });
1304 }
1305
1306 #[test]
1307 fn current_app_context_scope_does_not_extend_context_lifetime() {
1308 let weak = {
1309 let context = AppContext::new_with_density(1.0);
1310 let weak = Rc::downgrade(&context);
1311 context.enter(|| {
1312 assert!(current_app_context().is_some());
1313 });
1314 weak
1315 };
1316
1317 assert!(weak.upgrade().is_none());
1318 assert!(current_app_context().is_none());
1319 }
1320
1321 #[test]
1322 fn dropped_app_context_unregisters_from_thread_lookup_registry() {
1323 let start_count = app_context_registry_entry_count();
1324
1325 let id = {
1326 let context = AppContext::new_with_density(1.0);
1327 let id = context.id;
1328 assert!(app_context_by_id(id).is_some());
1329 id
1330 };
1331
1332 assert_eq!(
1333 app_context_registry_entry_count(),
1334 start_count,
1335 "dropped AppContexts must remove their weak registry entry"
1336 );
1337 assert!(app_context_by_id(id).is_none());
1338 }
1339}