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