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