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 geometry_scene_nodes: Mutex<LayoutRepassManager>,
34 render_invalidated: AtomicBool,
35 pointer_invalidated: AtomicBool,
36 focus_invalidated: AtomicBool,
37 layout_invalidated: AtomicBool,
38 density_bits: AtomicU32,
39 font_scale: Mutex<crate::font_scale::FontScaleCurve>,
40}
41
42#[doc(hidden)]
43pub struct AppContext {
44 id: AppContextId,
45 self_weak: RefCell<Weak<AppContext>>,
46 state: RenderState,
47 draw_observer: SnapshotStateObserver,
48 text: crate::text::measure::TextService,
49 layout_frame_arena: RefCell<crate::layout::FrameLayoutArena>,
50 layout_cache_epoch: AtomicU64,
51 last_fling_velocity_bits: AtomicU32,
52 scroll_motion_contexts: crate::scroll::ScrollMotionContextStore,
53 layout_node_registry: crate::widgets::nodes::layout_node::LayoutNodeRegistryState,
54 pointer_dispatch: crate::pointer_dispatch::PointerDispatchState,
55 focus_dispatch: crate::focus_dispatch::FocusInvalidationState,
56 semantics_dispatch: crate::semantics_dispatch::SemanticsInvalidationState,
57 cursor_animation: crate::cursor_animation::CursorAnimationState,
58 text_field_focus: crate::text_field_focus::TextFieldFocusState,
59 text_input_session: crate::text_input_session::PlatformTextInputState,
60 clipboard_session: crate::clipboard_session::ClipboardSessionState,
61 pointer_input_tasks: crate::modifier::pointer_input::PointerInputTaskRegistry,
62 modifier_chain_trace: RefCell<Option<Arc<ModifierChainTraceCallback>>>,
63}
64
65#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
66pub(crate) struct AppContextId(u64);
67
68#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
69pub(crate) struct DrawObservationScope {
70 node_id: NodeId,
71 command_index: usize,
72}
73
74impl DrawObservationScope {
75 pub(crate) fn new(node_id: NodeId, command_index: usize) -> Self {
76 Self {
77 node_id,
78 command_index,
79 }
80 }
81}
82
83fn new_draw_observer() -> SnapshotStateObserver {
84 let observer = SnapshotStateObserver::new(|callback| {
85 if let Some(runtime) = current_runtime_handle() {
86 runtime.enqueue_ui_task(callback);
87 } else {
88 callback();
89 }
90 });
91 observer.start();
92 observer
93}
94
95thread_local! {
96 static CURRENT_DRAW_NODE: std::cell::Cell<Option<NodeId>> = const { std::cell::Cell::new(None) };
100}
101
102struct CurrentDrawNodeGuard {
103 previous: Option<NodeId>,
104}
105
106impl Drop for CurrentDrawNodeGuard {
107 fn drop(&mut self) {
108 CURRENT_DRAW_NODE.with(|current| current.set(self.previous));
109 }
110}
111
112pub(crate) fn observe_draw_reads<R>(scope: DrawObservationScope, block: impl FnOnce() -> R) -> R {
113 let context = require_current_app_context("draw observer access");
114 let context_id = context.id;
115 let _guard = CurrentDrawNodeGuard {
116 previous: CURRENT_DRAW_NODE.with(|current| current.replace(Some(scope.node_id))),
117 };
118 context.draw_observer.observe_reads(
119 scope,
120 move |scope| {
121 schedule_draw_repass_for_app_context(context_id, scope.node_id);
122 },
123 block,
124 )
125}
126
127pub fn request_current_draw_redraw() {
134 if let Some(node_id) = CURRENT_DRAW_NODE.with(std::cell::Cell::get) {
135 schedule_draw_repass(node_id);
136 }
137 request_render_invalidation();
138}
139
140pub(crate) fn clear_draw_observations_for_node(node_id: NodeId) {
141 with_draw_observer(|observer| {
142 observer.clear_if(|scope| {
143 scope
144 .downcast_ref::<DrawObservationScope>()
145 .is_some_and(|scope| scope.node_id == node_id)
146 });
147 });
148}
149
150pub fn prune_draw_observations_to_nodes(retained: &HashSet<NodeId>) {
151 with_draw_observer(|observer| {
152 observer.clear_if(|scope| {
153 scope
154 .downcast_ref::<DrawObservationScope>()
155 .is_some_and(|scope| !retained.contains(&scope.node_id))
156 });
157 });
158}
159
160impl RenderState {
161 fn new_with_density(density: f32) -> Self {
162 Self {
163 layout_repasses: Mutex::new(LayoutRepassManager::new()),
164 measure_repasses: Mutex::new(LayoutRepassManager::new()),
165 draw_repasses: Mutex::new(DrawRepassManager::new()),
166 modifier_slice_repasses: Mutex::new(LayoutRepassManager::new()),
167 geometry_scene_nodes: Mutex::new(LayoutRepassManager::new()),
168 render_invalidated: AtomicBool::new(false),
169 pointer_invalidated: AtomicBool::new(false),
170 focus_invalidated: AtomicBool::new(false),
171 layout_invalidated: AtomicBool::new(false),
172 density_bits: AtomicU32::new(normalize_density(density).to_bits()),
173 font_scale: Mutex::new(crate::font_scale::FontScaleCurve::linear(1.0)),
174 }
175 }
176}
177
178std::thread_local! {
179 static NEXT_APP_CONTEXT_ID: Cell<u64> = const { Cell::new(1) };
180 static CURRENT_APP_CONTEXT: RefCell<Vec<Weak<AppContext>>> = const { RefCell::new(Vec::new()) };
181 static APP_CONTEXTS: RefCell<HashMap<AppContextId, Weak<AppContext>>> = RefCell::new(HashMap::new());
182}
183
184fn next_app_context_id() -> AppContextId {
185 NEXT_APP_CONTEXT_ID.with(|next| {
186 let id = next.get();
187 next.set(id.wrapping_add(1));
188 AppContextId(id)
189 })
190}
191
192#[doc(hidden)]
193pub struct AppContextScope;
194
195impl Drop for AppContextScope {
196 fn drop(&mut self) {
197 CURRENT_APP_CONTEXT.with(|stack| {
198 stack.borrow_mut().pop();
199 });
200 }
201}
202
203impl AppContext {
204 pub fn new() -> Rc<Self> {
205 Self::new_with_density(1.0)
206 }
207
208 pub fn new_with_density(density: f32) -> Rc<Self> {
209 let context = Rc::new(Self {
210 id: next_app_context_id(),
211 self_weak: RefCell::new(Weak::new()),
212 state: RenderState::new_with_density(density),
213 draw_observer: new_draw_observer(),
214 text: crate::text::measure::TextService::new(),
215 layout_frame_arena: RefCell::new(crate::layout::FrameLayoutArena::default()),
216 layout_cache_epoch: AtomicU64::new(1),
217 last_fling_velocity_bits: AtomicU32::new(0.0f32.to_bits()),
218 scroll_motion_contexts: crate::scroll::ScrollMotionContextStore::new(),
219 layout_node_registry: crate::widgets::nodes::layout_node::LayoutNodeRegistryState::new(
220 ),
221 pointer_dispatch: crate::pointer_dispatch::PointerDispatchState::new(),
222 focus_dispatch: crate::focus_dispatch::FocusInvalidationState::new(),
223 semantics_dispatch: crate::semantics_dispatch::SemanticsInvalidationState::new(),
224 cursor_animation: crate::cursor_animation::CursorAnimationState::new(),
225 text_field_focus: crate::text_field_focus::TextFieldFocusState::new(),
226 text_input_session: crate::text_input_session::PlatformTextInputState::new(),
227 clipboard_session: crate::clipboard_session::ClipboardSessionState::new(),
228 pointer_input_tasks: crate::modifier::pointer_input::PointerInputTaskRegistry::new(),
229 modifier_chain_trace: RefCell::new(None),
230 });
231 *context.self_weak.borrow_mut() = Rc::downgrade(&context);
232 APP_CONTEXTS.with(|contexts| {
233 contexts
234 .borrow_mut()
235 .insert(context.id, Rc::downgrade(&context));
236 });
237 context
238 }
239
240 pub fn enter<R>(self: &Rc<Self>, block: impl FnOnce() -> R) -> R {
241 let _scope = self.enter_scope();
242 block()
243 }
244
245 #[doc(hidden)]
246 pub fn enter_scope(self: &Rc<Self>) -> AppContextScope {
247 CURRENT_APP_CONTEXT.with(|stack| {
248 stack.borrow_mut().push(Rc::downgrade(self));
249 });
250 AppContextScope
251 }
252
253 pub fn set_text_measurer<M: crate::text::TextMeasurer>(&self, measurer: M) {
254 self.set_text_measurer_rc(Rc::new(measurer));
255 }
256
257 pub fn set_text_measurer_rc(&self, measurer: Rc<dyn crate::text::TextMeasurer>) {
258 self.text.set_measurer(measurer);
259 self.layout_cache_epoch.fetch_add(1, Ordering::Relaxed);
260 self.state.layout_invalidated.store(true, Ordering::Relaxed);
261 self.state.render_invalidated.store(true, Ordering::Relaxed);
262 }
263
264 #[doc(hidden)]
265 pub fn downgrade(&self) -> Weak<Self> {
266 self.self_weak.borrow().clone()
267 }
268}
269
270impl Drop for AppContext {
271 fn drop(&mut self) {
272 let id = self.id;
273 let _ = APP_CONTEXTS.try_with(|contexts| {
274 contexts.borrow_mut().remove(&id);
275 });
276 }
277}
278
279fn app_context_by_id(id: AppContextId) -> Option<Rc<AppContext>> {
280 APP_CONTEXTS
281 .try_with(|contexts| {
282 let context = contexts.borrow().get(&id).cloned()?;
283 let Some(context) = context.upgrade() else {
284 contexts.borrow_mut().remove(&id);
285 return None;
286 };
287 Some(context)
288 })
289 .ok()
290 .flatten()
291}
292
293#[cfg(test)]
294fn app_context_registry_entry_count() -> usize {
295 APP_CONTEXTS
296 .try_with(|contexts| contexts.borrow().len())
297 .unwrap_or_default()
298}
299
300fn with_app_context_by_id<R>(id: AppContextId, f: impl FnOnce(&Rc<AppContext>) -> R) -> Option<R> {
301 app_context_by_id(id).map(|context| f(&context))
302}
303
304pub(crate) fn current_app_context_id() -> AppContextId {
305 require_current_app_context("app context identity access").id
306}
307
308pub(crate) fn with_layout_node_registry_by_app_context<R>(
309 id: AppContextId,
310 f: impl FnOnce(&crate::widgets::nodes::layout_node::LayoutNodeRegistryState) -> R,
311) -> Option<R> {
312 with_app_context_by_id(id, |context| f(&context.layout_node_registry))
313}
314
315pub(crate) fn enter_app_context_by_id<R>(id: AppContextId, f: impl FnOnce() -> R) -> Option<R> {
316 with_app_context_by_id(id, |context| context.enter(f))
317}
318
319pub(crate) fn current_app_context() -> Option<Rc<AppContext>> {
320 CURRENT_APP_CONTEXT
321 .try_with(|stack| {
322 let mut stack = stack.borrow_mut();
323 loop {
324 let context = stack.last()?;
325 if let Some(context) = context.upgrade() {
326 return Some(context);
327 }
328 stack.pop();
329 }
330 })
331 .ok()
332 .flatten()
333}
334
335#[doc(hidden)]
336pub fn has_current_app_context() -> bool {
337 current_app_context().is_some()
338}
339
340fn require_current_app_context(operation: &str) -> Rc<AppContext> {
341 if let Some(context) = current_app_context() {
342 return context;
343 }
344 require_current_app_context_without_scope(operation)
345}
346
347fn require_current_app_context_without_scope(operation: &str) -> Rc<AppContext> {
348 panic!("{operation} requires an active AppContext")
349}
350
351fn with_render_state<R>(f: impl FnOnce(&RenderState) -> R) -> R {
352 let context = require_current_app_context("render state access");
353 f(&context.state)
354}
355
356fn normalize_density(density: f32) -> f32 {
357 if density.is_finite() && density > 0.0 {
358 density
359 } else {
360 1.0
361 }
362}
363
364fn normalize_font_scale(scale: f32) -> f32 {
371 if scale.is_finite() && scale > 0.0 {
372 scale.clamp(MIN_FONT_SCALE, MAX_FONT_SCALE)
373 } else {
374 1.0
375 }
376}
377
378pub const MIN_FONT_SCALE: f32 = 0.5;
381pub const MAX_FONT_SCALE: f32 = 3.0;
384
385pub(crate) fn with_text_measurer<R>(f: impl FnOnce(&dyn crate::text::TextMeasurer) -> R) -> R {
386 let context = require_current_app_context("text measurer access");
387 context.text.with_measurer(f)
388}
389
390pub(crate) fn with_text_service<R>(f: impl FnOnce(&crate::text::measure::TextService) -> R) -> R {
391 let context = require_current_app_context("text service access");
392 f(&context.text)
393}
394
395pub(crate) fn set_current_text_measurer(measurer: Rc<dyn crate::text::TextMeasurer>) {
396 let Some(context) = current_app_context() else {
397 panic!("set_text_measurer requires an active AppContext");
398 };
399 context.set_text_measurer_rc(measurer);
400}
401
402pub(crate) fn set_modifier_chain_trace(callback: Arc<ModifierChainTraceCallback>) -> AppContextId {
403 let context = require_current_app_context("modifier chain trace installation");
404 *context.modifier_chain_trace.borrow_mut() = Some(callback);
405 context.id
406}
407
408pub(crate) fn clear_modifier_chain_trace(context_id: AppContextId) {
409 let _ = with_app_context_by_id(context_id, |context| {
410 *context.modifier_chain_trace.borrow_mut() = None;
411 });
412}
413
414pub(crate) fn emit_modifier_chain_trace(nodes: &[crate::modifier::ModifierChainInspectorNode]) {
415 let Some(context) = current_app_context() else {
416 return;
417 };
418 let callback = context.modifier_chain_trace.borrow().clone();
419 if let Some(callback) = callback {
420 callback(nodes);
421 }
422}
423
424pub(crate) fn take_layout_frame_arena() -> crate::layout::FrameLayoutArena {
425 let context = require_current_app_context("layout frame arena access");
426
427 std::mem::take(&mut *context.layout_frame_arena.borrow_mut())
428}
429
430pub(crate) fn replace_layout_frame_arena(arena: crate::layout::FrameLayoutArena) {
431 let context = require_current_app_context("layout frame arena access");
432 *context.layout_frame_arena.borrow_mut() = arena;
433}
434
435pub(crate) fn invalidate_layout_cache_epoch() {
436 let context = require_current_app_context("layout cache epoch access");
437 context.layout_cache_epoch.fetch_add(1, Ordering::Relaxed);
438}
439
440pub(crate) fn next_layout_cache_epoch() -> u64 {
441 let context = require_current_app_context("layout cache epoch access");
442 context.layout_cache_epoch.fetch_add(1, Ordering::Relaxed)
443}
444
445pub(crate) fn current_layout_cache_epoch() -> u64 {
446 let context = require_current_app_context("layout cache epoch access");
447 context.layout_cache_epoch.load(Ordering::Relaxed)
448}
449
450pub(crate) fn record_last_fling_velocity(velocity: f32) {
451 if let Some(context) = current_app_context() {
452 context
453 .last_fling_velocity_bits
454 .store(velocity.to_bits(), Ordering::Relaxed);
455 }
456}
457
458#[doc(hidden)]
459pub fn debug_last_fling_velocity() -> f32 {
460 let context = require_current_app_context("fling velocity diagnostics access");
461 f32::from_bits(context.last_fling_velocity_bits.load(Ordering::Relaxed))
462}
463
464#[doc(hidden)]
465pub fn debug_reset_last_fling_velocity() {
466 let context = require_current_app_context("fling velocity diagnostics access");
467 context
468 .last_fling_velocity_bits
469 .store(0.0f32.to_bits(), Ordering::Relaxed);
470}
471
472pub(crate) fn with_scroll_motion_context_store<R>(
473 f: impl FnOnce(&crate::scroll::ScrollMotionContextStore) -> R,
474) -> R {
475 let context = require_current_app_context("scroll motion context access");
476 f(&context.scroll_motion_contexts)
477}
478
479#[doc(hidden)]
480pub fn clear_transient_scroll_motion_contexts() {
481 let Some(context) = current_app_context() else {
482 return;
483 };
484 context.scroll_motion_contexts.clear_transient_after_frame();
485}
486
487#[cfg(test)]
488pub(crate) fn layout_frame_arena_placement_scratch_count() -> usize {
489 let context = require_current_app_context("layout frame arena access");
490
491 context
492 .layout_frame_arena
493 .borrow()
494 .available_placement_scratch_count()
495}
496
497pub(crate) fn with_layout_node_registry<R>(
498 f: impl FnOnce(&crate::widgets::nodes::layout_node::LayoutNodeRegistryState) -> R,
499) -> R {
500 let context = require_current_app_context("layout node registry access");
501 f(&context.layout_node_registry)
502}
503
504pub(crate) fn with_pointer_dispatch<R>(
505 f: impl FnOnce(&crate::pointer_dispatch::PointerDispatchState) -> R,
506) -> R {
507 let context = require_current_app_context("pointer dispatch access");
508 f(&context.pointer_dispatch)
509}
510
511pub(crate) fn with_focus_dispatch<R>(
512 f: impl FnOnce(&crate::focus_dispatch::FocusInvalidationState) -> R,
513) -> R {
514 let context = require_current_app_context("focus dispatch access");
515 f(&context.focus_dispatch)
516}
517
518pub(crate) fn with_semantics_dispatch<R>(
519 f: impl FnOnce(&crate::semantics_dispatch::SemanticsInvalidationState) -> R,
520) -> R {
521 let context = require_current_app_context("semantics dispatch access");
522 f(&context.semantics_dispatch)
523}
524
525pub(crate) fn with_semantics_dispatch_by_app_context(
534 id: AppContextId,
535 f: impl FnOnce(&crate::semantics_dispatch::SemanticsInvalidationState),
536) {
537 with_app_context_by_id(id, |context| f(&context.semantics_dispatch));
538}
539
540pub(crate) fn current_app_context_id_opt() -> Option<AppContextId> {
542 current_app_context().map(|context| context.id)
543}
544
545pub(crate) fn with_cursor_animation<R>(
546 f: impl FnOnce(&crate::cursor_animation::CursorAnimationState) -> R,
547) -> R {
548 let context = require_current_app_context("cursor animation access");
549 f(&context.cursor_animation)
550}
551
552pub(crate) fn with_text_field_focus<R>(
553 f: impl FnOnce(&crate::text_field_focus::TextFieldFocusState) -> R,
554) -> R {
555 let context = require_current_app_context("text field focus access");
556 f(&context.text_field_focus)
557}
558
559pub(crate) fn with_text_input_session<R>(
560 f: impl FnOnce(&crate::text_input_session::PlatformTextInputState) -> R,
561) -> R {
562 let context = require_current_app_context("platform text input session access");
563 f(&context.text_input_session)
564}
565
566pub(crate) fn with_clipboard_session<R>(
567 f: impl FnOnce(&crate::clipboard_session::ClipboardSessionState) -> R,
568) -> R {
569 let context = require_current_app_context("clipboard session access");
570 f(&context.clipboard_session)
571}
572
573pub(crate) fn register_pointer_input_task(
574 task_id: u64,
575 task: Rc<crate::modifier::pointer_input::PointerInputTaskInner>,
576) -> crate::modifier::pointer_input::PointerInputTaskOwner {
577 let context = require_current_app_context("pointer input task registration");
578 context.pointer_input_tasks.insert(task_id, task);
579 crate::modifier::pointer_input::PointerInputTaskOwner::App(context.id)
580}
581
582pub(crate) fn remove_pointer_input_task(
583 owner: crate::modifier::pointer_input::PointerInputTaskOwner,
584 task_id: u64,
585) {
586 match owner {
587 crate::modifier::pointer_input::PointerInputTaskOwner::App(context_id) => {
588 let _ = with_app_context_by_id(context_id, |context| {
589 context.pointer_input_tasks.remove(task_id);
590 });
591 }
592 }
593}
594
595pub(crate) fn request_pointer_input_task_poll(
596 owner: crate::modifier::pointer_input::PointerInputTaskOwner,
597 task_id: u64,
598) {
599 match owner {
600 crate::modifier::pointer_input::PointerInputTaskOwner::App(context_id) => {
601 let _ = with_app_context_by_id(context_id, |context| {
602 context.enter(|| {
603 context.pointer_input_tasks.request_poll(task_id, owner);
604 });
605 });
606 }
607 }
608}
609
610fn with_draw_observer<R>(f: impl FnOnce(&SnapshotStateObserver) -> R) -> R {
611 let context = require_current_app_context("draw observer access");
612 f(&context.draw_observer)
613}
614
615struct LayoutRepassManager {
620 dirty_nodes: HashSet<NodeId>,
621}
622
623impl LayoutRepassManager {
624 fn new() -> Self {
625 Self {
626 dirty_nodes: HashSet::new(),
627 }
628 }
629
630 fn schedule_repass(&mut self, node_id: NodeId) {
631 self.dirty_nodes.insert(node_id);
632 }
633
634 fn has_pending_repass(&self) -> bool {
635 !self.dirty_nodes.is_empty()
636 }
637
638 fn take_dirty_nodes(&mut self) -> Vec<NodeId> {
639 self.dirty_nodes.drain().collect()
640 }
641
642 fn dirty_nodes_snapshot(&self) -> Vec<NodeId> {
643 let mut nodes = self.dirty_nodes.iter().copied().collect::<Vec<_>>();
644 nodes.sort_unstable();
645 nodes
646 }
647}
648
649struct DrawRepassManager {
651 dirty_nodes: HashSet<NodeId>,
652}
653
654impl DrawRepassManager {
655 fn new() -> Self {
656 Self {
657 dirty_nodes: HashSet::new(),
658 }
659 }
660
661 fn schedule_repass(&mut self, node_id: NodeId) {
662 self.dirty_nodes.insert(node_id);
663 }
664
665 fn has_pending_repass(&self) -> bool {
666 !self.dirty_nodes.is_empty()
667 }
668
669 fn take_dirty_nodes(&mut self) -> Vec<NodeId> {
670 self.dirty_nodes.drain().collect()
671 }
672}
673
674fn lock_repass_manager<T>(manager: &Mutex<T>) -> MutexGuard<'_, T> {
675 manager
676 .lock()
677 .unwrap_or_else(|poisoned| poisoned.into_inner())
678}
679
680#[track_caller]
699pub fn schedule_layout_repass(node_id: NodeId) {
700 if layout_repass_schedule_diagnostics_enabled_for(node_id) {
701 let caller = std::panic::Location::caller();
702 log::warn!(
703 "[layout-repass-schedule] node={} caller={}:{}:{}",
704 node_id,
705 caller.file(),
706 caller.line(),
707 caller.column()
708 );
709 }
710 with_render_state(|state| {
711 lock_repass_manager(&state.layout_repasses).schedule_repass(node_id);
712 state.layout_invalidated.store(true, Ordering::Relaxed);
713 });
714 request_render_invalidation();
721}
722
723#[derive(Clone, Copy)]
724enum LayoutRepassScheduleDiag {
725 Disabled,
726 All,
727 Node(NodeId),
728}
729
730fn layout_repass_schedule_diagnostics_enabled_for(node_id: NodeId) -> bool {
731 static MODE: std::sync::OnceLock<LayoutRepassScheduleDiag> = std::sync::OnceLock::new();
732 match *MODE.get_or_init(|| {
733 let Some(value) = std::env::var_os("CRANPOSE_LAYOUT_REPASS_SCHEDULE_DIAG") else {
734 return LayoutRepassScheduleDiag::Disabled;
735 };
736 if value == "all" {
737 return LayoutRepassScheduleDiag::All;
738 }
739 value
740 .to_string_lossy()
741 .parse::<NodeId>()
742 .map(LayoutRepassScheduleDiag::Node)
743 .unwrap_or(LayoutRepassScheduleDiag::Disabled)
744 }) {
745 LayoutRepassScheduleDiag::Disabled => false,
746 LayoutRepassScheduleDiag::All => true,
747 LayoutRepassScheduleDiag::Node(target) => target == node_id,
748 }
749}
750
751pub(crate) fn schedule_modifier_slices_repass(node_id: NodeId) {
752 with_render_state(|state| {
753 lock_repass_manager(&state.modifier_slice_repasses).schedule_repass(node_id);
754 });
755 schedule_draw_repass(node_id);
756}
757
758pub fn schedule_draw_repass(node_id: NodeId) {
763 let context = require_current_app_context("render state access");
764 schedule_draw_repass_in_context(&context, node_id);
765}
766
767fn schedule_draw_repass_for_app_context(context_id: AppContextId, node_id: NodeId) {
768 let _ = with_app_context_by_id(context_id, |context| {
769 schedule_draw_repass_in_context(context, node_id);
770 });
771}
772
773fn schedule_draw_repass_in_context(context: &AppContext, node_id: NodeId) {
774 lock_repass_manager(&context.state.draw_repasses).schedule_repass(node_id);
775 context
776 .state
777 .render_invalidated
778 .store(true, Ordering::Relaxed);
779}
780
781pub fn has_pending_draw_repasses() -> bool {
783 with_render_state(|state| lock_repass_manager(&state.draw_repasses).has_pending_repass())
784}
785
786pub fn take_draw_repass_nodes() -> Vec<NodeId> {
788 with_render_state(|state| lock_repass_manager(&state.draw_repasses).take_dirty_nodes())
789}
790
791pub fn has_pending_layout_repasses() -> bool {
793 with_render_state(|state| lock_repass_manager(&state.layout_repasses).has_pending_repass())
794}
795
796pub fn pending_layout_repass_nodes_snapshot() -> Vec<NodeId> {
798 with_render_state(|state| lock_repass_manager(&state.layout_repasses).dirty_nodes_snapshot())
799}
800
801pub fn take_layout_repass_nodes() -> Vec<NodeId> {
805 with_render_state(|state| lock_repass_manager(&state.layout_repasses).take_dirty_nodes())
806}
807
808pub fn schedule_measure_repass(node_id: NodeId) {
817 with_render_state(|state| {
818 lock_repass_manager(&state.measure_repasses).schedule_repass(node_id);
819 state.layout_invalidated.store(true, Ordering::Relaxed);
820 });
821 request_render_invalidation();
822}
823
824pub fn has_pending_measure_repasses() -> bool {
826 with_render_state(|state| lock_repass_manager(&state.measure_repasses).has_pending_repass())
827}
828
829pub fn pending_measure_repass_nodes_snapshot() -> Vec<NodeId> {
838 with_render_state(|state| lock_repass_manager(&state.measure_repasses).dirty_nodes_snapshot())
839}
840
841pub fn take_measure_repass_nodes() -> Vec<NodeId> {
845 with_render_state(|state| lock_repass_manager(&state.measure_repasses).take_dirty_nodes())
846}
847
848pub(crate) fn take_modifier_slice_repass_nodes() -> Vec<NodeId> {
849 with_render_state(|state| {
850 lock_repass_manager(&state.modifier_slice_repasses).take_dirty_nodes()
851 })
852}
853
854pub(crate) fn record_geometry_scene_node(node_id: NodeId) {
859 with_render_state(|state| {
860 lock_repass_manager(&state.geometry_scene_nodes).schedule_repass(node_id);
861 });
862}
863
864pub fn take_geometry_scene_nodes() -> Vec<NodeId> {
870 with_render_state(|state| lock_repass_manager(&state.geometry_scene_nodes).take_dirty_nodes())
871}
872
873pub fn current_density() -> f32 {
875 with_render_state(|state| f32::from_bits(state.density_bits.load(Ordering::Relaxed)))
876}
877
878pub fn set_density(density: f32) {
883 let normalized = normalize_density(density);
884 let new_bits = normalized.to_bits();
885 with_render_state(|state| {
886 let old_bits = state.density_bits.swap(new_bits, Ordering::Relaxed);
887 if old_bits != new_bits {
888 state.layout_invalidated.store(true, Ordering::Relaxed);
889 }
890 });
891}
892
893pub fn current_font_scale() -> f32 {
904 current_font_scale_curve().scale()
905}
906
907pub fn current_font_scale_curve() -> crate::font_scale::FontScaleCurve {
913 with_render_state(|state| *lock_font_scale(&state.font_scale))
914}
915
916pub fn scale_sp(sp: f32) -> f32 {
918 current_font_scale_curve().sp_to_dp(sp)
919}
920
921pub fn set_font_scale(scale: f32) {
931 set_font_scale_curve(crate::font_scale::FontScaleCurve::linear(scale));
932}
933
934pub fn set_font_scale_curve(curve: crate::font_scale::FontScaleCurve) {
942 let normalized = normalize_font_scale(curve.scale());
943 let curve = if (normalized - curve.scale()).abs() <= f32::EPSILON {
944 curve
945 } else {
946 crate::font_scale::FontScaleCurve::linear(normalized)
947 };
948 with_render_state(|state| {
949 let mut current = lock_font_scale(&state.font_scale);
950 if *current != curve {
951 *current = curve;
952 state.layout_invalidated.store(true, Ordering::Relaxed);
953 }
954 });
955}
956
957fn lock_font_scale(
958 slot: &Mutex<crate::font_scale::FontScaleCurve>,
959) -> MutexGuard<'_, crate::font_scale::FontScaleCurve> {
960 match slot.lock() {
961 Ok(guard) => guard,
962 Err(poisoned) => poisoned.into_inner(),
963 }
964}
965
966pub fn request_render_invalidation() {
968 with_render_state(|state| state.render_invalidated.store(true, Ordering::Relaxed));
969}
970
971pub fn take_render_invalidation() -> bool {
973 with_render_state(|state| state.render_invalidated.swap(false, Ordering::Relaxed))
974}
975
976pub fn peek_render_invalidation() -> bool {
978 with_render_state(|state| state.render_invalidated.load(Ordering::Relaxed))
979}
980
981pub fn request_pointer_invalidation() {
983 with_render_state(|state| state.pointer_invalidated.store(true, Ordering::Relaxed));
984}
985
986pub fn take_pointer_invalidation() -> bool {
988 with_render_state(|state| state.pointer_invalidated.swap(false, Ordering::Relaxed))
989}
990
991pub fn peek_pointer_invalidation() -> bool {
993 with_render_state(|state| state.pointer_invalidated.load(Ordering::Relaxed))
994}
995
996pub fn request_focus_invalidation() {
998 with_render_state(|state| state.focus_invalidated.store(true, Ordering::Relaxed));
999}
1000
1001pub fn take_focus_invalidation() -> bool {
1003 with_render_state(|state| state.focus_invalidated.swap(false, Ordering::Relaxed))
1004}
1005
1006pub fn peek_focus_invalidation() -> bool {
1008 with_render_state(|state| state.focus_invalidated.load(Ordering::Relaxed))
1009}
1010
1011pub fn request_layout_invalidation() {
1038 with_render_state(|state| state.layout_invalidated.store(true, Ordering::Relaxed));
1039}
1040
1041pub fn take_layout_invalidation() -> bool {
1043 with_render_state(|state| state.layout_invalidated.swap(false, Ordering::Relaxed))
1044}
1045
1046pub fn peek_layout_invalidation() -> bool {
1048 with_render_state(|state| state.layout_invalidated.load(Ordering::Relaxed))
1049}
1050
1051#[cfg(any(test, feature = "test-helpers"))]
1052#[doc(hidden)]
1053pub fn reset_render_state_for_tests() {
1054 let _ = take_draw_repass_nodes();
1055 let _ = take_layout_repass_nodes();
1056 let _ = take_modifier_slice_repass_nodes();
1057 let _ = take_render_invalidation();
1058 let _ = take_pointer_invalidation();
1059 let _ = take_focus_invalidation();
1060 let _ = take_layout_invalidation();
1061 debug_reset_last_fling_velocity();
1062 set_density(1.0);
1063 set_font_scale(1.0);
1064 let _ = take_layout_invalidation();
1065}
1066
1067#[cfg(test)]
1068pub(crate) struct TestAppContextScope {
1069 _scope: AppContextScope,
1070 _context: Rc<AppContext>,
1071}
1072
1073#[cfg(test)]
1074pub(crate) fn app_context_test_scope() -> TestAppContextScope {
1075 let context = AppContext::new();
1076 let scope = context.enter_scope();
1077 context.enter(reset_render_state_for_tests);
1078 TestAppContextScope {
1079 _scope: scope,
1080 _context: context,
1081 }
1082}
1083
1084#[cfg(test)]
1085pub(crate) struct RenderStateTestGuard {
1086 _app_scope: TestAppContextScope,
1087 _lock: std::sync::MutexGuard<'static, ()>,
1088}
1089
1090#[cfg(test)]
1091pub(crate) fn render_state_test_guard() -> RenderStateTestGuard {
1092 static TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
1093 let lock = match TEST_LOCK.get_or_init(|| Mutex::new(())).lock() {
1094 Ok(guard) => guard,
1095 Err(poisoned) => poisoned.into_inner(),
1096 };
1097 RenderStateTestGuard {
1098 _app_scope: app_context_test_scope(),
1099 _lock: lock,
1100 }
1101}
1102
1103#[cfg(test)]
1104mod tests {
1105 use std::sync::{Arc, mpsc};
1106
1107 use super::*;
1108 use crate::text::{AnnotatedString, TextLayoutResult, TextMeasurer, TextMetrics, TextStyle};
1109
1110 #[test]
1115 fn a_draw_closure_can_name_its_own_node_for_the_next_frame() {
1116 let _scope = app_context_test_scope();
1117 let _ = take_draw_repass_nodes();
1118 let _ = take_render_invalidation();
1119
1120 observe_draw_reads(DrawObservationScope::new(33, 0), || {
1121 request_current_draw_redraw();
1122 });
1123 assert_eq!(
1124 take_draw_repass_nodes(),
1125 vec![33],
1126 "the recording node must be scheduled for re-record"
1127 );
1128 assert!(
1129 take_render_invalidation(),
1130 "the next frame must be requested"
1131 );
1132
1133 request_current_draw_redraw();
1136 assert!(take_draw_repass_nodes().is_empty());
1137 assert!(take_render_invalidation());
1138 }
1139
1140 struct TestTextMeasurer;
1141
1142 impl TextMeasurer for TestTextMeasurer {
1143 fn measure(&self, text: &AnnotatedString, _style: &TextStyle) -> TextMetrics {
1144 TextMetrics {
1145 width: text.text.len() as f32,
1146 height: 1.0,
1147 line_height: 1.0,
1148 line_count: 1,
1149 }
1150 }
1151
1152 fn get_offset_for_position(
1153 &self,
1154 text: &AnnotatedString,
1155 _style: &TextStyle,
1156 x: f32,
1157 _y: f32,
1158 ) -> usize {
1159 x.round().max(0.0) as usize % text.text.len().max(1)
1160 }
1161
1162 fn get_cursor_x_for_offset(
1163 &self,
1164 _text: &AnnotatedString,
1165 _style: &TextStyle,
1166 offset: usize,
1167 ) -> f32 {
1168 offset as f32
1169 }
1170
1171 fn layout(&self, text: &AnnotatedString, _style: &TextStyle) -> TextLayoutResult {
1172 TextLayoutResult::monospaced(&text.text, 1.0, 1.0)
1173 }
1174 }
1175
1176 #[test]
1177 fn app_context_ids_do_not_use_process_global_counter() {
1178 let source = include_str!("render_state.rs");
1179 assert!(!source.contains(concat!("NEXT_", "APP_CONTEXT_ID: Atomic")));
1180 }
1181
1182 #[test]
1183 fn app_context_ids_are_unique_within_thread_registry() {
1184 let first = AppContext::new();
1185 let second = AppContext::new();
1186
1187 assert_ne!(first.id, second.id);
1188 assert!(app_context_by_id(first.id).is_some());
1189 assert!(app_context_by_id(second.id).is_some());
1190 }
1191
1192 #[test]
1193 fn set_text_measurer_requires_active_app_context() {
1194 let result = std::panic::catch_unwind(|| {
1195 crate::text::set_text_measurer(TestTextMeasurer);
1196 });
1197 assert!(result.is_err());
1198
1199 let context = AppContext::new();
1200 context.enter(|| {
1201 crate::text::set_text_measurer(TestTextMeasurer);
1202 });
1203 }
1204
1205 #[test]
1206 fn the_font_scale_starts_at_one_and_invalidates_layout_when_it_moves() {
1207 let context = AppContext::new();
1208 context.enter(|| {
1209 assert_eq!(current_font_scale(), 1.0);
1210 let _ = take_layout_invalidation();
1211
1212 set_font_scale(1.3);
1213 assert_eq!(current_font_scale(), 1.3);
1214 assert!(
1215 take_layout_invalidation(),
1216 "every Sp on screen just changed size"
1217 );
1218
1219 set_font_scale(1.3);
1222 assert!(!take_layout_invalidation());
1223 });
1224 }
1225
1226 #[test]
1227 fn a_font_scale_no_platform_reports_is_refused() {
1228 let context = AppContext::new();
1229 context.enter(|| {
1230 for nonsense in [0.0, -1.0, f32::NAN, f32::INFINITY] {
1231 set_font_scale(1.0);
1232 set_font_scale(nonsense);
1233 assert_eq!(current_font_scale(), 1.0, "{nonsense} was let through");
1234 }
1235 set_font_scale(99.0);
1236 assert_eq!(current_font_scale(), MAX_FONT_SCALE);
1237 set_font_scale(0.01);
1238 assert_eq!(current_font_scale(), MIN_FONT_SCALE);
1239 });
1240 }
1241
1242 #[test]
1243 fn the_font_scale_is_per_app_context() {
1244 let first = AppContext::new();
1245 let second = AppContext::new();
1246 first.enter(|| set_font_scale(1.5));
1247 first.enter(|| assert_eq!(current_font_scale(), 1.5));
1248 second.enter(|| assert_eq!(current_font_scale(), 1.0));
1249 }
1250
1251 #[test]
1252 fn invalidation_flags_are_shared_across_threads() {
1253 let state = Arc::new(RenderState::new_with_density(1.0));
1254 let (tx, rx) = mpsc::channel();
1255 let worker_state = Arc::clone(&state);
1256
1257 let handle = std::thread::spawn(move || {
1258 worker_state
1259 .render_invalidated
1260 .store(true, Ordering::Relaxed);
1261 worker_state
1262 .pointer_invalidated
1263 .store(true, Ordering::Relaxed);
1264 worker_state
1265 .focus_invalidated
1266 .store(true, Ordering::Relaxed);
1267 worker_state
1268 .layout_invalidated
1269 .store(true, Ordering::Relaxed);
1270 worker_state
1271 .density_bits
1272 .store(f32::to_bits(2.0), Ordering::Relaxed);
1273 tx.send(()).expect("signal invalidation setup");
1274
1275 f32::from_bits(worker_state.density_bits.load(Ordering::Relaxed))
1276 });
1277
1278 rx.recv().expect("wait for worker invalidation setup");
1279 assert!(state.render_invalidated.load(Ordering::Relaxed));
1280 assert!(state.pointer_invalidated.load(Ordering::Relaxed));
1281 assert!(state.focus_invalidated.load(Ordering::Relaxed));
1282 assert!(state.layout_invalidated.load(Ordering::Relaxed));
1283 assert_eq!(
1284 f32::from_bits(state.density_bits.load(Ordering::Relaxed)),
1285 2.0
1286 );
1287 assert!(state.render_invalidated.swap(false, Ordering::Relaxed));
1288 assert!(state.pointer_invalidated.swap(false, Ordering::Relaxed));
1289 assert!(state.focus_invalidated.swap(false, Ordering::Relaxed));
1290 assert!(state.layout_invalidated.swap(false, Ordering::Relaxed));
1291
1292 let density = handle.join().expect("worker invalidation snapshot");
1293 assert_eq!(density, 2.0);
1294 assert!(!state.render_invalidated.load(Ordering::Relaxed));
1295 assert!(!state.pointer_invalidated.load(Ordering::Relaxed));
1296 assert!(!state.focus_invalidated.load(Ordering::Relaxed));
1297 assert!(!state.layout_invalidated.load(Ordering::Relaxed));
1298 }
1299
1300 #[test]
1301 fn app_contexts_keep_density_and_invalidations_isolated() {
1302 let first = AppContext::new_with_density(1.0);
1303 let second = AppContext::new_with_density(1.0);
1304
1305 first.enter(|| {
1306 set_density(2.0);
1307 request_render_invalidation();
1308 request_pointer_invalidation();
1309 schedule_layout_repass(11);
1310 schedule_draw_repass(12);
1311 });
1312
1313 second.enter(|| {
1314 assert_eq!(current_density(), 1.0);
1315 assert!(!peek_render_invalidation());
1316 assert!(!peek_pointer_invalidation());
1317 assert!(!peek_layout_invalidation());
1318 assert!(!has_pending_layout_repasses());
1319 assert!(!has_pending_draw_repasses());
1320 });
1321
1322 first.enter(|| {
1323 assert_eq!(current_density(), 2.0);
1324 assert!(peek_render_invalidation());
1325 assert!(peek_pointer_invalidation());
1326 assert!(peek_layout_invalidation());
1327 assert!(has_pending_layout_repasses());
1328 assert!(has_pending_draw_repasses());
1329 assert_eq!(take_layout_repass_nodes(), vec![11]);
1330 assert_eq!(take_draw_repass_nodes(), vec![12]);
1331 assert!(take_render_invalidation());
1332 assert!(take_pointer_invalidation());
1333 assert!(take_layout_invalidation());
1334 });
1335 }
1336
1337 #[test]
1338 fn app_contexts_keep_fling_velocity_diagnostics_isolated() {
1339 let first = AppContext::new_with_density(1.0);
1340 let second = AppContext::new_with_density(1.0);
1341
1342 first.enter(|| {
1343 record_last_fling_velocity(1200.0);
1344 assert_eq!(debug_last_fling_velocity(), 1200.0);
1345 });
1346
1347 second.enter(|| {
1348 assert_eq!(debug_last_fling_velocity(), 0.0);
1349 record_last_fling_velocity(-450.0);
1350 assert_eq!(debug_last_fling_velocity(), -450.0);
1351 });
1352
1353 first.enter(|| {
1354 assert_eq!(debug_last_fling_velocity(), 1200.0);
1355 debug_reset_last_fling_velocity();
1356 assert_eq!(debug_last_fling_velocity(), 0.0);
1357 });
1358
1359 second.enter(|| {
1360 assert_eq!(debug_last_fling_velocity(), -450.0);
1361 });
1362 }
1363
1364 #[test]
1365 fn app_context_new_uses_independent_density() {
1366 let outer = AppContext::new_with_density(2.0);
1367 let context = AppContext::new();
1368 context.enter(|| {
1369 assert_eq!(current_density(), 1.0);
1370 });
1371 outer.enter(|| {
1372 assert_eq!(current_density(), 2.0);
1373 });
1374 }
1375
1376 #[test]
1377 fn runtime_state_access_requires_explicit_app_context_even_in_tests() {
1378 let result = std::panic::catch_unwind(|| {
1379 request_render_invalidation();
1380 });
1381 assert!(result.is_err());
1382 }
1383
1384 #[test]
1385 fn app_contexts_keep_layout_frame_arenas_isolated() {
1386 let first = AppContext::new_with_density(1.0);
1387 let second = AppContext::new_with_density(1.0);
1388
1389 first.enter(|| {
1390 assert_eq!(layout_frame_arena_placement_scratch_count(), 0);
1391 let mut arena = take_layout_frame_arena();
1392 arena.seed_placement_scratch_for_test();
1393 replace_layout_frame_arena(arena);
1394 assert_eq!(layout_frame_arena_placement_scratch_count(), 1);
1395 });
1396
1397 second.enter(|| {
1398 assert_eq!(layout_frame_arena_placement_scratch_count(), 0);
1399 });
1400
1401 first.enter(|| {
1402 assert_eq!(layout_frame_arena_placement_scratch_count(), 1);
1403 });
1404 }
1405
1406 #[test]
1407 fn current_app_context_scope_does_not_extend_context_lifetime() {
1408 let weak = {
1409 let context = AppContext::new_with_density(1.0);
1410 let weak = Rc::downgrade(&context);
1411 context.enter(|| {
1412 assert!(current_app_context().is_some());
1413 });
1414 weak
1415 };
1416
1417 assert!(weak.upgrade().is_none());
1418 assert!(current_app_context().is_none());
1419 }
1420
1421 #[test]
1422 fn dropped_app_context_unregisters_from_thread_lookup_registry() {
1423 let start_count = app_context_registry_entry_count();
1424
1425 let id = {
1426 let context = AppContext::new_with_density(1.0);
1427 let id = context.id;
1428 assert!(app_context_by_id(id).is_some());
1429 id
1430 };
1431
1432 assert_eq!(
1433 app_context_registry_entry_count(),
1434 start_count,
1435 "dropped AppContexts must remove their weak registry entry"
1436 );
1437 assert!(app_context_by_id(id).is_none());
1438 }
1439}