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