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