Added optional tokio integration

Closes #147
This commit is contained in:
Jonathan Johnson 2024-05-11 21:25:54 -07:00
parent 8f5ce94a87
commit b57188f80f
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
9 changed files with 346 additions and 20 deletions

View file

@ -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

69
Cargo.lock generated
View file

@ -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"

View file

@ -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"

31
examples/tokio.rs Normal file
View file

@ -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<u8>) {
for i in 0..u8::MAX {
progress.set(i);
sleep(Duration::from_millis(10)).await
}
}

View file

@ -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<Animating> = 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<Cushy>) -> 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<dyn Animate> {
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.

View file

@ -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: AppRuntime>(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<AppEvent<WindowCommand>> 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<Handle> for TokioRuntime {
fn from(handle: Handle) -> Self {
Self { handle }
}
}
static TOKIO: Lazy<Handle> = 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<Self::Output> {
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<dyn BoxableRuntime>);
impl Clone for BoxedRuntime {
fn clone(&self) -> Self {
self.0.cloned()
}
}
trait BoxableRuntime: Send {
fn enter_runtime(&self) -> RuntimeGuard<'_>;
fn cloned(&self) -> BoxedRuntime;
}
impl<T> 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<dyn BoxableGuard<'a> + '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<Arc<Mutex<Clipboard>>>,
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::<DefaultRuntime>::default()))
}
}
/// A type that is a Cushy application.

View file

@ -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<App>(self, app: &mut App) -> crate::Result<Option<crate::window::WindowHandle>>
where
App: crate::Application + ?Sized,
App: Application + ?Sized,
{
self.into_window().open(app)
}

View file

@ -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;

View file

@ -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,