diff --git a/api/python/Cargo.toml b/api/python/Cargo.toml index c490fc652..5001b7792 100644 --- a/api/python/Cargo.toml +++ b/api/python/Cargo.toml @@ -24,8 +24,9 @@ i-slint-backend-testing = { version = "=1.4.0", path="../../internal/backends/te i-slint-renderer-skia = { version = "=1.4.0", path="../../internal/renderers/skia", optional = true, features = ["x11", "wayland"] } i-slint-core = { version = "=1.4.0", path="../../internal/core", features = ["ffi"] } slint-interpreter = { workspace = true, features = ["default", "display-diagnostics", "internal"] } -pyo3 = { version = "0.20.0", features = ["extension-module", "indexmap"] } +pyo3 = { version = "0.20.0", features = ["extension-module", "indexmap", "chrono"] } indexmap = { version = "2.1.0" } +chrono = "0.4" spin_on = "0.1" [build-dependencies] diff --git a/api/python/errors.rs b/api/python/errors.rs index 643e50245..f6d020fe4 100644 --- a/api/python/errors.rs +++ b/api/python/errors.rs @@ -45,6 +45,20 @@ impl From for PyPlatformError { } } +pub struct PyEventLoopError(pub slint_interpreter::EventLoopError); + +impl From for PyErr { + fn from(err: PyEventLoopError) -> Self { + pyo3::exceptions::PyRuntimeError::new_err(err.0.to_string()) + } +} + +impl From for PyEventLoopError { + fn from(err: slint_interpreter::EventLoopError) -> Self { + Self(err) + } +} + pub struct PyInvokeError(pub slint_interpreter::InvokeError); impl From for PyErr { diff --git a/api/python/lib.rs b/api/python/lib.rs index 23b06415a..1e3369f60 100644 --- a/api/python/lib.rs +++ b/api/python/lib.rs @@ -4,16 +4,37 @@ mod interpreter; use interpreter::{ComponentCompiler, PyDiagnostic, PyDiagnosticLevel, PyValueType}; mod errors; +mod timer; mod value; +#[pyfunction] +fn run_event_loop() -> Result<(), errors::PyPlatformError> { + slint_interpreter::run_event_loop().map_err(|e| e.into()) +} + +#[pyfunction] +fn quit_event_loop() -> Result<(), errors::PyEventLoopError> { + slint_interpreter::quit_event_loop().map_err(|e| e.into()) +} + use pyo3::prelude::*; #[pymodule] fn slint(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + i_slint_backend_selector::with_platform(|_b| { + // Nothing to do, just make sure a backend was created + Ok(()) + }) + .map_err(|e| errors::PyPlatformError(e))?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(run_event_loop, m)?)?; + m.add_function(wrap_pyfunction!(quit_event_loop, m)?)?; Ok(()) } diff --git a/api/python/tests/test_timers.py b/api/python/tests/test_timers.py new file mode 100644 index 000000000..1d43c83fa --- /dev/null +++ b/api/python/tests/test_timers.py @@ -0,0 +1,26 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial + +import pytest +import slint +from slint import ValueType +from datetime import timedelta + +def test_timer(): + global counter + counter = 0 + def quit_after_two_invocations(): + global counter + counter = counter + 1 + if counter >= 2: + slint.quit_event_loop() + + test_timer = slint.Timer() + test_timer.start(slint.TimerMode.Repeated, timedelta(milliseconds=100), quit_after_two_invocations) + slint.run_event_loop() + test_timer.stop() + assert(counter == 2) + +def test_single_shot(): + slint.Timer.single_shot(timedelta(milliseconds=100), slint.quit_event_loop) + slint.run_event_loop() diff --git a/api/python/timer.rs b/api/python/timer.rs new file mode 100644 index 000000000..e8dd52d71 --- /dev/null +++ b/api/python/timer.rs @@ -0,0 +1,85 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial + +use pyo3::prelude::*; + +#[derive(Copy, Clone)] +#[pyclass(name = "TimerMode")] +pub enum PyTimerMode { + /// A SingleShot timer is fired only once. + SingleShot, + /// A Repeated timer is fired repeatedly until it is stopped or dropped. + Repeated, +} + +impl From for i_slint_core::timers::TimerMode { + fn from(value: PyTimerMode) -> Self { + match value { + PyTimerMode::SingleShot => i_slint_core::timers::TimerMode::SingleShot, + PyTimerMode::Repeated => i_slint_core::timers::TimerMode::Repeated, + } + } +} + +#[pyclass(name = "Timer")] +pub struct PyTimer { + timer: i_slint_core::timers::Timer, +} + +#[pymethods] +impl PyTimer { + #[new] + fn py_new() -> Self { + PyTimer { timer: Default::default() } + } + + fn start( + &self, + mode: PyTimerMode, + interval: chrono::Duration, + callback: PyObject, + ) -> PyResult<()> { + let interval = interval + .to_std() + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + self.timer.start(mode.into(), interval, move || { + Python::with_gil(|py| { + callback.call0(py).expect("unexpected failure running python timer callback"); + }); + }); + Ok(()) + } + + #[staticmethod] + fn single_shot(duration: chrono::Duration, callback: PyObject) -> PyResult<()> { + let duration = duration + .to_std() + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + i_slint_core::timers::Timer::single_shot(duration, move || { + Python::with_gil(|py| { + callback.call0(py).expect("unexpected failure running python timer callback"); + }); + }); + Ok(()) + } + + fn stop(&self) { + self.timer.stop(); + } + + fn restart(&self) { + self.timer.restart(); + } + + fn running(&self) -> bool { + self.timer.running() + } + + fn set_interval(&self, interval: chrono::Duration) -> PyResult<()> { + let interval = interval + .to_std() + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + self.timer.set_interval(interval); + Ok(()) + } +}