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