1#[cfg(test)]
2use std::sync::{Mutex, OnceLock};
3use std::{
4 cell::{Cell, RefCell},
5 collections::HashMap,
6 rc::{Rc, Weak},
7 sync::Arc,
8};
9
10use cranpose_core::{
11 NodeId, SnapshotStateObserver, collections::map::HashSet, current_runtime_handle,
12};
13
14pub(crate) type ModifierChainTraceCallback =
15 dyn Fn(&[crate::modifier::ModifierChainInspectorNode]) + Send + Sync + 'static;
16
17struct RenderState {
18 layout_repasses: RefCell<DirtyNodeSet>,
19 measure_repasses: RefCell<DirtyNodeSet>,
20 draw_repasses: RefCell<DirtyNodeSet>,
21 layer_property_repasses: RefCell<DirtyNodeSet>,
22 modifier_slice_repasses: RefCell<DirtyNodeSet>,
23 geometry_scene_nodes: RefCell<DirtyNodeSet>,
24 moved_scene_nodes: RefCell<DirtyNodeSet>,
25 render_invalidated: Cell<bool>,
26 pointer_invalidated: Cell<bool>,
27 focus_invalidated: Cell<bool>,
28 layout_invalidated: Cell<bool>,
29 density_bits: Cell<u32>,
30 font_scale: Cell<crate::font_scale::FontScaleCurve>,
31 font_scale_identity: Cell<bool>,
34 font_scale_fingerprint: Cell<u32>,
36}
37
38#[doc(hidden)]
39pub struct AppContext {
40 id: AppContextId,
41 self_weak: RefCell<Weak<AppContext>>,
42 state: RenderState,
43 draw_observer: SnapshotStateObserver,
44 text: crate::text::measure::TextService,
45 layout_frame_arena: RefCell<crate::layout::FrameLayoutArena>,
46 layout_cache_epoch: Cell<u64>,
47 layout_cache_floor: Cell<u64>,
48 last_fling_velocity_bits: Cell<u32>,
49 scroll_motion_contexts: crate::scroll::ScrollMotionContextStore,
50 lazy_prefetch: crate::lazy_prefetch::LazyPrefetchState,
51 layout_node_registry: crate::widgets::nodes::layout_node::LayoutNodeRegistryState,
52 pointer_dispatch: crate::pointer_dispatch::PointerDispatchState,
53 mouse_input: crate::mouse_input::MouseInputQueue,
54 focus_dispatch: crate::focus_dispatch::FocusInvalidationState,
55 modal: crate::modal::ModalState,
56 modal_nodes: crate::modal_nodes::ModalNodes,
57 semantics_layout: crate::semantics_layout_log::SemanticsLayoutLog,
58 hosted_popups: crate::widgets::popup::HostedPopupRegistries,
59 semantics_dispatch: crate::semantics_dispatch::SemanticsInvalidationState,
60 cursor_animation: crate::cursor_animation::CursorAnimationState,
61 text_field_focus: crate::text_field_focus::TextFieldFocusState,
62 text_input_session: crate::text_input_session::PlatformTextInputState,
63 clipboard_session: crate::clipboard_session::ClipboardSessionState,
64 pointer_icon: crate::pointer_icon_session::PointerIconState,
65 pointer_input_tasks: crate::modifier::pointer_input::PointerInputTaskRegistry,
66 modifier_chain_trace: RefCell<Option<Arc<ModifierChainTraceCallback>>>,
67 window_roots: crate::modifier::WindowRootRegistry,
68 drag_and_drop: crate::modifier::DragAndDropState,
69}
70
71#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
74pub(crate) struct AppContextId(std::num::NonZeroU64);
75
76#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
77pub(crate) struct DrawObservationScope {
78 node_id: NodeId,
79 modifier_index: usize,
80 command_index: usize,
81 kind: DrawObservationKind,
82}
83
84#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
85enum DrawObservationKind {
86 Content,
87 LayerProperties,
88}
89
90impl DrawObservationScope {
91 pub(crate) fn new(node_id: NodeId, modifier_index: usize, command_index: usize) -> Self {
92 Self {
93 node_id,
94 modifier_index,
95 command_index,
96 kind: DrawObservationKind::Content,
97 }
98 }
99
100 pub(crate) fn layer_properties(node_id: NodeId, modifier_index: usize) -> Self {
101 Self {
102 node_id,
103 modifier_index,
104 command_index: 0,
105 kind: DrawObservationKind::LayerProperties,
106 }
107 }
108}
109
110fn new_draw_observer() -> SnapshotStateObserver {
111 let observer = SnapshotStateObserver::new(|callback| {
112 if let Some(runtime) = current_runtime_handle() {
113 runtime.enqueue_ui_task(callback);
114 } else {
115 callback();
116 }
117 });
118 observer.start();
119 observer
120}
121
122thread_local! {
123 static CURRENT_DRAW_NODE: std::cell::Cell<Option<NodeId>> = const { std::cell::Cell::new(None) };
124}
125
126struct CurrentDrawNodeGuard {
127 previous: Option<NodeId>,
128}
129
130impl Drop for CurrentDrawNodeGuard {
131 fn drop(&mut self) {
132 CURRENT_DRAW_NODE.with(|current| current.set(self.previous));
133 }
134}
135
136pub(crate) fn observe_draw_reads<R>(scope: DrawObservationScope, block: impl FnOnce() -> R) -> R {
137 let context = require_current_app_context("draw observer access");
138 let context_id = context.id;
139 let _guard = CurrentDrawNodeGuard {
140 previous: CURRENT_DRAW_NODE.with(|current| current.replace(Some(scope.node_id))),
141 };
142 context.draw_observer.observe_reads(
143 scope,
144 move |scope| match scope.kind {
145 DrawObservationKind::Content => {
146 schedule_draw_repass_for_app_context(context_id, scope.node_id);
147 }
148 DrawObservationKind::LayerProperties => {
149 schedule_layer_property_repass_for_app_context(context_id, scope.node_id);
150 }
151 },
152 block,
153 )
154}
155
156pub fn request_current_draw_redraw() {
163 if let Some(node_id) = CURRENT_DRAW_NODE.with(Cell::get) {
164 schedule_draw_repass(node_id);
165 }
166 request_render_invalidation();
167}
168
169pub(crate) fn clear_draw_observations_for_node(node_id: NodeId) {
170 with_draw_observer(|observer| {
171 observer.clear_if(|scope| {
172 scope
173 .downcast_ref::<DrawObservationScope>()
174 .is_some_and(|scope| scope.node_id == node_id)
175 });
176 });
177}
178
179pub fn prune_draw_observations_to_nodes(retained: &HashSet<NodeId>) {
181 with_draw_observer(|observer| {
182 observer.clear_if(|scope| {
183 scope
184 .downcast_ref::<DrawObservationScope>()
185 .is_some_and(|scope| !retained.contains(&scope.node_id))
186 });
187 });
188}
189
190impl RenderState {
191 fn new_with_density(density: f32) -> Self {
192 Self {
193 layout_repasses: RefCell::new(DirtyNodeSet::new()),
194 measure_repasses: RefCell::new(DirtyNodeSet::new()),
195 draw_repasses: RefCell::new(DirtyNodeSet::new()),
196 layer_property_repasses: RefCell::new(DirtyNodeSet::new()),
197 modifier_slice_repasses: RefCell::new(DirtyNodeSet::new()),
198 geometry_scene_nodes: RefCell::new(DirtyNodeSet::new()),
199 moved_scene_nodes: RefCell::new(DirtyNodeSet::new()),
200 render_invalidated: Cell::new(false),
201 pointer_invalidated: Cell::new(false),
202 focus_invalidated: Cell::new(false),
203 layout_invalidated: Cell::new(false),
204 density_bits: Cell::new(normalize_density(density).to_bits()),
205 font_scale: Cell::new(crate::font_scale::FontScaleCurve::linear(1.0)),
206 font_scale_identity: Cell::new(true),
207 font_scale_fingerprint: Cell::new(
208 crate::font_scale::FontScaleCurve::linear(1.0).fingerprint(),
209 ),
210 }
211 }
212}
213
214std::thread_local! {
215 static NEXT_APP_CONTEXT_ID: Cell<std::num::NonZeroU64> = const { Cell::new(std::num::NonZeroU64::MIN) };
216 static CURRENT_APP_CONTEXT: RefCell<Vec<Weak<AppContext>>> = const { RefCell::new(Vec::new()) };
217 static APP_CONTEXTS: RefCell<HashMap<AppContextId, Weak<AppContext>>> = RefCell::new(HashMap::new());
218}
219
220fn next_app_context_id() -> AppContextId {
221 NEXT_APP_CONTEXT_ID.with(|next| {
222 let id = next.get();
223 next.set(id.checked_add(1).unwrap_or(std::num::NonZeroU64::MIN));
224 AppContextId(id)
225 })
226}
227
228#[doc(hidden)]
229pub struct AppContextScope;
230
231impl Drop for AppContextScope {
232 fn drop(&mut self) {
233 CURRENT_APP_CONTEXT.with(|stack| {
234 stack.borrow_mut().pop();
235 });
236 }
237}
238
239impl AppContext {
240 pub fn new() -> Rc<Self> {
241 Self::new_with_density(1.0)
242 }
243
244 pub fn new_with_density(density: f32) -> Rc<Self> {
245 let context = Rc::new(Self {
246 id: next_app_context_id(),
247 self_weak: RefCell::new(Weak::new()),
248 state: RenderState::new_with_density(density),
249 draw_observer: new_draw_observer(),
250 text: crate::text::measure::TextService::new(),
251 layout_frame_arena: RefCell::new(crate::layout::FrameLayoutArena::default()),
252 layout_cache_epoch: Cell::new(1),
253 layout_cache_floor: Cell::new(0),
254 last_fling_velocity_bits: Cell::new(0.0f32.to_bits()),
255 scroll_motion_contexts: crate::scroll::ScrollMotionContextStore::new(),
256 lazy_prefetch: crate::lazy_prefetch::LazyPrefetchState::new(),
257 layout_node_registry: crate::widgets::nodes::layout_node::LayoutNodeRegistryState::new(
258 ),
259 pointer_dispatch: crate::pointer_dispatch::PointerDispatchState::new(),
260 mouse_input: crate::mouse_input::MouseInputQueue::default(),
261 focus_dispatch: crate::focus_dispatch::FocusInvalidationState::new(),
262 modal: crate::modal::ModalState::new(),
263 modal_nodes: crate::modal_nodes::ModalNodes::default(),
264 semantics_layout: crate::semantics_layout_log::SemanticsLayoutLog::default(),
265 hosted_popups: crate::widgets::popup::HostedPopupRegistries::default(),
266 semantics_dispatch: crate::semantics_dispatch::SemanticsInvalidationState::new(),
267 cursor_animation: crate::cursor_animation::CursorAnimationState::new(),
268 text_field_focus: crate::text_field_focus::TextFieldFocusState::new(),
269 text_input_session: crate::text_input_session::PlatformTextInputState::new(),
270 clipboard_session: crate::clipboard_session::ClipboardSessionState::new(),
271 pointer_icon: crate::pointer_icon_session::PointerIconState::new(),
272 pointer_input_tasks: crate::modifier::pointer_input::PointerInputTaskRegistry::new(),
273 modifier_chain_trace: RefCell::new(None),
274 window_roots: crate::modifier::WindowRootRegistry::default(),
275 drag_and_drop: crate::modifier::DragAndDropState::default(),
276 });
277 *context.self_weak.borrow_mut() = Rc::downgrade(&context);
278 APP_CONTEXTS.with(|contexts| {
279 contexts
280 .borrow_mut()
281 .insert(context.id, Rc::downgrade(&context));
282 });
283 context
284 }
285
286 pub fn enter<R>(self: &Rc<Self>, block: impl FnOnce() -> R) -> R {
287 let _scope = self.enter_scope();
288 block()
289 }
290
291 pub(crate) fn id(&self) -> AppContextId {
292 self.id
293 }
294
295 pub(crate) fn mouse_input(&self) -> &crate::mouse_input::MouseInputQueue {
296 &self.mouse_input
297 }
298
299 #[doc(hidden)]
301 pub fn pending_mouse_moves(&self) -> usize {
302 self.mouse_input.len()
303 }
304
305 #[doc(hidden)]
307 pub fn take_mouse_move(&self) -> Option<crate::mouse_input::MouseMoveRequest> {
308 self.mouse_input.take()
309 }
310
311 pub fn window_roots(&self) -> &crate::modifier::WindowRootRegistry {
313 &self.window_roots
314 }
315
316 pub fn drag_and_drop(&self) -> &crate::modifier::DragAndDropState {
318 &self.drag_and_drop
319 }
320
321 #[doc(hidden)]
322 pub fn enter_scope(self: &Rc<Self>) -> AppContextScope {
323 CURRENT_APP_CONTEXT.with(|stack| {
324 stack.borrow_mut().push(Rc::downgrade(self));
325 });
326 AppContextScope
327 }
328
329 pub fn set_text_measurer<M: crate::text::TextMeasurer>(&self, measurer: M) {
330 self.set_text_measurer_rc(Rc::new(measurer));
331 }
332
333 pub fn set_text_measurer_rc(&self, measurer: Rc<dyn crate::text::TextMeasurer>) {
334 self.text.set_measurer(measurer);
335 self.invalidate_layout_caches();
336 self.state.layout_invalidated.set(true);
337 self.state.render_invalidated.set(true);
338 }
339
340 fn invalidate_layout_caches(&self) {
341 let floor = self.layout_cache_epoch.get() + 1;
342 self.layout_cache_epoch.set(floor);
343 self.layout_cache_floor.set(floor);
344 }
345
346 #[doc(hidden)]
347 pub fn downgrade(&self) -> Weak<Self> {
348 self.self_weak.borrow().clone()
349 }
350}
351
352impl Drop for AppContext {
353 fn drop(&mut self) {
354 let id = self.id;
355 let _ = APP_CONTEXTS.try_with(|contexts| {
356 contexts.borrow_mut().remove(&id);
357 });
358 }
359}
360
361fn app_context_by_id(id: AppContextId) -> Option<Rc<AppContext>> {
362 APP_CONTEXTS
363 .try_with(|contexts| {
364 let context = contexts.borrow().get(&id).cloned()?;
365 let Some(context) = context.upgrade() else {
366 contexts.borrow_mut().remove(&id);
367 return None;
368 };
369 Some(context)
370 })
371 .ok()
372 .flatten()
373}
374
375#[cfg(test)]
376fn app_context_registry_entry_count() -> usize {
377 APP_CONTEXTS
378 .try_with(|contexts| contexts.borrow().len())
379 .unwrap_or_default()
380}
381
382pub(crate) fn with_app_context_by_id<R>(
383 id: AppContextId,
384 f: impl FnOnce(&Rc<AppContext>) -> R,
385) -> Option<R> {
386 app_context_by_id(id).map(|context| f(&context))
387}
388
389pub(crate) fn current_app_context_id() -> AppContextId {
390 require_current_app_context("app context identity access").id
391}
392
393pub(crate) fn with_layout_node_registry_by_app_context<R>(
394 id: AppContextId,
395 f: impl FnOnce(&crate::widgets::nodes::layout_node::LayoutNodeRegistryState) -> R,
396) -> Option<R> {
397 with_app_context_by_id(id, |context| f(&context.layout_node_registry))
398}
399
400pub(crate) fn enter_app_context_by_id<R>(id: AppContextId, f: impl FnOnce() -> R) -> Option<R> {
401 with_app_context_by_id(id, |context| context.enter(f))
402}
403
404pub(crate) fn current_app_context() -> Option<Rc<AppContext>> {
405 CURRENT_APP_CONTEXT
406 .try_with(|stack| {
407 let mut stack = stack.borrow_mut();
408 loop {
409 let context = stack.last()?;
410 if let Some(context) = context.upgrade() {
411 return Some(context);
412 }
413 stack.pop();
414 }
415 })
416 .ok()
417 .flatten()
418}
419
420#[doc(hidden)]
421pub fn has_current_app_context() -> bool {
422 current_app_context().is_some()
423}
424
425pub(crate) fn require_current_app_context(operation: &str) -> Rc<AppContext> {
426 if let Some(context) = current_app_context() {
427 return context;
428 }
429 require_current_app_context_without_scope(operation)
430}
431
432fn require_current_app_context_without_scope(operation: &str) -> Rc<AppContext> {
433 panic!("{operation} requires an active AppContext")
434}
435
436fn with_render_state<R>(f: impl FnOnce(&RenderState) -> R) -> R {
437 let context = require_current_app_context("render state access");
438 f(&context.state)
439}
440
441fn normalize_density(density: f32) -> f32 {
442 if density.is_finite() && density > 0.0 {
443 density
444 } else {
445 1.0
446 }
447}
448
449fn normalize_font_scale(scale: f32) -> f32 {
450 if scale.is_finite() && scale > 0.0 {
451 scale.clamp(MIN_FONT_SCALE, MAX_FONT_SCALE)
452 } else {
453 1.0
454 }
455}
456
457pub const MIN_FONT_SCALE: f32 = 0.5;
460pub const MAX_FONT_SCALE: f32 = 3.0;
463
464pub(crate) fn with_text_measurer<R>(f: impl FnOnce(&dyn crate::text::TextMeasurer) -> R) -> R {
465 let context = require_current_app_context("text measurer access");
466 context.text.with_measurer(f)
467}
468
469pub(crate) fn begin_text_layout_pass() {
472 if let Some(context) = current_app_context() {
473 context.text.begin_layout_pass();
474 }
475}
476
477pub(crate) fn with_text_service<R>(f: impl FnOnce(&crate::text::measure::TextService) -> R) -> R {
478 let context = require_current_app_context("text service access");
479 f(&context.text)
480}
481
482pub(crate) fn set_current_text_measurer(measurer: Rc<dyn crate::text::TextMeasurer>) {
483 let Some(context) = current_app_context() else {
484 panic!("set_text_measurer requires an active AppContext");
485 };
486 context.set_text_measurer_rc(measurer);
487}
488
489pub(crate) fn set_modifier_chain_trace(callback: Arc<ModifierChainTraceCallback>) -> AppContextId {
490 let context = require_current_app_context("modifier chain trace installation");
491 *context.modifier_chain_trace.borrow_mut() = Some(callback);
492 context.id
493}
494
495pub(crate) fn clear_modifier_chain_trace(context_id: AppContextId) {
496 let _ = with_app_context_by_id(context_id, |context| {
497 *context.modifier_chain_trace.borrow_mut() = None;
498 });
499}
500
501pub(crate) fn emit_modifier_chain_trace(nodes: &[crate::modifier::ModifierChainInspectorNode]) {
502 let Some(context) = current_app_context() else {
503 return;
504 };
505 let callback = context.modifier_chain_trace.borrow().clone();
506 if let Some(callback) = callback {
507 callback(nodes);
508 }
509}
510
511pub(crate) fn take_layout_frame_arena() -> crate::layout::FrameLayoutArena {
512 let context = require_current_app_context("layout frame arena access");
513
514 std::mem::take(&mut *context.layout_frame_arena.borrow_mut())
515}
516
517pub(crate) fn replace_layout_frame_arena(arena: crate::layout::FrameLayoutArena) {
518 let context = require_current_app_context("layout frame arena access");
519 *context.layout_frame_arena.borrow_mut() = arena;
520}
521
522pub(crate) fn invalidate_layout_cache_epoch() {
523 require_current_app_context("layout cache epoch access").invalidate_layout_caches();
524}
525
526pub(crate) fn next_layout_cache_epoch() -> u64 {
527 let context = require_current_app_context("layout cache epoch access");
528 let epoch = context.layout_cache_epoch.get();
529 context.layout_cache_epoch.set(epoch + 1);
530 epoch
531}
532
533pub(crate) fn current_layout_cache_epoch() -> u64 {
534 let context = require_current_app_context("layout cache epoch access");
535 context.layout_cache_epoch.get()
536}
537
538pub(crate) fn layout_cache_floor() -> u64 {
539 let context = require_current_app_context("layout cache epoch access");
540 context.layout_cache_floor.get()
541}
542
543pub(crate) fn record_last_fling_velocity(velocity: f32) {
544 if let Some(context) = current_app_context() {
545 context.last_fling_velocity_bits.set(velocity.to_bits());
546 }
547}
548
549#[doc(hidden)]
550pub fn debug_last_fling_velocity() -> f32 {
551 let context = require_current_app_context("fling velocity diagnostics access");
552 f32::from_bits(context.last_fling_velocity_bits.get())
553}
554
555#[doc(hidden)]
556pub fn debug_reset_last_fling_velocity() {
557 let context = require_current_app_context("fling velocity diagnostics access");
558 context.last_fling_velocity_bits.set(0.0f32.to_bits());
559}
560
561impl AppContext {
562 pub(crate) fn lazy_prefetch(&self) -> &crate::lazy_prefetch::LazyPrefetchState {
563 &self.lazy_prefetch
564 }
565}
566
567pub(crate) fn with_scroll_motion_context_store<R>(
568 f: impl FnOnce(&crate::scroll::ScrollMotionContextStore) -> R,
569) -> R {
570 let context = require_current_app_context("scroll motion context access");
571 f(&context.scroll_motion_contexts)
572}
573
574#[doc(hidden)]
575pub fn clear_transient_scroll_motion_contexts() {
576 let Some(context) = current_app_context() else {
577 return;
578 };
579 context.scroll_motion_contexts.clear_transient_after_frame();
580}
581
582#[cfg(test)]
583pub(crate) fn layout_frame_arena_placement_scratch_count() -> usize {
584 let context = require_current_app_context("layout frame arena access");
585
586 context
587 .layout_frame_arena
588 .borrow()
589 .available_placement_scratch_count()
590}
591
592pub(crate) fn with_layout_node_registry<R>(
593 f: impl FnOnce(&crate::widgets::nodes::layout_node::LayoutNodeRegistryState) -> R,
594) -> R {
595 let context = require_current_app_context("layout node registry access");
596 f(&context.layout_node_registry)
597}
598
599pub(crate) fn with_pointer_dispatch<R>(
600 f: impl FnOnce(&crate::pointer_dispatch::PointerDispatchState) -> R,
601) -> R {
602 let context = require_current_app_context("pointer dispatch access");
603 f(&context.pointer_dispatch)
604}
605
606pub(crate) fn with_focus_dispatch<R>(
607 f: impl FnOnce(&crate::focus_dispatch::FocusInvalidationState) -> R,
608) -> R {
609 let context = require_current_app_context("focus dispatch access");
610 f(&context.focus_dispatch)
611}
612
613pub(crate) fn with_focus_dispatch_by_app_context<R>(
614 id: AppContextId,
615 f: impl FnOnce(&crate::focus_dispatch::FocusInvalidationState) -> R,
616) -> Option<R> {
617 with_app_context_by_id(id, |context| context.enter(|| f(&context.focus_dispatch)))
618}
619
620pub(crate) fn with_modal_state<R>(f: impl FnOnce(&crate::modal::ModalState) -> R) -> R {
621 let context = require_current_app_context("modal state access");
622 f(&context.modal)
623}
624
625pub(crate) fn with_current_modal_nodes<R>(
626 f: impl FnOnce(&crate::modal_nodes::ModalNodes) -> R,
627) -> Option<R> {
628 current_app_context().map(|context| f(&context.modal_nodes))
629}
630
631pub(crate) fn with_current_semantics_layout_log<R>(
632 f: impl FnOnce(&crate::semantics_layout_log::SemanticsLayoutLog) -> R,
633) -> Option<R> {
634 current_app_context().map(|context| f(&context.semantics_layout))
635}
636
637pub(crate) fn with_hosted_popup_registries<R>(
638 f: impl FnOnce(&crate::widgets::popup::HostedPopupRegistries) -> R,
639) -> R {
640 let context = require_current_app_context("hosted popup registries access");
641 f(&context.hosted_popups)
642}
643
644pub(crate) fn with_semantics_dispatch<R>(
645 f: impl FnOnce(&crate::semantics_dispatch::SemanticsInvalidationState) -> R,
646) -> R {
647 let context = require_current_app_context("semantics dispatch access");
648 f(&context.semantics_dispatch)
649}
650
651pub(crate) fn with_semantics_dispatch_by_app_context(
652 id: AppContextId,
653 f: impl FnOnce(&crate::semantics_dispatch::SemanticsInvalidationState),
654) {
655 with_app_context_by_id(id, |context| f(&context.semantics_dispatch));
656}
657
658pub(crate) fn current_app_context_id_opt() -> Option<AppContextId> {
659 current_app_context().map(|context| context.id)
660}
661
662pub(crate) fn with_cursor_animation<R>(
663 f: impl FnOnce(&crate::cursor_animation::CursorAnimationState) -> R,
664) -> R {
665 let context = require_current_app_context("cursor animation access");
666 f(&context.cursor_animation)
667}
668
669pub(crate) fn with_text_field_focus<R>(
670 f: impl FnOnce(&crate::text_field_focus::TextFieldFocusState) -> R,
671) -> R {
672 let context = require_current_app_context("text field focus access");
673 f(&context.text_field_focus)
674}
675
676pub(crate) fn with_text_input_session<R>(
677 f: impl FnOnce(&crate::text_input_session::PlatformTextInputState) -> R,
678) -> R {
679 let context = require_current_app_context("platform text input session access");
680 f(&context.text_input_session)
681}
682
683pub(crate) fn with_clipboard_session<R>(
684 f: impl FnOnce(&crate::clipboard_session::ClipboardSessionState) -> R,
685) -> R {
686 let context = require_current_app_context("clipboard session access");
687 f(&context.clipboard_session)
688}
689
690pub(crate) fn with_pointer_icon_session<R>(
691 f: impl FnOnce(&crate::pointer_icon_session::PointerIconState) -> R,
692) -> R {
693 let context = require_current_app_context("pointer icon session access");
694 f(&context.pointer_icon)
695}
696
697pub(crate) fn register_pointer_input_task(
698 task_id: u64,
699 task: Rc<crate::modifier::pointer_input::PointerInputTaskInner>,
700) -> crate::modifier::pointer_input::PointerInputTaskOwner {
701 let context = require_current_app_context("pointer input task registration");
702 context.pointer_input_tasks.insert(task_id, task);
703 crate::modifier::pointer_input::PointerInputTaskOwner::App(context.id)
704}
705
706pub(crate) fn remove_pointer_input_task(
707 owner: crate::modifier::pointer_input::PointerInputTaskOwner,
708 task_id: u64,
709) {
710 match owner {
711 crate::modifier::pointer_input::PointerInputTaskOwner::App(context_id) => {
712 let _ = with_app_context_by_id(context_id, |context| {
713 context.pointer_input_tasks.remove(task_id);
714 });
715 }
716 }
717}
718
719pub(crate) fn request_pointer_input_task_poll(
720 owner: crate::modifier::pointer_input::PointerInputTaskOwner,
721 task_id: u64,
722) {
723 match owner {
724 crate::modifier::pointer_input::PointerInputTaskOwner::App(context_id) => {
725 let _ = with_app_context_by_id(context_id, |context| {
726 context.enter(|| {
727 context.pointer_input_tasks.request_poll(task_id, owner);
728 });
729 });
730 }
731 }
732}
733
734fn with_draw_observer<R>(f: impl FnOnce(&SnapshotStateObserver) -> R) -> R {
735 let context = require_current_app_context("draw observer access");
736 f(&context.draw_observer)
737}
738
739struct DirtyNodeSet {
740 dirty_nodes: HashSet<NodeId>,
741}
742
743impl DirtyNodeSet {
744 fn new() -> Self {
745 Self {
746 dirty_nodes: HashSet::default(),
747 }
748 }
749
750 fn schedule_repass(&mut self, node_id: NodeId) {
751 self.dirty_nodes.insert(node_id);
752 }
753
754 fn has_pending_repass(&self) -> bool {
755 !self.dirty_nodes.is_empty()
756 }
757
758 fn take_dirty_nodes(&mut self) -> Vec<NodeId> {
759 self.dirty_nodes.drain().collect()
760 }
761
762 fn take_dirty_nodes_into(&mut self, output: &mut Vec<NodeId>) {
763 output.clear();
764 output.reserve(self.dirty_nodes.len());
765 output.extend(self.dirty_nodes.drain());
766 }
767
768 fn dirty_nodes_snapshot(&self) -> Vec<NodeId> {
769 let mut nodes = self.dirty_nodes.iter().copied().collect::<Vec<_>>();
770 nodes.sort_unstable();
771 nodes
772 }
773}
774
775#[track_caller]
790pub fn schedule_layout_repass(node_id: NodeId) {
791 if layout_repass_schedule_diagnostics_enabled_for(node_id) {
792 let caller = std::panic::Location::caller();
793 log::warn!(
794 "[layout-repass-schedule] node={} caller={}:{}:{}",
795 node_id,
796 caller.file(),
797 caller.line(),
798 caller.column()
799 );
800 }
801 with_render_state(|state| {
802 state.layout_repasses.borrow_mut().schedule_repass(node_id);
803 });
804 request_render_invalidation();
805}
806
807#[derive(Clone, Copy)]
808enum LayoutRepassScheduleDiag {
809 Disabled,
810 All,
811 Node(NodeId),
812}
813
814fn layout_repass_schedule_diagnostics_enabled_for(node_id: NodeId) -> bool {
815 static MODE: std::sync::OnceLock<LayoutRepassScheduleDiag> = std::sync::OnceLock::new();
816 match *MODE.get_or_init(|| {
817 let Some(value) = std::env::var_os("CRANPOSE_LAYOUT_REPASS_SCHEDULE_DIAG") else {
818 return LayoutRepassScheduleDiag::Disabled;
819 };
820 if value == "all" {
821 return LayoutRepassScheduleDiag::All;
822 }
823 value.to_string_lossy().parse::<NodeId>().map_or(
824 LayoutRepassScheduleDiag::Disabled,
825 LayoutRepassScheduleDiag::Node,
826 )
827 }) {
828 LayoutRepassScheduleDiag::Disabled => false,
829 LayoutRepassScheduleDiag::All => true,
830 LayoutRepassScheduleDiag::Node(target) => target == node_id,
831 }
832}
833
834pub(crate) fn schedule_modifier_slices_repass(node_id: NodeId) {
835 with_render_state(|state| {
836 state
837 .modifier_slice_repasses
838 .borrow_mut()
839 .schedule_repass(node_id);
840 });
841 schedule_draw_repass(node_id);
842}
843
844pub fn schedule_draw_repass(node_id: NodeId) {
849 let context = require_current_app_context("render state access");
850 schedule_draw_repass_in_context(&context, node_id);
851}
852
853fn schedule_draw_repass_for_app_context(context_id: AppContextId, node_id: NodeId) {
854 let _ = with_app_context_by_id(context_id, |context| {
855 schedule_draw_repass_in_context(context, node_id);
856 });
857}
858
859fn schedule_draw_repass_in_context(context: &AppContext, node_id: NodeId) {
860 context
861 .state
862 .draw_repasses
863 .borrow_mut()
864 .schedule_repass(node_id);
865 context.state.render_invalidated.set(true);
866}
867
868fn schedule_layer_property_repass_for_app_context(context_id: AppContextId, node_id: NodeId) {
869 let _ = with_app_context_by_id(context_id, |context| {
870 schedule_layer_property_repass_in_context(context, node_id);
871 });
872}
873
874fn schedule_layer_property_repass_in_context(context: &AppContext, node_id: NodeId) {
875 context
876 .state
877 .layer_property_repasses
878 .borrow_mut()
879 .schedule_repass(node_id);
880 context.state.render_invalidated.set(true);
881}
882
883pub fn has_pending_draw_repasses() -> bool {
885 with_render_state(|state| state.draw_repasses.borrow_mut().has_pending_repass())
886}
887
888pub fn take_draw_repass_nodes() -> Vec<NodeId> {
890 with_render_state(|state| state.draw_repasses.borrow_mut().take_dirty_nodes())
891}
892
893pub fn take_draw_repass_nodes_into(output: &mut Vec<NodeId>) {
904 with_render_state(|state| {
905 state
906 .draw_repasses
907 .borrow_mut()
908 .take_dirty_nodes_into(output);
909 });
910}
911
912#[doc(hidden)]
914pub fn has_pending_layer_property_repasses() -> bool {
915 with_render_state(|state| {
916 state
917 .layer_property_repasses
918 .borrow_mut()
919 .has_pending_repass()
920 })
921}
922
923#[doc(hidden)]
925pub fn take_layer_property_repass_nodes_into(output: &mut Vec<NodeId>) {
926 with_render_state(|state| {
927 state
928 .layer_property_repasses
929 .borrow_mut()
930 .take_dirty_nodes_into(output);
931 });
932}
933
934pub fn has_pending_layout_repasses() -> bool {
936 with_render_state(|state| state.layout_repasses.borrow_mut().has_pending_repass())
937}
938
939pub fn pending_layout_repass_nodes_snapshot() -> Vec<NodeId> {
941 with_render_state(|state| state.layout_repasses.borrow_mut().dirty_nodes_snapshot())
942}
943
944pub fn take_layout_repass_nodes() -> Vec<NodeId> {
948 with_render_state(|state| state.layout_repasses.borrow_mut().take_dirty_nodes())
949}
950
951pub fn schedule_measure_repass(node_id: NodeId) {
960 with_render_state(|state| {
961 state.measure_repasses.borrow_mut().schedule_repass(node_id);
962 });
963 request_render_invalidation();
964}
965
966pub fn has_pending_measure_repasses() -> bool {
968 with_render_state(|state| state.measure_repasses.borrow_mut().has_pending_repass())
969}
970
971pub fn pending_measure_repass_nodes_snapshot() -> Vec<NodeId> {
980 with_render_state(|state| state.measure_repasses.borrow_mut().dirty_nodes_snapshot())
981}
982
983pub fn take_measure_repass_nodes() -> Vec<NodeId> {
987 with_render_state(|state| state.measure_repasses.borrow_mut().take_dirty_nodes())
988}
989
990pub(crate) fn take_modifier_slice_repass_nodes() -> Vec<NodeId> {
991 with_render_state(|state| {
992 state
993 .modifier_slice_repasses
994 .borrow_mut()
995 .take_dirty_nodes()
996 })
997}
998
999pub(crate) fn record_geometry_scene_node(node_id: NodeId) {
1000 with_render_state(|state| {
1001 state
1002 .geometry_scene_nodes
1003 .borrow_mut()
1004 .schedule_repass(node_id);
1005 });
1006}
1007
1008pub(crate) fn record_moved_scene_node(node_id: NodeId) {
1009 with_render_state(|state| {
1010 state
1011 .moved_scene_nodes
1012 .borrow_mut()
1013 .schedule_repass(node_id);
1014 });
1015}
1016
1017#[derive(Clone, Debug, Default, PartialEq, Eq)]
1020pub struct GeometrySceneNodes {
1021 pub reshaped: Vec<NodeId>,
1024 pub moved: Vec<NodeId>,
1027}
1028
1029impl GeometrySceneNodes {
1030 pub fn is_empty(&self) -> bool {
1032 self.reshaped.is_empty() && self.moved.is_empty()
1033 }
1034}
1035
1036pub fn take_geometry_scene_nodes() -> GeometrySceneNodes {
1042 let mut nodes = GeometrySceneNodes::default();
1043 take_geometry_scene_nodes_into(&mut nodes);
1044 nodes
1045}
1046
1047pub fn take_geometry_scene_nodes_into(output: &mut GeometrySceneNodes) {
1058 with_render_state(|state| {
1059 state
1060 .geometry_scene_nodes
1061 .borrow_mut()
1062 .take_dirty_nodes_into(&mut output.reshaped);
1063 state
1064 .moved_scene_nodes
1065 .borrow_mut()
1066 .take_dirty_nodes_into(&mut output.moved);
1067 });
1068}
1069
1070pub fn current_density() -> f32 {
1072 with_render_state(|state| f32::from_bits(state.density_bits.get()))
1073}
1074
1075pub fn set_density(density: f32) {
1080 let normalized = normalize_density(density);
1081 let new_bits = normalized.to_bits();
1082 with_render_state(|state| {
1083 let old_bits = state.density_bits.replace(new_bits);
1084 if old_bits != new_bits {
1085 state.layout_invalidated.set(true);
1086 }
1087 });
1088}
1089
1090pub fn current_font_scale() -> f32 {
1101 current_font_scale_curve().scale()
1102}
1103
1104pub fn current_font_scale_curve() -> crate::font_scale::FontScaleCurve {
1110 with_render_state(|state| state.font_scale.get())
1111}
1112
1113pub(crate) fn current_scaling_font_scale_curve() -> Option<crate::font_scale::FontScaleCurve> {
1116 with_render_state(|state| (!state.font_scale_identity.get()).then(|| state.font_scale.get()))
1117}
1118
1119pub(crate) fn text_layout_stamp() -> (u64, u32) {
1122 let context = require_current_app_context("text layout stamp access");
1123 (
1124 context.text.generation(),
1125 context.state.font_scale_fingerprint.get(),
1126 )
1127}
1128
1129pub fn scale_sp(sp: f32) -> f32 {
1131 current_font_scale_curve().sp_to_dp(sp)
1132}
1133
1134pub fn set_font_scale(scale: f32) {
1144 set_font_scale_curve(crate::font_scale::FontScaleCurve::linear(scale));
1145}
1146
1147pub fn set_font_scale_curve(curve: crate::font_scale::FontScaleCurve) {
1155 let normalized = normalize_font_scale(curve.scale());
1156 let curve = if (normalized - curve.scale()).abs() <= f32::EPSILON {
1157 curve
1158 } else {
1159 crate::font_scale::FontScaleCurve::linear(normalized)
1160 };
1161 with_render_state(|state| {
1162 if state.font_scale.get() != curve {
1163 state.font_scale.set(curve);
1164 state.font_scale_identity.set(curve.is_identity());
1165 state.font_scale_fingerprint.set(curve.fingerprint());
1166 state.layout_invalidated.set(true);
1167 }
1168 });
1169}
1170
1171pub fn request_render_invalidation() {
1173 with_render_state(|state| state.render_invalidated.set(true));
1174}
1175
1176pub fn take_render_invalidation() -> bool {
1178 with_render_state(|state| state.render_invalidated.replace(false))
1179}
1180
1181pub fn peek_render_invalidation() -> bool {
1183 with_render_state(|state| state.render_invalidated.get())
1184}
1185
1186pub fn request_pointer_invalidation() {
1188 with_render_state(|state| state.pointer_invalidated.set(true));
1189}
1190
1191pub fn take_pointer_invalidation() -> bool {
1193 with_render_state(|state| state.pointer_invalidated.replace(false))
1194}
1195
1196pub fn peek_pointer_invalidation() -> bool {
1198 with_render_state(|state| state.pointer_invalidated.get())
1199}
1200
1201pub fn request_focus_invalidation() {
1203 with_render_state(|state| state.focus_invalidated.set(true));
1204}
1205
1206pub fn take_focus_invalidation() -> bool {
1208 with_render_state(|state| state.focus_invalidated.replace(false))
1209}
1210
1211pub fn peek_focus_invalidation() -> bool {
1213 with_render_state(|state| state.focus_invalidated.get())
1214}
1215
1216pub fn request_layout_invalidation() {
1243 with_render_state(|state| state.layout_invalidated.set(true));
1244}
1245
1246pub fn take_layout_invalidation() -> bool {
1248 with_render_state(|state| state.layout_invalidated.replace(false))
1249}
1250
1251pub fn peek_layout_invalidation() -> bool {
1253 with_render_state(|state| state.layout_invalidated.get())
1254}
1255
1256#[cfg(any(test, feature = "test-helpers"))]
1257#[doc(hidden)]
1258pub fn reset_render_state_for_tests() {
1259 let _ = take_draw_repass_nodes();
1260 let mut layer_nodes = Vec::new();
1261 take_layer_property_repass_nodes_into(&mut layer_nodes);
1262 let _ = take_layout_repass_nodes();
1263 let _ = take_modifier_slice_repass_nodes();
1264 let _ = take_render_invalidation();
1265 let _ = take_pointer_invalidation();
1266 let _ = take_focus_invalidation();
1267 let _ = take_layout_invalidation();
1268 debug_reset_last_fling_velocity();
1269 set_density(1.0);
1270 set_font_scale(1.0);
1271 let _ = take_layout_invalidation();
1272}
1273
1274#[cfg(test)]
1275pub(crate) struct TestAppContextScope {
1276 _scope: AppContextScope,
1277 _context: Rc<AppContext>,
1278}
1279
1280#[cfg(test)]
1281pub(crate) fn app_context_test_scope() -> TestAppContextScope {
1282 let context = AppContext::new();
1283 let scope = context.enter_scope();
1284 context.enter(reset_render_state_for_tests);
1285 TestAppContextScope {
1286 _scope: scope,
1287 _context: context,
1288 }
1289}
1290
1291#[cfg(test)]
1292pub(crate) struct RenderStateTestGuard {
1293 _app_scope: TestAppContextScope,
1294 _lock: std::sync::MutexGuard<'static, ()>,
1295}
1296
1297#[cfg(test)]
1298pub(crate) fn render_state_test_guard() -> RenderStateTestGuard {
1299 static TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
1300 let lock = match TEST_LOCK.get_or_init(|| Mutex::new(())).lock() {
1301 Ok(guard) => guard,
1302 Err(poisoned) => poisoned.into_inner(),
1303 };
1304 RenderStateTestGuard {
1305 _app_scope: app_context_test_scope(),
1306 _lock: lock,
1307 }
1308}
1309
1310#[cfg(test)]
1311#[path = "tests/render_state_tests.rs"]
1312mod tests;