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