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 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(std::cell::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::new(),
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::new(),
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
710 .lock()
711 .unwrap_or_else(|poisoned| poisoned.into_inner())
712}
713
714#[track_caller]
729pub fn schedule_layout_repass(node_id: NodeId) {
730 if layout_repass_schedule_diagnostics_enabled_for(node_id) {
731 let caller = std::panic::Location::caller();
732 log::warn!(
733 "[layout-repass-schedule] node={} caller={}:{}:{}",
734 node_id,
735 caller.file(),
736 caller.line(),
737 caller.column()
738 );
739 }
740 with_render_state(|state| {
741 lock_repass_manager(&state.layout_repasses).schedule_repass(node_id);
742 });
743 request_render_invalidation();
744}
745
746#[derive(Clone, Copy)]
747enum LayoutRepassScheduleDiag {
748 Disabled,
749 All,
750 Node(NodeId),
751}
752
753fn layout_repass_schedule_diagnostics_enabled_for(node_id: NodeId) -> bool {
754 static MODE: std::sync::OnceLock<LayoutRepassScheduleDiag> = std::sync::OnceLock::new();
755 match *MODE.get_or_init(|| {
756 let Some(value) = std::env::var_os("CRANPOSE_LAYOUT_REPASS_SCHEDULE_DIAG") else {
757 return LayoutRepassScheduleDiag::Disabled;
758 };
759 if value == "all" {
760 return LayoutRepassScheduleDiag::All;
761 }
762 value
763 .to_string_lossy()
764 .parse::<NodeId>()
765 .map(LayoutRepassScheduleDiag::Node)
766 .unwrap_or(LayoutRepassScheduleDiag::Disabled)
767 }) {
768 LayoutRepassScheduleDiag::Disabled => false,
769 LayoutRepassScheduleDiag::All => true,
770 LayoutRepassScheduleDiag::Node(target) => target == node_id,
771 }
772}
773
774pub(crate) fn schedule_modifier_slices_repass(node_id: NodeId) {
775 with_render_state(|state| {
776 lock_repass_manager(&state.modifier_slice_repasses).schedule_repass(node_id);
777 });
778 schedule_draw_repass(node_id);
779}
780
781pub fn schedule_draw_repass(node_id: NodeId) {
786 let context = require_current_app_context("render state access");
787 schedule_draw_repass_in_context(&context, node_id);
788}
789
790fn schedule_draw_repass_for_app_context(context_id: AppContextId, node_id: NodeId) {
791 let _ = with_app_context_by_id(context_id, |context| {
792 schedule_draw_repass_in_context(context, node_id);
793 });
794}
795
796fn schedule_draw_repass_in_context(context: &AppContext, node_id: NodeId) {
797 lock_repass_manager(&context.state.draw_repasses).schedule_repass(node_id);
798 context
799 .state
800 .render_invalidated
801 .store(true, Ordering::Relaxed);
802}
803
804pub fn has_pending_draw_repasses() -> bool {
806 with_render_state(|state| lock_repass_manager(&state.draw_repasses).has_pending_repass())
807}
808
809pub fn take_draw_repass_nodes() -> Vec<NodeId> {
811 with_render_state(|state| lock_repass_manager(&state.draw_repasses).take_dirty_nodes())
812}
813
814pub fn has_pending_layout_repasses() -> bool {
816 with_render_state(|state| lock_repass_manager(&state.layout_repasses).has_pending_repass())
817}
818
819pub fn pending_layout_repass_nodes_snapshot() -> Vec<NodeId> {
821 with_render_state(|state| lock_repass_manager(&state.layout_repasses).dirty_nodes_snapshot())
822}
823
824pub fn take_layout_repass_nodes() -> Vec<NodeId> {
828 with_render_state(|state| lock_repass_manager(&state.layout_repasses).take_dirty_nodes())
829}
830
831pub fn schedule_measure_repass(node_id: NodeId) {
840 with_render_state(|state| {
841 lock_repass_manager(&state.measure_repasses).schedule_repass(node_id);
842 });
843 request_render_invalidation();
844}
845
846pub fn has_pending_measure_repasses() -> bool {
848 with_render_state(|state| lock_repass_manager(&state.measure_repasses).has_pending_repass())
849}
850
851pub fn pending_measure_repass_nodes_snapshot() -> Vec<NodeId> {
860 with_render_state(|state| lock_repass_manager(&state.measure_repasses).dirty_nodes_snapshot())
861}
862
863pub fn take_measure_repass_nodes() -> Vec<NodeId> {
867 with_render_state(|state| lock_repass_manager(&state.measure_repasses).take_dirty_nodes())
868}
869
870pub(crate) fn take_modifier_slice_repass_nodes() -> Vec<NodeId> {
871 with_render_state(|state| {
872 lock_repass_manager(&state.modifier_slice_repasses).take_dirty_nodes()
873 })
874}
875
876pub(crate) fn record_geometry_scene_node(node_id: NodeId) {
877 with_render_state(|state| {
878 lock_repass_manager(&state.geometry_scene_nodes).schedule_repass(node_id);
879 });
880}
881
882pub fn take_geometry_scene_nodes() -> Vec<NodeId> {
888 with_render_state(|state| lock_repass_manager(&state.geometry_scene_nodes).take_dirty_nodes())
889}
890
891pub fn current_density() -> f32 {
893 with_render_state(|state| f32::from_bits(state.density_bits.load(Ordering::Relaxed)))
894}
895
896pub fn set_density(density: f32) {
901 let normalized = normalize_density(density);
902 let new_bits = normalized.to_bits();
903 with_render_state(|state| {
904 let old_bits = state.density_bits.swap(new_bits, Ordering::Relaxed);
905 if old_bits != new_bits {
906 state.layout_invalidated.store(true, Ordering::Relaxed);
907 }
908 });
909}
910
911pub fn current_font_scale() -> f32 {
922 current_font_scale_curve().scale()
923}
924
925pub fn current_font_scale_curve() -> crate::font_scale::FontScaleCurve {
931 with_render_state(|state| *lock_font_scale(&state.font_scale))
932}
933
934pub fn scale_sp(sp: f32) -> f32 {
936 current_font_scale_curve().sp_to_dp(sp)
937}
938
939pub fn set_font_scale(scale: f32) {
949 set_font_scale_curve(crate::font_scale::FontScaleCurve::linear(scale));
950}
951
952pub fn set_font_scale_curve(curve: crate::font_scale::FontScaleCurve) {
960 let normalized = normalize_font_scale(curve.scale());
961 let curve = if (normalized - curve.scale()).abs() <= f32::EPSILON {
962 curve
963 } else {
964 crate::font_scale::FontScaleCurve::linear(normalized)
965 };
966 with_render_state(|state| {
967 let mut current = lock_font_scale(&state.font_scale);
968 if *current != curve {
969 *current = curve;
970 state.layout_invalidated.store(true, Ordering::Relaxed);
971 }
972 });
973}
974
975fn lock_font_scale(
976 slot: &Mutex<crate::font_scale::FontScaleCurve>,
977) -> MutexGuard<'_, crate::font_scale::FontScaleCurve> {
978 match slot.lock() {
979 Ok(guard) => guard,
980 Err(poisoned) => poisoned.into_inner(),
981 }
982}
983
984pub fn request_render_invalidation() {
986 with_render_state(|state| state.render_invalidated.store(true, Ordering::Relaxed));
987}
988
989pub fn take_render_invalidation() -> bool {
991 with_render_state(|state| state.render_invalidated.swap(false, Ordering::Relaxed))
992}
993
994pub fn peek_render_invalidation() -> bool {
996 with_render_state(|state| state.render_invalidated.load(Ordering::Relaxed))
997}
998
999pub fn request_pointer_invalidation() {
1001 with_render_state(|state| state.pointer_invalidated.store(true, Ordering::Relaxed));
1002}
1003
1004pub fn take_pointer_invalidation() -> bool {
1006 with_render_state(|state| state.pointer_invalidated.swap(false, Ordering::Relaxed))
1007}
1008
1009pub fn peek_pointer_invalidation() -> bool {
1011 with_render_state(|state| state.pointer_invalidated.load(Ordering::Relaxed))
1012}
1013
1014pub fn request_focus_invalidation() {
1016 with_render_state(|state| state.focus_invalidated.store(true, Ordering::Relaxed));
1017}
1018
1019pub fn take_focus_invalidation() -> bool {
1021 with_render_state(|state| state.focus_invalidated.swap(false, Ordering::Relaxed))
1022}
1023
1024pub fn peek_focus_invalidation() -> bool {
1026 with_render_state(|state| state.focus_invalidated.load(Ordering::Relaxed))
1027}
1028
1029pub fn request_layout_invalidation() {
1056 with_render_state(|state| state.layout_invalidated.store(true, Ordering::Relaxed));
1057}
1058
1059pub fn take_layout_invalidation() -> bool {
1061 with_render_state(|state| state.layout_invalidated.swap(false, Ordering::Relaxed))
1062}
1063
1064pub fn peek_layout_invalidation() -> bool {
1066 with_render_state(|state| state.layout_invalidated.load(Ordering::Relaxed))
1067}
1068
1069#[cfg(any(test, feature = "test-helpers"))]
1070#[doc(hidden)]
1071pub fn reset_render_state_for_tests() {
1072 let _ = take_draw_repass_nodes();
1073 let _ = take_layout_repass_nodes();
1074 let _ = take_modifier_slice_repass_nodes();
1075 let _ = take_render_invalidation();
1076 let _ = take_pointer_invalidation();
1077 let _ = take_focus_invalidation();
1078 let _ = take_layout_invalidation();
1079 debug_reset_last_fling_velocity();
1080 set_density(1.0);
1081 set_font_scale(1.0);
1082 let _ = take_layout_invalidation();
1083}
1084
1085#[cfg(test)]
1086pub(crate) struct TestAppContextScope {
1087 _scope: AppContextScope,
1088 _context: Rc<AppContext>,
1089}
1090
1091#[cfg(test)]
1092pub(crate) fn app_context_test_scope() -> TestAppContextScope {
1093 let context = AppContext::new();
1094 let scope = context.enter_scope();
1095 context.enter(reset_render_state_for_tests);
1096 TestAppContextScope {
1097 _scope: scope,
1098 _context: context,
1099 }
1100}
1101
1102#[cfg(test)]
1103pub(crate) struct RenderStateTestGuard {
1104 _app_scope: TestAppContextScope,
1105 _lock: std::sync::MutexGuard<'static, ()>,
1106}
1107
1108#[cfg(test)]
1109pub(crate) fn render_state_test_guard() -> RenderStateTestGuard {
1110 static TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
1111 let lock = match TEST_LOCK.get_or_init(|| Mutex::new(())).lock() {
1112 Ok(guard) => guard,
1113 Err(poisoned) => poisoned.into_inner(),
1114 };
1115 RenderStateTestGuard {
1116 _app_scope: app_context_test_scope(),
1117 _lock: lock,
1118 }
1119}
1120
1121#[cfg(test)]
1122#[path = "tests/render_state_tests.rs"]
1123mod tests;