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