1use crate::bindings::ui;
2use std::cell::{Cell, RefCell};
3
4type LoadedCallback = Box<dyn FnOnce(LoadedEventArgs)>;
5
6#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
7pub struct LoadedEventArgs;
8
9impl LoadedEventArgs {
10 pub const EMPTY: Self = Self;
11}
12
13thread_local! {
14 static NEEDS_COMMIT: Cell<bool> = const { Cell::new(false) };
15 static FLUSH_SCHEDULED: Cell<bool> = const { Cell::new(false) };
16 static DID_FIRST_COMMIT: Cell<bool> = const { Cell::new(false) };
17 static LOADED_CALLBACKS: RefCell<Vec<LoadedCallback>> = const { RefCell::new(Vec::new()) };
18}
19
20pub fn mark_needs_commit() {
21 NEEDS_COMMIT.with(|needs_commit| needs_commit.set(true));
22 let already_scheduled = FLUSH_SCHEDULED.with(|scheduled| {
23 let already_scheduled = scheduled.get();
24 if !already_scheduled {
25 scheduled.set(true);
26 }
27 already_scheduled
28 });
29 if !already_scheduled {
30 ui::request_render();
31 }
32}
33
34pub fn on_loaded(callback: impl FnOnce(LoadedEventArgs) + 'static) {
35 if DID_FIRST_COMMIT.with(Cell::get) {
36 callback(LoadedEventArgs::EMPTY);
37 return;
38 }
39 LOADED_CALLBACKS.with(|callbacks| callbacks.borrow_mut().push(Box::new(callback)));
40}
41
42pub(crate) fn fire_loaded_callbacks() {
43 if DID_FIRST_COMMIT.with(Cell::get) {
44 return;
45 }
46 DID_FIRST_COMMIT.with(|did_first_commit| did_first_commit.set(true));
47 let callbacks = LOADED_CALLBACKS.with(|callbacks| std::mem::take(&mut *callbacks.borrow_mut()));
48 for callback in callbacks {
49 callback(LoadedEventArgs::EMPTY);
50 }
51}
52
53pub(crate) fn flush_commit() -> bool {
54 FLUSH_SCHEDULED.with(|scheduled| scheduled.set(false));
55 let needs_commit = NEEDS_COMMIT.with(|slot| {
56 let needs_commit = slot.get();
57 if needs_commit {
58 slot.set(false);
59 }
60 needs_commit
61 });
62 if !needs_commit {
63 return false;
64 }
65 ui::commit_frame();
66 true
67}
68
69pub(crate) fn reset_commit_state() {
70 NEEDS_COMMIT.with(|needs_commit| needs_commit.set(false));
71 FLUSH_SCHEDULED.with(|scheduled| scheduled.set(false));
72 DID_FIRST_COMMIT.with(|did_first_commit| did_first_commit.set(false));
73 LOADED_CALLBACKS.with(|callbacks| callbacks.borrow_mut().clear());
74}
75
76pub(crate) fn reset_commit_state_preserving_loaded_callbacks() {
77 NEEDS_COMMIT.with(|needs_commit| needs_commit.set(false));
78 FLUSH_SCHEDULED.with(|scheduled| scheduled.set(false));
79 DID_FIRST_COMMIT.with(|did_first_commit| did_first_commit.set(false));
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85 use crate::ffi::{self, Call};
86 use std::cell::Cell;
87 use std::rc::Rc;
88
89 #[test]
90 fn on_loaded_fires_once_and_late_registration_fires_immediately() {
91 reset_commit_state();
92 let count = Rc::new(Cell::new(0));
93 on_loaded({
94 let count = count.clone();
95 move |_| count.set(count.get() + 1)
96 });
97
98 fire_loaded_callbacks();
99 fire_loaded_callbacks();
100 assert_eq!(count.get(), 1);
101
102 on_loaded({
103 let count = count.clone();
104 move |_| count.set(count.get() + 1)
105 });
106 assert_eq!(count.get(), 2);
107 }
108
109 #[test]
110 fn mark_needs_commit_coalesces_render_requests_until_flush() {
111 reset_commit_state();
112 ffi::test::reset();
113
114 mark_needs_commit();
115 mark_needs_commit();
116 let calls = ffi::test::take_calls();
117 assert_eq!(
118 calls
119 .iter()
120 .filter(|call| matches!(call, Call::RequestRender))
121 .count(),
122 1
123 );
124
125 assert!(flush_commit());
126 let _ = ffi::test::take_calls();
127 mark_needs_commit();
128 let calls = ffi::test::take_calls();
129 assert_eq!(
130 calls
131 .iter()
132 .filter(|call| matches!(call, Call::RequestRender))
133 .count(),
134 1
135 );
136 }
137
138 #[test]
139 fn flush_commit_noops_without_pending_work() {
140 reset_commit_state();
141 ffi::test::reset();
142
143 assert!(!flush_commit());
144 assert!(ffi::test::take_calls()
145 .iter()
146 .all(|call| !matches!(call, Call::CommitFrame)));
147
148 mark_needs_commit();
149 assert!(flush_commit());
150 assert!(ffi::test::take_calls()
151 .iter()
152 .any(|call| matches!(call, Call::CommitFrame)));
153
154 assert!(!flush_commit());
155 assert!(ffi::test::take_calls()
156 .iter()
157 .all(|call| !matches!(call, Call::CommitFrame)));
158 }
159}