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