Skip to main content

fui/
frame_scheduler.rs

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}