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