diff --git a/api/sixtyfps-cpp/include/sixtyfps.h b/api/sixtyfps-cpp/include/sixtyfps.h index 169ce7509..17671b9a2 100644 --- a/api/sixtyfps-cpp/include/sixtyfps.h +++ b/api/sixtyfps-cpp/include/sixtyfps.h @@ -337,24 +337,67 @@ private: private_api::WindowRc inner; }; +#if !defined(DOXYGEN) +using cbindgen_private::TimerMode; +#else +/// The TimerMode specifies what should happen after the timer fired. +/// +/// Used by the sixtyfps::Timer::start() function. +enum class TimerMode { + /// A SingleShot timer is fired only once. + SingleShot, + /// A Repeated timer is fired repeatedly until it is stopped or dropped. + Repeated, +}; +#endif + /// A Timer that can call a callback at repeated interval /// /// Use the static single_shot function to make a single shot timer struct Timer { + /// Construct a null timer. Use the start() method to activate the timer with a mode, interval + /// and callback. + Timer() : id(-1) { } /// Construct a timer which will repeat the callback every `interval` milliseconds until /// the destructor of the timer is called. + /// + /// This is a convenience function and equivalent to calling + /// `start(sixtyfps::TimerMode::Repeated, interval, callback);` on a default constructed Timer. template Timer(std::chrono::milliseconds interval, F callback) : id(cbindgen_private::sixtyfps_timer_start( - interval.count(), [](void *data) { (*reinterpret_cast(data))(); }, - new F(std::move(callback)), [](void *data) { delete reinterpret_cast(data); })) + -1, cbindgen_private::TimerMode::Repeated, interval.count(), + [](void *data) { (*reinterpret_cast(data))(); }, new F(std::move(callback)), + [](void *data) { delete reinterpret_cast(data); })) { } Timer(const Timer &) = delete; Timer &operator=(const Timer &) = delete; ~Timer() { cbindgen_private::sixtyfps_timer_stop(id); } + /// Starts the timer with the given \a mode and \a interval, in order for the \a callback to + /// called when the timer fires. If the timer has been started previously and not fired yet, + /// then it will be restarted. + template + void start(TimerMode mode, std::chrono::milliseconds interval, F callback) + { + id = cbindgen_private::sixtyfps_timer_start( + id, mode, interval.count(), [](void *data) { (*reinterpret_cast(data))(); }, + new F(std::move(callback)), [](void *data) { delete reinterpret_cast(data); }); + } + /// Stops the previously started timer. Does nothing if the timer has never been started. A + /// stopped timer cannot be restarted with restart() -- instead you need to call start(). + void stop() + { + cbindgen_private::sixtyfps_timer_stop(id); + id = -1; + } + /// Restarts the timer, if it was previously started. + void restart() { cbindgen_private::sixtyfps_timer_restart(id); } + /// Returns true if the timer is running; false otherwise. + bool running() const { return cbindgen_private::sixtyfps_timer_running(id); } + /// Call the callback after the given duration. template static void single_shot(std::chrono::milliseconds duration, F callback) diff --git a/api/sixtyfps-cpp/tests/eventloop.cpp b/api/sixtyfps-cpp/tests/eventloop.cpp index 9c51c025c..fda61b8a7 100644 --- a/api/sixtyfps-cpp/tests/eventloop.cpp +++ b/api/sixtyfps-cpp/tests/eventloop.cpp @@ -7,7 +7,7 @@ #include #include -TEST_CASE("C++ Timers") +TEST_CASE("C++ Singleshot Timers") { using namespace sixtyfps; int called = 0; @@ -20,6 +20,62 @@ TEST_CASE("C++ Timers") REQUIRE(called == 10); } +TEST_CASE("C++ Repeated Timer") +{ + int timer_triggered = 0; + sixtyfps::Timer timer; + + timer.start(sixtyfps::TimerMode::Repeated, std::chrono::milliseconds(30), + [&]() { timer_triggered++; }); + + REQUIRE(timer_triggered == 0); + + bool timer_was_running = false; + + sixtyfps::Timer::single_shot(std::chrono::milliseconds(500), [&]() { + timer_was_running = timer.running(); + sixtyfps::quit_event_loop(); + }); + + sixtyfps::run_event_loop(); + + REQUIRE(timer_triggered > 1); + REQUIRE(timer_was_running); +} + +TEST_CASE("C++ Restart Singleshot Timer") +{ + int timer_triggered = 0; + sixtyfps::Timer timer; + + timer.start(sixtyfps::TimerMode::SingleShot, std::chrono::milliseconds(30), + [&]() { timer_triggered++; }); + + REQUIRE(timer_triggered == 0); + + bool timer_was_running = false; + + sixtyfps::Timer::single_shot(std::chrono::milliseconds(500), [&]() { + timer_was_running = timer.running(); + sixtyfps::quit_event_loop(); + }); + + sixtyfps::run_event_loop(); + + REQUIRE(timer_triggered == 1); + REQUIRE(timer_was_running); + timer_triggered = 0; + timer.restart(); + sixtyfps::Timer::single_shot(std::chrono::milliseconds(500), [&]() { + timer_was_running = timer.running(); + sixtyfps::quit_event_loop(); + }); + + sixtyfps::run_event_loop(); + + REQUIRE(timer_triggered == 1); + REQUIRE(timer_was_running); +} TEST_CASE("Quit from event") { @@ -33,7 +89,6 @@ TEST_CASE("Quit from event") REQUIRE(called == 10); } - TEST_CASE("Event from thread") { std::atomic called = 0; @@ -50,15 +105,13 @@ TEST_CASE("Event from thread") t.join(); } - TEST_CASE("Blocking Event from thread") { std::atomic called = 0; auto t = std::thread([&] { // test returning a, unique_ptr because it is movable-only - std::unique_ptr foo = sixtyfps::blocking_invoke_from_event_loop([&] { - return std::make_unique(42); - }); + std::unique_ptr foo = sixtyfps::blocking_invoke_from_event_loop( + [&] { return std::make_unique(42); }); called = *foo; int xxx = 123; sixtyfps::blocking_invoke_from_event_loop([&] { diff --git a/sixtyfps_runtime/corelib/timers.rs b/sixtyfps_runtime/corelib/timers.rs index 52733e883..418f23d5b 100644 --- a/sixtyfps_runtime/corelib/timers.rs +++ b/sixtyfps_runtime/corelib/timers.rs @@ -21,6 +21,7 @@ type SingleShotTimerCallback = Box; /// /// Used by the [`Timer::start`] function. #[derive(Copy, Clone)] +#[repr(C)] pub enum TimerMode { /// A SingleShot timer is fired only once. SingleShot, @@ -360,6 +361,8 @@ pub(crate) mod ffi { /// The timer MUST be stopped with sixtyfps_timer_stop #[no_mangle] pub extern "C" fn sixtyfps_timer_start( + id: i64, + mode: TimerMode, duration: u64, callback: extern "C" fn(*mut c_void), user_data: *mut c_void, @@ -367,7 +370,10 @@ pub(crate) mod ffi { ) -> i64 { let wrap = WrapFn { callback, user_data, drop_user_data }; let timer = Timer::default(); - timer.start(TimerMode::Repeated, core::time::Duration::from_millis(duration), move || { + if id != -1 { + timer.id.set(Some(id as _)); + } + timer.start(mode, core::time::Duration::from_millis(duration), move || { (wrap.callback)(wrap.user_data) }); timer.id.take().map(|x| x as i64).unwrap_or(-1) @@ -396,4 +402,27 @@ pub(crate) mod ffi { let timer = Timer { id: Cell::new(Some(id as _)) }; timer.stop() } + + /// Restart a repeated timer + #[no_mangle] + pub extern "C" fn sixtyfps_timer_restart(id: i64) { + if id == -1 { + return; + } + let timer = Timer { id: Cell::new(Some(id as _)) }; + timer.restart(); + timer.id.take(); // Make sure that dropping the Timer doesn't unregister it. C++ will call stop() in the destructor. + } + + /// Returns true if the timer is running; false otherwise. + #[no_mangle] + pub extern "C" fn sixtyfps_timer_running(id: i64) -> bool { + if id == -1 { + return false; + } + let timer = Timer { id: Cell::new(Some(id as _)) }; + let running = timer.running(); + timer.id.take(); // Make sure that dropping the Timer doesn't unregister it. C++ will call stop() in the destructor. + running + } }