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, PoisonError,
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 layout_cache_floor: AtomicU64,
44 last_fling_velocity_bits: AtomicU32,
45 scroll_motion_contexts: crate::scroll::ScrollMotionContextStore,
46 lazy_prefetch: crate::lazy_prefetch::LazyPrefetchState,
47 layout_node_registry: crate::widgets::nodes::layout_node::LayoutNodeRegistryState,
48 pointer_dispatch: crate::pointer_dispatch::PointerDispatchState,
49 focus_dispatch: crate::focus_dispatch::FocusInvalidationState,
50 modal: crate::modal::ModalState,
51 hosted_popups: crate::widgets::popup::HostedPopupRegistries,
52 semantics_dispatch: crate::semantics_dispatch::SemanticsInvalidationState,
53 cursor_animation: crate::cursor_animation::CursorAnimationState,
54 text_field_focus: crate::text_field_focus::TextFieldFocusState,
55 text_input_session: crate::text_input_session::PlatformTextInputState,
56 clipboard_session: crate::clipboard_session::ClipboardSessionState,
57 pointer_icon: crate::pointer_icon_session::PointerIconState,
58 pointer_input_tasks: crate::modifier::pointer_input::PointerInputTaskRegistry,
59 modifier_chain_trace: RefCell<Option<Arc<ModifierChainTraceCallback>>>,
60 window_roots: crate::modifier::WindowRootRegistry,
61 drag_and_drop: crate::modifier::DragAndDropState,
62}
63
64#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
65pub(crate) struct AppContextId(u64);
66
67#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
68pub(crate) struct DrawObservationScope {
69 node_id: NodeId,
70 command_index: usize,
71}
72
73impl DrawObservationScope {
74 pub(crate) fn new(node_id: NodeId, command_index: usize) -> Self {
75 Self {
76 node_id,
77 command_index,
78 }
79 }
80}
81
82fn new_draw_observer() -> SnapshotStateObserver {
83 let observer = SnapshotStateObserver::new(|callback| {
84 if let Some(runtime) = current_runtime_handle() {
85 runtime.enqueue_ui_task(callback);
86 } else {
87 callback();
88 }
89 });
90 observer.start();
91 observer
92}
93
94thread_local! {
95 static CURRENT_DRAW_NODE: std::cell::Cell<Option<NodeId>> = const { std::cell::Cell::new(None) };
96}
97
98struct CurrentDrawNodeGuard {
99 previous: Option<NodeId>,
100}
101
102impl Drop for CurrentDrawNodeGuard {
103 fn drop(&mut self) {
104 CURRENT_DRAW_NODE.with(|current| current.set(self.previous));
105 }
106}
107
108pub(crate) fn observe_draw_reads<R>(scope: DrawObservationScope, block: impl FnOnce() -> R) -> R {
109 let context = require_current_app_context("draw observer access");
110 let context_id = context.id;
111 let _guard = CurrentDrawNodeGuard {
112 previous: CURRENT_DRAW_NODE.with(|current| current.replace(Some(scope.node_id))),
113 };
114 context.draw_observer.observe_reads(
115 scope,
116 move |scope| {
117 schedule_draw_repass_for_app_context(context_id, scope.node_id);
118 },
119 block,
120 )
121}
122
123pub fn request_current_draw_redraw() {
130 if let Some(node_id) = CURRENT_DRAW_NODE.with(Cell::get) {
131 schedule_draw_repass(node_id);
132 }
133 request_render_invalidation();
134}
135
136pub(crate) fn clear_draw_observations_for_node(node_id: NodeId) {
137 with_draw_observer(|observer| {
138 observer.clear_if(|scope| {
139 scope
140 .downcast_ref::<DrawObservationScope>()
141 .is_some_and(|scope| scope.node_id == node_id)
142 });
143 });
144}
145
146pub fn prune_draw_observations_to_nodes(retained: &HashSet<NodeId>) {
148 with_draw_observer(|observer| {
149 observer.clear_if(|scope| {
150 scope
151 .downcast_ref::<DrawObservationScope>()
152 .is_some_and(|scope| !retained.contains(&scope.node_id))
153 });
154 });
155}
156
157impl RenderState {
158 fn new_with_density(density: f32) -> Self {
159 Self {
160 layout_repasses: Mutex::new(LayoutRepassManager::new()),
161 measure_repasses: Mutex::new(LayoutRepassManager::new()),
162 draw_repasses: Mutex::new(DrawRepassManager::new()),
163 modifier_slice_repasses: Mutex::new(LayoutRepassManager::new()),
164 geometry_scene_nodes: Mutex::new(LayoutRepassManager::new()),
165 render_invalidated: AtomicBool::new(false),
166 pointer_invalidated: AtomicBool::new(false),
167 focus_invalidated: AtomicBool::new(false),
168 layout_invalidated: AtomicBool::new(false),
169 density_bits: AtomicU32::new(normalize_density(density).to_bits()),
170 font_scale: Mutex::new(crate::font_scale::FontScaleCurve::linear(1.0)),
171 }
172 }
173}
174
175std::thread_local! {
176 static NEXT_APP_CONTEXT_ID: Cell<u64> = const { Cell::new(1) };
177 static CURRENT_APP_CONTEXT: RefCell<Vec<Weak<AppContext>>> = const { RefCell::new(Vec::new()) };
178 static APP_CONTEXTS: RefCell<HashMap<AppContextId, Weak<AppContext>>> = RefCell::new(HashMap::new());
179}
180
181fn next_app_context_id() -> AppContextId {
182 NEXT_APP_CONTEXT_ID.with(|next| {
183 let id = next.get();
184 next.set(id.wrapping_add(1));
185 AppContextId(id)
186 })
187}
188
189#[doc(hidden)]
190pub struct AppContextScope;
191
192impl Drop for AppContextScope {
193 fn drop(&mut self) {
194 CURRENT_APP_CONTEXT.with(|stack| {
195 stack.borrow_mut().pop();
196 });
197 }
198}
199
200impl AppContext {
201 pub fn new() -> Rc<Self> {
202 Self::new_with_density(1.0)
203 }
204
205 pub fn new_with_density(density: f32) -> Rc<Self> {
206 let context = Rc::new(Self {
207 id: next_app_context_id(),
208 self_weak: RefCell::new(Weak::new()),
209 state: RenderState::new_with_density(density),
210 draw_observer: new_draw_observer(),
211 text: crate::text::measure::TextService::new(),
212 layout_frame_arena: RefCell::new(crate::layout::FrameLayoutArena::default()),
213 layout_cache_epoch: AtomicU64::new(1),
214 layout_cache_floor: AtomicU64::new(0),
215 last_fling_velocity_bits: AtomicU32::new(0.0f32.to_bits()),
216 scroll_motion_contexts: crate::scroll::ScrollMotionContextStore::new(),
217 lazy_prefetch: crate::lazy_prefetch::LazyPrefetchState::new(),
218 layout_node_registry: crate::widgets::nodes::layout_node::LayoutNodeRegistryState::new(
219 ),
220 pointer_dispatch: crate::pointer_dispatch::PointerDispatchState::new(),
221 focus_dispatch: crate::focus_dispatch::FocusInvalidationState::new(),
222 modal: crate::modal::ModalState::new(),
223 hosted_popups: crate::widgets::popup::HostedPopupRegistries::default(),
224 semantics_dispatch: crate::semantics_dispatch::SemanticsInvalidationState::new(),
225 cursor_animation: crate::cursor_animation::CursorAnimationState::new(),
226 text_field_focus: crate::text_field_focus::TextFieldFocusState::new(),
227 text_input_session: crate::text_input_session::PlatformTextInputState::new(),
228 clipboard_session: crate::clipboard_session::ClipboardSessionState::new(),
229 pointer_icon: crate::pointer_icon_session::PointerIconState::new(),
230 pointer_input_tasks: crate::modifier::pointer_input::PointerInputTaskRegistry::new(),
231 modifier_chain_trace: RefCell::new(None),
232 window_roots: crate::modifier::WindowRootRegistry::default(),
233 drag_and_drop: crate::modifier::DragAndDropState::default(),
234 });
235 *context.self_weak.borrow_mut() = Rc::downgrade(&context);
236 APP_CONTEXTS.with(|contexts| {
237 contexts
238 .borrow_mut()
239 .insert(context.id, Rc::downgrade(&context));
240 });
241 context
242 }
243
244 pub fn enter<R>(self: &Rc<Self>, block: impl FnOnce() -> R) -> R {
245 let _scope = self.enter_scope();
246 block()
247 }
248
249 pub(crate) fn id(&self) -> AppContextId {
250 self.id
251 }
252
253 pub fn window_roots(&self) -> &crate::modifier::WindowRootRegistry {
255 &self.window_roots
256 }
257
258 pub fn drag_and_drop(&self) -> &crate::modifier::DragAndDropState {
260 &self.drag_and_drop
261 }
262
263 #[doc(hidden)]
264 pub fn enter_scope(self: &Rc<Self>) -> AppContextScope {
265 CURRENT_APP_CONTEXT.with(|stack| {
266 stack.borrow_mut().push(Rc::downgrade(self));
267 });
268 AppContextScope
269 }
270
271 pub fn set_text_measurer<M: crate::text::TextMeasurer>(&self, measurer: M) {
272 self.set_text_measurer_rc(Rc::new(measurer));
273 }
274
275 pub fn set_text_measurer_rc(&self, measurer: Rc<dyn crate::text::TextMeasurer>) {
276 self.text.set_measurer(measurer);
277 self.invalidate_layout_caches();
278 self.state.layout_invalidated.store(true, Ordering::Relaxed);
279 self.state.render_invalidated.store(true, Ordering::Relaxed);
280 }
281
282 fn invalidate_layout_caches(&self) {
283 let floor = self.layout_cache_epoch.fetch_add(1, Ordering::Relaxed) + 1;
284 self.layout_cache_floor.store(floor, Ordering::Relaxed);
285 }
286
287 #[doc(hidden)]
288 pub fn downgrade(&self) -> Weak<Self> {
289 self.self_weak.borrow().clone()
290 }
291}
292
293impl Drop for AppContext {
294 fn drop(&mut self) {
295 let id = self.id;
296 let _ = APP_CONTEXTS.try_with(|contexts| {
297 contexts.borrow_mut().remove(&id);
298 });
299 }
300}
301
302fn app_context_by_id(id: AppContextId) -> Option<Rc<AppContext>> {
303 APP_CONTEXTS
304 .try_with(|contexts| {
305 let context = contexts.borrow().get(&id).cloned()?;
306 let Some(context) = context.upgrade() else {
307 contexts.borrow_mut().remove(&id);
308 return None;
309 };
310 Some(context)
311 })
312 .ok()
313 .flatten()
314}
315
316#[cfg(test)]
317fn app_context_registry_entry_count() -> usize {
318 APP_CONTEXTS
319 .try_with(|contexts| contexts.borrow().len())
320 .unwrap_or_default()
321}
322
323pub(crate) fn with_app_context_by_id<R>(
324 id: AppContextId,
325 f: impl FnOnce(&Rc<AppContext>) -> R,
326) -> Option<R> {
327 app_context_by_id(id).map(|context| f(&context))
328}
329
330pub(crate) fn current_app_context_id() -> AppContextId {
331 require_current_app_context("app context identity access").id
332}
333
334pub(crate) fn with_layout_node_registry_by_app_context<R>(
335 id: AppContextId,
336 f: impl FnOnce(&crate::widgets::nodes::layout_node::LayoutNodeRegistryState) -> R,
337) -> Option<R> {
338 with_app_context_by_id(id, |context| f(&context.layout_node_registry))
339}
340
341pub(crate) fn enter_app_context_by_id<R>(id: AppContextId, f: impl FnOnce() -> R) -> Option<R> {
342 with_app_context_by_id(id, |context| context.enter(f))
343}
344
345pub(crate) fn current_app_context() -> Option<Rc<AppContext>> {
346 CURRENT_APP_CONTEXT
347 .try_with(|stack| {
348 let mut stack = stack.borrow_mut();
349 loop {
350 let context = stack.last()?;
351 if let Some(context) = context.upgrade() {
352 return Some(context);
353 }
354 stack.pop();
355 }
356 })
357 .ok()
358 .flatten()
359}
360
361#[doc(hidden)]
362pub fn has_current_app_context() -> bool {
363 current_app_context().is_some()
364}
365
366pub(crate) fn require_current_app_context(operation: &str) -> Rc<AppContext> {
367 if let Some(context) = current_app_context() {
368 return context;
369 }
370 require_current_app_context_without_scope(operation)
371}
372
373fn require_current_app_context_without_scope(operation: &str) -> Rc<AppContext> {
374 panic!("{operation} requires an active AppContext")
375}
376
377fn with_render_state<R>(f: impl FnOnce(&RenderState) -> R) -> R {
378 let context = require_current_app_context("render state access");
379 f(&context.state)
380}
381
382fn normalize_density(density: f32) -> f32 {
383 if density.is_finite() && density > 0.0 {
384 density
385 } else {
386 1.0
387 }
388}
389
390fn normalize_font_scale(scale: f32) -> f32 {
391 if scale.is_finite() && scale > 0.0 {
392 scale.clamp(MIN_FONT_SCALE, MAX_FONT_SCALE)
393 } else {
394 1.0
395 }
396}
397
398pub const MIN_FONT_SCALE: f32 = 0.5;
401pub const MAX_FONT_SCALE: f32 = 3.0;
404
405pub(crate) fn with_text_measurer<R>(f: impl FnOnce(&dyn crate::text::TextMeasurer) -> R) -> R {
406 let context = require_current_app_context("text measurer access");
407 context.text.with_measurer(f)
408}
409
410pub(crate) fn with_text_service<R>(f: impl FnOnce(&crate::text::measure::TextService) -> R) -> R {
411 let context = require_current_app_context("text service access");
412 f(&context.text)
413}
414
415pub(crate) fn set_current_text_measurer(measurer: Rc<dyn crate::text::TextMeasurer>) {
416 let Some(context) = current_app_context() else {
417 panic!("set_text_measurer requires an active AppContext");
418 };
419 context.set_text_measurer_rc(measurer);
420}
421
422pub(crate) fn set_modifier_chain_trace(callback: Arc<ModifierChainTraceCallback>) -> AppContextId {
423 let context = require_current_app_context("modifier chain trace installation");
424 *context.modifier_chain_trace.borrow_mut() = Some(callback);
425 context.id
426}
427
428pub(crate) fn clear_modifier_chain_trace(context_id: AppContextId) {
429 let _ = with_app_context_by_id(context_id, |context| {
430 *context.modifier_chain_trace.borrow_mut() = None;
431 });
432}
433
434pub(crate) fn emit_modifier_chain_trace(nodes: &[crate::modifier::ModifierChainInspectorNode]) {
435 let Some(context) = current_app_context() else {
436 return;
437 };
438 let callback = context.modifier_chain_trace.borrow().clone();
439 if let Some(callback) = callback {
440 callback(nodes);
441 }
442}
443
444pub(crate) fn take_layout_frame_arena() -> crate::layout::FrameLayoutArena {
445 let context = require_current_app_context("layout frame arena access");
446
447 std::mem::take(&mut *context.layout_frame_arena.borrow_mut())
448}
449
450pub(crate) fn replace_layout_frame_arena(arena: crate::layout::FrameLayoutArena) {
451 let context = require_current_app_context("layout frame arena access");
452 *context.layout_frame_arena.borrow_mut() = arena;
453}
454
455pub(crate) fn invalidate_layout_cache_epoch() {
456 require_current_app_context("layout cache epoch access").invalidate_layout_caches();
457}
458
459pub(crate) fn next_layout_cache_epoch() -> u64 {
460 let context = require_current_app_context("layout cache epoch access");
461 context.layout_cache_epoch.fetch_add(1, Ordering::Relaxed)
462}
463
464pub(crate) fn current_layout_cache_epoch() -> u64 {
465 let context = require_current_app_context("layout cache epoch access");
466 context.layout_cache_epoch.load(Ordering::Relaxed)
467}
468
469pub(crate) fn layout_cache_floor() -> u64 {
470 let context = require_current_app_context("layout cache epoch access");
471 context.layout_cache_floor.load(Ordering::Relaxed)
472}
473
474pub(crate) fn record_last_fling_velocity(velocity: f32) {
475 if let Some(context) = current_app_context() {
476 context
477 .last_fling_velocity_bits
478 .store(velocity.to_bits(), Ordering::Relaxed);
479 }
480}
481
482#[doc(hidden)]
483pub fn debug_last_fling_velocity() -> f32 {
484 let context = require_current_app_context("fling velocity diagnostics access");
485 f32::from_bits(context.last_fling_velocity_bits.load(Ordering::Relaxed))
486}
487
488#[doc(hidden)]
489pub fn debug_reset_last_fling_velocity() {
490 let context = require_current_app_context("fling velocity diagnostics access");
491 context
492 .last_fling_velocity_bits
493 .store(0.0f32.to_bits(), Ordering::Relaxed);
494}
495
496impl AppContext {
497 pub(crate) fn lazy_prefetch(&self) -> &crate::lazy_prefetch::LazyPrefetchState {
498 &self.lazy_prefetch
499 }
500}
501
502pub(crate) fn with_scroll_motion_context_store<R>(
503 f: impl FnOnce(&crate::scroll::ScrollMotionContextStore) -> R,
504) -> R {
505 let context = require_current_app_context("scroll motion context access");
506 f(&context.scroll_motion_contexts)
507}
508
509#[doc(hidden)]
510pub fn clear_transient_scroll_motion_contexts() {
511 let Some(context) = current_app_context() else {
512 return;
513 };
514 context.scroll_motion_contexts.clear_transient_after_frame();
515}
516
517#[cfg(test)]
518pub(crate) fn layout_frame_arena_placement_scratch_count() -> usize {
519 let context = require_current_app_context("layout frame arena access");
520
521 context
522 .layout_frame_arena
523 .borrow()
524 .available_placement_scratch_count()
525}
526
527pub(crate) fn with_layout_node_registry<R>(
528 f: impl FnOnce(&crate::widgets::nodes::layout_node::LayoutNodeRegistryState) -> R,
529) -> R {
530 let context = require_current_app_context("layout node registry access");
531 f(&context.layout_node_registry)
532}
533
534pub(crate) fn with_pointer_dispatch<R>(
535 f: impl FnOnce(&crate::pointer_dispatch::PointerDispatchState) -> R,
536) -> R {
537 let context = require_current_app_context("pointer dispatch access");
538 f(&context.pointer_dispatch)
539}
540
541pub(crate) fn with_focus_dispatch<R>(
542 f: impl FnOnce(&crate::focus_dispatch::FocusInvalidationState) -> R,
543) -> R {
544 let context = require_current_app_context("focus dispatch access");
545 f(&context.focus_dispatch)
546}
547
548pub(crate) fn with_focus_dispatch_by_app_context<R>(
549 id: AppContextId,
550 f: impl FnOnce(&crate::focus_dispatch::FocusInvalidationState) -> R,
551) -> Option<R> {
552 with_app_context_by_id(id, |context| context.enter(|| f(&context.focus_dispatch)))
553}
554
555pub(crate) fn with_modal_state<R>(f: impl FnOnce(&crate::modal::ModalState) -> R) -> R {
556 let context = require_current_app_context("modal state access");
557 f(&context.modal)
558}
559
560pub(crate) fn with_hosted_popup_registries<R>(
561 f: impl FnOnce(&crate::widgets::popup::HostedPopupRegistries) -> R,
562) -> R {
563 let context = require_current_app_context("hosted popup registries access");
564 f(&context.hosted_popups)
565}
566
567pub(crate) fn with_semantics_dispatch<R>(
568 f: impl FnOnce(&crate::semantics_dispatch::SemanticsInvalidationState) -> R,
569) -> R {
570 let context = require_current_app_context("semantics dispatch access");
571 f(&context.semantics_dispatch)
572}
573
574pub(crate) fn with_semantics_dispatch_by_app_context(
575 id: AppContextId,
576 f: impl FnOnce(&crate::semantics_dispatch::SemanticsInvalidationState),
577) {
578 with_app_context_by_id(id, |context| f(&context.semantics_dispatch));
579}
580
581pub(crate) fn current_app_context_id_opt() -> Option<AppContextId> {
582 current_app_context().map(|context| context.id)
583}
584
585pub(crate) fn with_cursor_animation<R>(
586 f: impl FnOnce(&crate::cursor_animation::CursorAnimationState) -> R,
587) -> R {
588 let context = require_current_app_context("cursor animation access");
589 f(&context.cursor_animation)
590}
591
592pub(crate) fn with_text_field_focus<R>(
593 f: impl FnOnce(&crate::text_field_focus::TextFieldFocusState) -> R,
594) -> R {
595 let context = require_current_app_context("text field focus access");
596 f(&context.text_field_focus)
597}
598
599pub(crate) fn with_text_input_session<R>(
600 f: impl FnOnce(&crate::text_input_session::PlatformTextInputState) -> R,
601) -> R {
602 let context = require_current_app_context("platform text input session access");
603 f(&context.text_input_session)
604}
605
606pub(crate) fn with_clipboard_session<R>(
607 f: impl FnOnce(&crate::clipboard_session::ClipboardSessionState) -> R,
608) -> R {
609 let context = require_current_app_context("clipboard session access");
610 f(&context.clipboard_session)
611}
612
613pub(crate) fn with_pointer_icon_session<R>(
614 f: impl FnOnce(&crate::pointer_icon_session::PointerIconState) -> R,
615) -> R {
616 let context = require_current_app_context("pointer icon session access");
617 f(&context.pointer_icon)
618}
619
620pub(crate) fn register_pointer_input_task(
621 task_id: u64,
622 task: Rc<crate::modifier::pointer_input::PointerInputTaskInner>,
623) -> crate::modifier::pointer_input::PointerInputTaskOwner {
624 let context = require_current_app_context("pointer input task registration");
625 context.pointer_input_tasks.insert(task_id, task);
626 crate::modifier::pointer_input::PointerInputTaskOwner::App(context.id)
627}
628
629pub(crate) fn remove_pointer_input_task(
630 owner: crate::modifier::pointer_input::PointerInputTaskOwner,
631 task_id: u64,
632) {
633 match owner {
634 crate::modifier::pointer_input::PointerInputTaskOwner::App(context_id) => {
635 let _ = with_app_context_by_id(context_id, |context| {
636 context.pointer_input_tasks.remove(task_id);
637 });
638 }
639 }
640}
641
642pub(crate) fn request_pointer_input_task_poll(
643 owner: crate::modifier::pointer_input::PointerInputTaskOwner,
644 task_id: u64,
645) {
646 match owner {
647 crate::modifier::pointer_input::PointerInputTaskOwner::App(context_id) => {
648 let _ = with_app_context_by_id(context_id, |context| {
649 context.enter(|| {
650 context.pointer_input_tasks.request_poll(task_id, owner);
651 });
652 });
653 }
654 }
655}
656
657fn with_draw_observer<R>(f: impl FnOnce(&SnapshotStateObserver) -> R) -> R {
658 let context = require_current_app_context("draw observer access");
659 f(&context.draw_observer)
660}
661
662struct LayoutRepassManager {
663 dirty_nodes: HashSet<NodeId>,
664}
665
666impl LayoutRepassManager {
667 fn new() -> Self {
668 Self {
669 dirty_nodes: HashSet::default(),
670 }
671 }
672
673 fn schedule_repass(&mut self, node_id: NodeId) {
674 self.dirty_nodes.insert(node_id);
675 }
676
677 fn has_pending_repass(&self) -> bool {
678 !self.dirty_nodes.is_empty()
679 }
680
681 fn take_dirty_nodes(&mut self) -> Vec<NodeId> {
682 self.dirty_nodes.drain().collect()
683 }
684
685 fn dirty_nodes_snapshot(&self) -> Vec<NodeId> {
686 let mut nodes = self.dirty_nodes.iter().copied().collect::<Vec<_>>();
687 nodes.sort_unstable();
688 nodes
689 }
690}
691
692struct DrawRepassManager {
693 dirty_nodes: HashSet<NodeId>,
694}
695
696impl DrawRepassManager {
697 fn new() -> Self {
698 Self {
699 dirty_nodes: HashSet::default(),
700 }
701 }
702
703 fn schedule_repass(&mut self, node_id: NodeId) {
704 self.dirty_nodes.insert(node_id);
705 }
706
707 fn has_pending_repass(&self) -> bool {
708 !self.dirty_nodes.is_empty()
709 }
710
711 fn take_dirty_nodes(&mut self) -> Vec<NodeId> {
712 self.dirty_nodes.drain().collect()
713 }
714}
715
716fn lock_repass_manager<T>(manager: &Mutex<T>) -> MutexGuard<'_, T> {
717 manager.lock().unwrap_or_else(PoisonError::into_inner)
718}
719
720#[track_caller]
735pub fn schedule_layout_repass(node_id: NodeId) {
736 if layout_repass_schedule_diagnostics_enabled_for(node_id) {
737 let caller = std::panic::Location::caller();
738 log::warn!(
739 "[layout-repass-schedule] node={} caller={}:{}:{}",
740 node_id,
741 caller.file(),
742 caller.line(),
743 caller.column()
744 );
745 }
746 with_render_state(|state| {
747 lock_repass_manager(&state.layout_repasses).schedule_repass(node_id);
748 });
749 request_render_invalidation();
750}
751
752#[derive(Clone, Copy)]
753enum LayoutRepassScheduleDiag {
754 Disabled,
755 All,
756 Node(NodeId),
757}
758
759fn layout_repass_schedule_diagnostics_enabled_for(node_id: NodeId) -> bool {
760 static MODE: std::sync::OnceLock<LayoutRepassScheduleDiag> = std::sync::OnceLock::new();
761 match *MODE.get_or_init(|| {
762 let Some(value) = std::env::var_os("CRANPOSE_LAYOUT_REPASS_SCHEDULE_DIAG") else {
763 return LayoutRepassScheduleDiag::Disabled;
764 };
765 if value == "all" {
766 return LayoutRepassScheduleDiag::All;
767 }
768 value.to_string_lossy().parse::<NodeId>().map_or(
769 LayoutRepassScheduleDiag::Disabled,
770 LayoutRepassScheduleDiag::Node,
771 )
772 }) {
773 LayoutRepassScheduleDiag::Disabled => false,
774 LayoutRepassScheduleDiag::All => true,
775 LayoutRepassScheduleDiag::Node(target) => target == node_id,
776 }
777}
778
779pub(crate) fn schedule_modifier_slices_repass(node_id: NodeId) {
780 with_render_state(|state| {
781 lock_repass_manager(&state.modifier_slice_repasses).schedule_repass(node_id);
782 });
783 schedule_draw_repass(node_id);
784}
785
786pub fn schedule_draw_repass(node_id: NodeId) {
791 let context = require_current_app_context("render state access");
792 schedule_draw_repass_in_context(&context, node_id);
793}
794
795fn schedule_draw_repass_for_app_context(context_id: AppContextId, node_id: NodeId) {
796 let _ = with_app_context_by_id(context_id, |context| {
797 schedule_draw_repass_in_context(context, node_id);
798 });
799}
800
801fn schedule_draw_repass_in_context(context: &AppContext, node_id: NodeId) {
802 lock_repass_manager(&context.state.draw_repasses).schedule_repass(node_id);
803 context
804 .state
805 .render_invalidated
806 .store(true, Ordering::Relaxed);
807}
808
809pub fn has_pending_draw_repasses() -> bool {
811 with_render_state(|state| lock_repass_manager(&state.draw_repasses).has_pending_repass())
812}
813
814pub fn take_draw_repass_nodes() -> Vec<NodeId> {
816 with_render_state(|state| lock_repass_manager(&state.draw_repasses).take_dirty_nodes())
817}
818
819pub fn has_pending_layout_repasses() -> bool {
821 with_render_state(|state| lock_repass_manager(&state.layout_repasses).has_pending_repass())
822}
823
824pub fn pending_layout_repass_nodes_snapshot() -> Vec<NodeId> {
826 with_render_state(|state| lock_repass_manager(&state.layout_repasses).dirty_nodes_snapshot())
827}
828
829pub fn take_layout_repass_nodes() -> Vec<NodeId> {
833 with_render_state(|state| lock_repass_manager(&state.layout_repasses).take_dirty_nodes())
834}
835
836pub fn schedule_measure_repass(node_id: NodeId) {
845 with_render_state(|state| {
846 lock_repass_manager(&state.measure_repasses).schedule_repass(node_id);
847 });
848 request_render_invalidation();
849}
850
851pub fn has_pending_measure_repasses() -> bool {
853 with_render_state(|state| lock_repass_manager(&state.measure_repasses).has_pending_repass())
854}
855
856pub fn pending_measure_repass_nodes_snapshot() -> Vec<NodeId> {
865 with_render_state(|state| lock_repass_manager(&state.measure_repasses).dirty_nodes_snapshot())
866}
867
868pub fn take_measure_repass_nodes() -> Vec<NodeId> {
872 with_render_state(|state| lock_repass_manager(&state.measure_repasses).take_dirty_nodes())
873}
874
875pub(crate) fn take_modifier_slice_repass_nodes() -> Vec<NodeId> {
876 with_render_state(|state| {
877 lock_repass_manager(&state.modifier_slice_repasses).take_dirty_nodes()
878 })
879}
880
881pub(crate) fn record_geometry_scene_node(node_id: NodeId) {
882 with_render_state(|state| {
883 lock_repass_manager(&state.geometry_scene_nodes).schedule_repass(node_id);
884 });
885}
886
887pub fn take_geometry_scene_nodes() -> Vec<NodeId> {
893 with_render_state(|state| lock_repass_manager(&state.geometry_scene_nodes).take_dirty_nodes())
894}
895
896pub fn current_density() -> f32 {
898 with_render_state(|state| f32::from_bits(state.density_bits.load(Ordering::Relaxed)))
899}
900
901pub fn set_density(density: f32) {
906 let normalized = normalize_density(density);
907 let new_bits = normalized.to_bits();
908 with_render_state(|state| {
909 let old_bits = state.density_bits.swap(new_bits, Ordering::Relaxed);
910 if old_bits != new_bits {
911 state.layout_invalidated.store(true, Ordering::Relaxed);
912 }
913 });
914}
915
916pub fn current_font_scale() -> f32 {
927 current_font_scale_curve().scale()
928}
929
930pub fn current_font_scale_curve() -> crate::font_scale::FontScaleCurve {
936 with_render_state(|state| *lock_font_scale(&state.font_scale))
937}
938
939pub fn scale_sp(sp: f32) -> f32 {
941 current_font_scale_curve().sp_to_dp(sp)
942}
943
944pub fn set_font_scale(scale: f32) {
954 set_font_scale_curve(crate::font_scale::FontScaleCurve::linear(scale));
955}
956
957pub fn set_font_scale_curve(curve: crate::font_scale::FontScaleCurve) {
965 let normalized = normalize_font_scale(curve.scale());
966 let curve = if (normalized - curve.scale()).abs() <= f32::EPSILON {
967 curve
968 } else {
969 crate::font_scale::FontScaleCurve::linear(normalized)
970 };
971 with_render_state(|state| {
972 let mut current = lock_font_scale(&state.font_scale);
973 if *current != curve {
974 *current = curve;
975 state.layout_invalidated.store(true, Ordering::Relaxed);
976 }
977 });
978}
979
980fn lock_font_scale(
981 slot: &Mutex<crate::font_scale::FontScaleCurve>,
982) -> MutexGuard<'_, crate::font_scale::FontScaleCurve> {
983 match slot.lock() {
984 Ok(guard) => guard,
985 Err(poisoned) => poisoned.into_inner(),
986 }
987}
988
989pub fn request_render_invalidation() {
991 with_render_state(|state| state.render_invalidated.store(true, Ordering::Relaxed));
992}
993
994pub fn take_render_invalidation() -> bool {
996 with_render_state(|state| state.render_invalidated.swap(false, Ordering::Relaxed))
997}
998
999pub fn peek_render_invalidation() -> bool {
1001 with_render_state(|state| state.render_invalidated.load(Ordering::Relaxed))
1002}
1003
1004pub fn request_pointer_invalidation() {
1006 with_render_state(|state| state.pointer_invalidated.store(true, Ordering::Relaxed));
1007}
1008
1009pub fn take_pointer_invalidation() -> bool {
1011 with_render_state(|state| state.pointer_invalidated.swap(false, Ordering::Relaxed))
1012}
1013
1014pub fn peek_pointer_invalidation() -> bool {
1016 with_render_state(|state| state.pointer_invalidated.load(Ordering::Relaxed))
1017}
1018
1019pub fn request_focus_invalidation() {
1021 with_render_state(|state| state.focus_invalidated.store(true, Ordering::Relaxed));
1022}
1023
1024pub fn take_focus_invalidation() -> bool {
1026 with_render_state(|state| state.focus_invalidated.swap(false, Ordering::Relaxed))
1027}
1028
1029pub fn peek_focus_invalidation() -> bool {
1031 with_render_state(|state| state.focus_invalidated.load(Ordering::Relaxed))
1032}
1033
1034pub fn request_layout_invalidation() {
1061 with_render_state(|state| state.layout_invalidated.store(true, Ordering::Relaxed));
1062}
1063
1064pub fn take_layout_invalidation() -> bool {
1066 with_render_state(|state| state.layout_invalidated.swap(false, Ordering::Relaxed))
1067}
1068
1069pub fn peek_layout_invalidation() -> bool {
1071 with_render_state(|state| state.layout_invalidated.load(Ordering::Relaxed))
1072}
1073
1074#[cfg(any(test, feature = "test-helpers"))]
1075#[doc(hidden)]
1076pub fn reset_render_state_for_tests() {
1077 let _ = take_draw_repass_nodes();
1078 let _ = take_layout_repass_nodes();
1079 let _ = take_modifier_slice_repass_nodes();
1080 let _ = take_render_invalidation();
1081 let _ = take_pointer_invalidation();
1082 let _ = take_focus_invalidation();
1083 let _ = take_layout_invalidation();
1084 debug_reset_last_fling_velocity();
1085 set_density(1.0);
1086 set_font_scale(1.0);
1087 let _ = take_layout_invalidation();
1088}
1089
1090#[cfg(test)]
1091pub(crate) struct TestAppContextScope {
1092 _scope: AppContextScope,
1093 _context: Rc<AppContext>,
1094}
1095
1096#[cfg(test)]
1097pub(crate) fn app_context_test_scope() -> TestAppContextScope {
1098 let context = AppContext::new();
1099 let scope = context.enter_scope();
1100 context.enter(reset_render_state_for_tests);
1101 TestAppContextScope {
1102 _scope: scope,
1103 _context: context,
1104 }
1105}
1106
1107#[cfg(test)]
1108pub(crate) struct RenderStateTestGuard {
1109 _app_scope: TestAppContextScope,
1110 _lock: std::sync::MutexGuard<'static, ()>,
1111}
1112
1113#[cfg(test)]
1114pub(crate) fn render_state_test_guard() -> RenderStateTestGuard {
1115 static TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
1116 let lock = match TEST_LOCK.get_or_init(|| Mutex::new(())).lock() {
1117 Ok(guard) => guard,
1118 Err(poisoned) => poisoned.into_inner(),
1119 };
1120 RenderStateTestGuard {
1121 _app_scope: app_context_test_scope(),
1122 _lock: lock,
1123 }
1124}
1125
1126#[cfg(test)]
1127#[path = "tests/render_state_tests.rs"]
1128mod tests;