From b57188f80f6a5098e2efbf5709fef9c05b25f4a8 Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Sat, 11 May 2024 21:25:54 -0700 Subject: [PATCH] Added optional tokio integration Closes #147 --- CHANGELOG.md | 8 ++ Cargo.lock | 69 ++++++++++++++++- Cargo.toml | 8 ++ examples/tokio.rs | 31 ++++++++ src/animation.rs | 26 ++++--- src/app.rs | 194 ++++++++++++++++++++++++++++++++++++++++++++-- src/debug.rs | 4 +- src/lib.rs | 4 +- src/window.rs | 22 +++++- 9 files changed, 346 insertions(+), 20 deletions(-) create mode 100644 examples/tokio.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a55b4080..e613288ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -251,6 +251,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `OverlayLayer::dismiss_all()` dismisses all overlays immediately. - `Menu` is a new widget type that can be shown in an `OverlayLayer` to create contextual menus or other popup menus. +- `PendingApp::new` is a new function that accepts an `AppRuntime` implementor. + This abstraction is how Cushy provides the optional integration for `tokio`. +- Features `tokio` and `tokio-multi-thread` enable the tokio integration for + this crate and expose a new type `TokioRuntime`. The `DefaultRuntime` + automatically will use the `TokioRuntime` if either feature is enabled. + + When the `tokio` integration is enabled, `tokio::spawn` is able to be invoked + from all Cushy code safely. [plotters]: https://github.com/plotters-rs/plotters diff --git a/Cargo.lock b/Cargo.lock index 4cd1d3e09..497d41b95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,6 +18,15 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + [[package]] name = "adler" version = "1.0.2" @@ -248,6 +257,21 @@ dependencies = [ "arrayvec", ] +[[package]] +name = "backtrace" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "bit-set" version = "0.5.3" @@ -652,6 +676,7 @@ dependencies = [ "png", "pollster", "rand", + "tokio", "tracing", "tracing-subscriber", "unicode-segmentation", @@ -941,6 +966,12 @@ dependencies = [ "weezl", ] +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + [[package]] name = "gl_generator" version = "0.14.0" @@ -1262,7 +1293,7 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "kludgine" version = "0.7.0" -source = "git+https://github.com/khonsulabs/kludgine#d98e0d6d2fa9e40e0c6a9d5b9b5c8079ba3a2e62" +source = "git+https://github.com/khonsulabs/kludgine#eed5d9e5c51ab9c29bb6014ca282852504d2ac87" dependencies = [ "ahash", "alot", @@ -1697,6 +1728,16 @@ dependencies = [ "libm", ] +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "num_enum" version = "0.7.2" @@ -1783,6 +1824,15 @@ dependencies = [ "objc2", ] +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.19.0" @@ -2358,6 +2408,12 @@ version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + [[package]] name = "rustc-hash" version = "1.1.0" @@ -2744,6 +2800,17 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tokio" +version = "1.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +dependencies = [ + "backtrace", + "num_cpus", + "pin-project-lite", +] + [[package]] name = "toml" version = "0.8.12" diff --git a/Cargo.toml b/Cargo.toml index cb95fd9ec..68558a8c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,8 @@ default = ["tracing-output", "roboto-flex"] tracing-output = ["dep:tracing-subscriber"] roboto-flex = [] plotters = ["dep:plotters", "kludgine/plotters"] +tokio = ["dep:tokio"] +tokio-multi-thread = ["tokio", "tokio/rt-multi-thread"] [dependencies] # kludgine = { version = "0.7.0", features = ["app"] } @@ -30,6 +32,7 @@ interner = "0.2.1" kempt = "0.2.1" intentional = "0.1.0" tracing = "0.1.40" +tokio = { version = "1.37.0", optional = true, features = ["rt"] } tracing-subscriber = { version = "0.3", optional = true, features = [ "env-filter", @@ -67,11 +70,16 @@ opt-level = 2 [dev-dependencies] rand = "0.8.5" +tokio = { version = "1.37.0", features = ["time"] } [[example]] name = "plotters" required-features = ["plotters"] +[[example]] +name = "tokio" +required-features = ["tokio"] + [profile.release] # debug = true # opt-level = "s" diff --git a/examples/tokio.rs b/examples/tokio.rs new file mode 100644 index 000000000..06554e381 --- /dev/null +++ b/examples/tokio.rs @@ -0,0 +1,31 @@ +use std::time::Duration; + +use cushy::value::{Destination, Dynamic}; +use cushy::widget::MakeWidget; +use cushy::widgets::progress::Progressable; +use cushy::{Open, PendingApp, TokioRuntime}; +use tokio::time::sleep; + +fn main() { + let app = PendingApp::new(TokioRuntime::default()); + let progress = Dynamic::new(0_u8); + let progress_bar = progress.clone().progress_bar(); + "Press Me" + .into_button() + .on_click(move |_| { + tokio::spawn(do_something(progress.clone())); + }) + .and(progress_bar) + .into_rows() + .centered() + .expand() + .run_in(app) + .expect("error starting Cushy"); +} + +async fn do_something(progress: Dynamic) { + for i in 0..u8::MAX { + progress.set(i); + sleep(Duration::from_millis(10)).await + } +} diff --git a/src/animation.rs b/src/animation.rs index a2d6491b9..c1667bb40 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -59,20 +59,26 @@ use crate::animation::easings::Linear; use crate::styles::{Component, RequireInvalidation}; use crate::utils::run_in_bg; use crate::value::{Destination, Dynamic, Source}; +use crate::Cushy; static ANIMATIONS: Mutex = Mutex::new(Animating::new()); static NEW_ANIMATIONS: Condvar = Condvar::new(); -fn thread_state() -> MutexGuard<'static, Animating> { +pub(crate) fn spawn(app: Cushy) { + let _ignored = thread_state(Some(app)); +} + +fn thread_state(app: Option) -> MutexGuard<'static, Animating> { static THREAD: OnceLock<()> = OnceLock::new(); - THREAD.get_or_init(|| { - thread::spawn(animation_thread); + THREAD.get_or_init(move || { + thread::spawn(move || animation_thread(app.as_ref())); }); ANIMATIONS.lock() } -fn animation_thread() { - let mut state = thread_state(); +fn animation_thread(app: Option<&Cushy>) { + let _guard = app.as_ref().map(|app| app.enter_runtime()); + let mut state = thread_state(None); loop { if state.running.is_empty() { state.last_updated = None; @@ -104,7 +110,7 @@ fn animation_thread() { .checked_duration_since(Instant::now()) .unwrap_or(Duration::from_millis(16)), ); - state = thread_state(); + state = thread_state(None); } } } @@ -456,7 +462,7 @@ where impl Spawn for Box { fn spawn(self) -> AnimationHandle { - thread_state().spawn(self) + thread_state(None).spawn(self) } } @@ -512,7 +518,7 @@ impl AnimationHandle { /// This has the same effect as dropping the handle. pub fn clear(&mut self) { if let Some(id) = self.0.take() { - thread_state().remove_animation(id); + thread_state(None).remove_animation(id); } } @@ -524,7 +530,7 @@ impl AnimationHandle { /// through completion without needing to hold onto the handle. pub fn detach(mut self) { if let Some(id) = self.0.take() { - thread_state().run_unattached(id); + thread_state(None).run_unattached(id); } } @@ -533,7 +539,7 @@ impl AnimationHandle { pub fn is_running(&self) -> bool { let Some(id) = self.0 else { return false }; - thread_state().running.contains(&id) + thread_state(None).running.contains(&id) } /// Returns true if this animation is complete. diff --git a/src/app.rs b/src/app.rs index b855e42bc..a6da131e2 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,9 +1,11 @@ +use std::marker::PhantomData; use std::sync::Arc; use arboard::Clipboard; use kludgine::app::{AppEvent, AsApplication}; use parking_lot::{Mutex, MutexGuard}; +use crate::animation; use crate::fonts::FontCollection; use crate::window::sealed::WindowCommand; use crate::window::WindowHandle; @@ -15,6 +17,14 @@ pub struct PendingApp { } impl PendingApp { + /// Returns a new app using the provided runtime. + pub fn new(runtime: Runtime) -> Self { + Self { + app: kludgine::app::PendingApp::default(), + cushy: Cushy::new(BoxedRuntime(Box::new(runtime))), + } + } + /// The shared resources this application utilizes. #[must_use] pub const fn cushy(&self) -> &Cushy { @@ -24,16 +34,15 @@ impl PendingApp { impl Run for PendingApp { fn run(self) -> crate::Result { + let _guard = self.cushy.enter_runtime(); + animation::spawn(self.cushy.clone()); self.app.run() } } impl Default for PendingApp { fn default() -> Self { - Self { - app: kludgine::app::PendingApp::default(), - cushy: Cushy::new(), - } + Self::new(DefaultRuntime::default()) } } @@ -50,20 +59,178 @@ impl AsApplication> for PendingApp { } } +/// A runtime associated with the Cushy application. +/// +/// This trait is how Cushy adds optional support for `tokio`. +pub trait AppRuntime: Send + Clone + 'static { + /// The guard type returned from entering the context of the app's runtime. + type Guard<'a>; + + /// Enter the application's rutime context. + fn enter(&self) -> Self::Guard<'_>; +} + +/// A default application runtime. +/// +/// When the `tokio` feature is enabled, a tokio runtime is spawned when this +/// runtime is used in Cushy. +#[derive(Debug, Clone, Default)] +pub struct DefaultRuntime { + #[cfg(feature = "tokio")] + tokio: TokioRuntime, + _private: (), +} + +impl AppRuntime for DefaultRuntime { + type Guard<'a> = DefaultRuntimeGuard<'a>; + + fn enter(&self) -> Self::Guard<'_> { + DefaultRuntimeGuard { + #[cfg(feature = "tokio")] + _tokio: self.tokio.enter(), + _phantom: PhantomData, + } + } +} + +pub struct DefaultRuntimeGuard<'a> { + #[cfg(feature = "tokio")] + _tokio: ::tokio::runtime::EnterGuard<'a>, + _phantom: PhantomData<&'a ()>, +} + +#[cfg(feature = "tokio")] +mod tokio { + use std::future::Future; + use std::ops::Deref; + use std::task::Poll; + use std::thread; + + use tokio::runtime::{self, Handle}; + + use super::AppRuntime; + use crate::Lazy; + + /// A spawned `tokio` runtime. + #[derive(Debug, Clone)] + pub struct TokioRuntime { + pub(crate) handle: Handle, + } + + impl From for TokioRuntime { + fn from(handle: Handle) -> Self { + Self { handle } + } + } + + static TOKIO: Lazy = Lazy::new(|| { + #[cfg(feature = "tokio-multi-thread")] + let mut rt = runtime::Builder::new_multi_thread(); + #[cfg(not(feature = "tokio-multi-thread"))] + let mut rt = runtime::Builder::new_current_thread(); + let runtime = rt + .enable_all() + .build() + .expect("failure to initialize tokio"); + let handle = runtime.handle().clone(); + thread::Builder::new() + .name(String::from("tokio")) + .spawn(move || { + runtime.block_on(BlockForever); + }) + .expect("error spawning tokio thread"); + handle + }); + + impl Default for TokioRuntime { + fn default() -> Self { + Self { + handle: TOKIO.clone(), + } + } + } + + impl Deref for TokioRuntime { + type Target = Handle; + + fn deref(&self) -> &Self::Target { + &self.handle + } + } + + struct BlockForever; + impl Future for BlockForever { + type Output = (); + + fn poll( + self: std::pin::Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + ) -> Poll { + Poll::<()>::Pending + } + } + + impl AppRuntime for TokioRuntime { + type Guard<'a> = tokio::runtime::EnterGuard<'a>; + + fn enter(&self) -> Self::Guard<'_> { + self.handle.enter() + } + } +} + +#[cfg(feature = "tokio")] +pub use tokio::TokioRuntime; + +struct BoxedRuntime(Box); + +impl Clone for BoxedRuntime { + fn clone(&self) -> Self { + self.0.cloned() + } +} + +trait BoxableRuntime: Send { + fn enter_runtime(&self) -> RuntimeGuard<'_>; + fn cloned(&self) -> BoxedRuntime; +} + +impl BoxableRuntime for T +where + T: AppRuntime, + for<'a> T::Guard<'a>: BoxableGuard<'a>, +{ + fn enter_runtime(&self) -> RuntimeGuard<'_> { + RuntimeGuard(Box::new(AppRuntime::enter(self))) + } + + fn cloned(&self) -> BoxedRuntime { + BoxedRuntime(Box::new(self.clone())) + } +} + +#[allow(dead_code)] +pub struct RuntimeGuard<'a>(Box + 'a>); + +trait BoxableGuard<'a> {} +impl<'a, T> BoxableGuard<'a> for T {} + /// Shared resources for a GUI application. #[derive(Clone)] pub struct Cushy { pub(crate) clipboard: Option>>, pub(crate) fonts: FontCollection, + runtime: BoxedRuntime, } impl Cushy { - pub(crate) fn new() -> Self { + fn new(runtime: BoxedRuntime) -> Self { Self { clipboard: Clipboard::new() .ok() .map(|clipboard| Arc::new(Mutex::new(clipboard))), fonts: FontCollection::default(), + runtime, } } @@ -79,6 +246,23 @@ impl Cushy { pub fn fonts(&self) -> &FontCollection { &self.fonts } + + /// Enters the application's runtime context. + /// + /// When the `tokio` feature is enabled, the guard returned by this function + /// allows for functions like `tokio::spawn` to work for the current thread. + /// Outside of application startup, this function shouldn't need to be + /// called unless you are manually spawning threads. + #[must_use] + pub fn enter_runtime(&self) -> RuntimeGuard<'_> { + self.runtime.0.enter_runtime() + } +} + +impl Default for Cushy { + fn default() -> Self { + Self::new(BoxedRuntime(Box::::default())) + } } /// A type that is a Cushy application. diff --git a/src/debug.rs b/src/debug.rs index db18ecc69..d7083e178 100644 --- a/src/debug.rs +++ b/src/debug.rs @@ -8,7 +8,7 @@ use crate::value::{Dynamic, DynamicReader, ForEach, Source, WeakDynamic}; use crate::widget::{MakeWidget, WidgetInstance, WidgetList}; use crate::widgets::grid::{Grid, GridWidgets}; use crate::window::Window; -use crate::{Open, PendingApp}; +use crate::{Application, Open, PendingApp}; /// A widget that can provide extra information when debugging. #[derive(Clone, Default)] @@ -117,7 +117,7 @@ impl DebugContext { impl Open for DebugContext { fn open(self, app: &mut App) -> crate::Result> where - App: crate::Application + ?Sized, + App: Application + ?Sized, { self.into_window().open(app) } diff --git a/src/lib.rs b/src/lib.rs index 791463180..ebcea991c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,7 +25,9 @@ pub mod widgets; pub mod window; use std::ops::{Add, AddAssign, Sub, SubAssign}; -pub use app::{App, Application, Cushy, Open, PendingApp, Run}; +#[cfg(feature = "tokio")] +pub use app::TokioRuntime; +pub use app::{App, AppRuntime, Application, Cushy, DefaultRuntime, Open, PendingApp, Run}; use figures::units::UPx; use figures::{Fraction, ScreenUnit, Size, Zero}; use kludgine::app::winit::error::EventLoopError; diff --git a/src/window.rs b/src/window.rs index aa6a8d828..d289478a5 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1211,6 +1211,8 @@ where where W: PlatformWindowImplementation, { + let cushy = self.cushy.clone(); + let _guard = cushy.enter_runtime(); if let Some(theme) = &mut self.theme { if theme.has_updated() { self.current_theme = theme.get(); @@ -1320,6 +1322,8 @@ where where W: PlatformWindowImplementation, { + let cushy = self.cushy.clone(); + let _guard = cushy.enter_runtime(); if self.behavior.close_requested(&mut RunningWindow::new( window, kludgine.id(), @@ -1362,6 +1366,8 @@ where where W: PlatformWindowImplementation, { + let cushy = self.cushy.clone(); + let _guard = cushy.enter_runtime(); let mut window = RunningWindow::new( window, kludgine.id(), @@ -1410,6 +1416,8 @@ where where W: PlatformWindowImplementation, { + let cushy = self.cushy.clone(); + let _guard = cushy.enter_runtime(); let mut window = RunningWindow::new( window, kludgine.id(), @@ -1451,6 +1459,8 @@ where where W: PlatformWindowImplementation, { + let cushy = self.cushy.clone(); + let _guard = cushy.enter_runtime(); let mut window = RunningWindow::new( window, kludgine.id(), @@ -1493,6 +1503,8 @@ where ) where W: PlatformWindowImplementation, { + let cushy = self.cushy.clone(); + let _guard = cushy.enter_runtime(); let mut window = RunningWindow::new( window, kludgine.id(), @@ -1548,6 +1560,8 @@ where where W: PlatformWindowImplementation, { + let cushy = self.cushy.clone(); + let _guard = cushy.enter_runtime(); if self.cursor.widget.take().is_some() { let mut window = RunningWindow::new( window, @@ -1585,6 +1599,8 @@ where where W: PlatformWindowImplementation, { + let cushy = self.cushy.clone(); + let _guard = cushy.enter_runtime(); let mut window = RunningWindow::new( window, kludgine.id(), @@ -1703,6 +1719,8 @@ where context: Self::Context, ) -> Self { let settings = context.settings.borrow_mut(); + let cushy = settings.cushy.clone(); + let _guard = cushy.enter_runtime(); let mut window = RunningWindow::new( window, graphics.id(), @@ -1784,6 +1802,8 @@ where window: kludgine::app::Window<'_, WindowCommand>, kludgine: &mut Kludgine, ) -> bool { + let cushy = self.cushy.clone(); + let _guard = cushy.enter_runtime(); Self::request_close( &mut self.should_close, &mut self.behavior, @@ -2532,7 +2552,7 @@ impl CushyWindowBuilder { window, &mut kludgine::Graphics::new(&mut kludgine, device, queue), sealed::WindowSettings { - cushy: Cushy::new(), + cushy: Cushy::default(), redraw_status: InvalidationStatus::default(), title: Value::default(), attributes: None,