diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 87ab448..01770cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,5 +10,7 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - - name: Build + - name: test base run: cargo test + - name: test sync + run: cargo test --features="sync" diff --git a/Cargo.toml b/Cargo.toml index 2ee7ccc..2225924 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,7 +42,17 @@ path = "examples/navigation.rs" name = "stack" path = "examples/stack.rs" +[[example]] +name = "external" +path = "examples/threading.rs" +required-features = ["sync"] + + [dependencies] anyhow = "1.0.71" crossterm = "0.27" ctrlc = "3.3.1" + +[features] +sync = [] +default = [] diff --git a/docs/codex.yml b/docs/codex.yml index ff45530..be369f5 100644 --- a/docs/codex.yml +++ b/docs/codex.yml @@ -1,5 +1,5 @@ # Project name -name: Arkham CLI Library +name: Arkham # The author displayed on the title page during PDF generation author: ~ # The path relative to the project root where the compiled static site files diff --git a/docs/guides/group.yml b/docs/guides/group.yml new file mode 100644 index 0000000..1a2e3f7 --- /dev/null +++ b/docs/guides/group.yml @@ -0,0 +1 @@ +menu_position: 1 diff --git a/docs/guides/threading.md b/docs/guides/threading.md new file mode 100644 index 0000000..2f0fab7 --- /dev/null +++ b/docs/guides/threading.md @@ -0,0 +1,75 @@ +--- +title: Threading +subtitle: Guides +--- + + +# The _sync_ flag + +You can enable the sync flag in your _cargo.toml_ file by changing the Arkham deceleration to: + +```Toml +arkham = { version = "*", features=["sync"] } +``` + +With the _sync_ flag enabled `Res` and `State` will be thread safe. This makes it easy to pass the application state or resources to other threads for processing. + +# Render signals + +When manipulating data from outside of components, especially in other threads, it is useful to be able to notify the app instance that it needs to render changes to the screen. a `Renderer` provides the ability to signal the app instance that it needs to render. + + +```Rust +let mut app = App::new(root_view); +let renderer = app.get_renderer(); +std::thread::spawn(move || loop { + renderer.render() + std::thread::sleep( + std::time::Duration::from_secs(10) + ); +}); +app.run(); + +``` + +# Full threading example + +```Rust +use arkham::prelude::*; + +#[derive(Default)] +pub struct AppState { + pub counter: i32, +} + +fn main() { + let app_state = State::new(AppState::default()); + let mut app = App::new(root_view) + .bind_state(app_state.clone()); + let renderer = app.get_renderer(); + + std::thread::spawn(move || loop { + app_state.get_mut().counter += 1; + renderer.render(); + std::thread::sleep( + std::time::Duration::from_secs(1) + ); + }); + + app.run().unwrap(); +} + +fn root_view( + ctx: &mut ViewContext, + state: State +) { + ctx.insert( + 0, + format!("Count is {}", state.get().counter) + ); +} +``` + + + + diff --git a/examples/threading.rs b/examples/threading.rs new file mode 100644 index 0000000..7302433 --- /dev/null +++ b/examples/threading.rs @@ -0,0 +1,24 @@ +use arkham::prelude::*; + +#[derive(Default)] +pub struct AppState { + pub counter: i32, +} + +fn main() { + let app_state = State::new(AppState::default()); + let mut app = App::new(root_view).bind_state(app_state.clone()); + let renderer = app.get_renderer(); + + std::thread::spawn(move || loop { + app_state.get_mut().counter += 1; + renderer.render(); + std::thread::sleep(std::time::Duration::from_secs(1)); + }); + + app.run().unwrap(); +} + +fn root_view(ctx: &mut ViewContext, state: State) { + ctx.insert(0, format!("Count is {}", state.get().counter)); +} diff --git a/src/app.rs b/src/app.rs index 712f1e7..d6c0863 100644 --- a/src/app.rs +++ b/src/app.rs @@ -117,7 +117,14 @@ where /// Alternatively, App::insert_state can be used to insert a state object, /// that can be borrowed mutable. pub fn insert_resource(self, v: T) -> Self { - self.container.borrow_mut().bind(Res::new(v)); + self.bind_resource(Res::new(v)) + } + + /// Bind an existing resource to the application + /// + /// Similar to `App::insert_resource` except it accepts an existing resource. + pub fn bind_resource(self, v: Res) -> Self { + self.container.borrow_mut().bind(v); self } @@ -142,7 +149,14 @@ where /// } /// ```` pub fn insert_state(self, v: T) -> Self { - self.container.borrow_mut().bind(State::new(v)); + self.bind_state(State::new(v)) + } + + /// Binds an existing state to the application. + /// + /// Similar to `App::insert_state` but will accept an existing state + pub fn bind_state(self, v: State) -> Self { + self.container.borrow_mut().bind(v); self } @@ -247,3 +261,25 @@ impl Terminal { crossterm::terminal::size().unwrap_or_default() } } + +#[cfg(test)] +mod tests { + #[cfg(feature = "sync")] + #[test] + fn test_threaded_state() { + use crate::prelude::{App, State, ViewContext}; + + #[derive(Default)] + struct S { + i: i32, + } + + let root_view = |_: &mut ViewContext| {}; + + let state = State::new(S::default()); + App::new(root_view).bind_state(state.clone()); + std::thread::spawn(move || { + state.get_mut().i = 10; + }); + } +} diff --git a/src/container.rs b/src/container.rs index 5c3dce0..5bedd22 100644 --- a/src/container.rs +++ b/src/container.rs @@ -1,9 +1,13 @@ +#[cfg(not(feature = "sync"))] +use std::{cell::RefCell, rc::Rc}; + +#[cfg(feature = "sync")] +use std::sync::{Arc, RwLock}; + use std::{ any::{Any, TypeId}, - cell::RefCell, collections::HashMap, ops::Deref, - rc::Rc, }; use crate::context::ViewContext; @@ -36,10 +40,19 @@ impl Container { /// A wrapper for state objcets. This internally holds a reference counted /// poitner to the object and is used when injecting itno functions. +#[cfg(not(feature = "sync"))] pub struct State(Rc>); +#[cfg(feature = "sync")] +pub struct State(Arc>); + impl State { /// Create a new state wrapper. + #[cfg(feature = "sync")] + pub fn new(val: T) -> Self { + State(Arc::new(RwLock::new(val))) + } + #[cfg(not(feature = "sync"))] pub fn new(val: T) -> Self { State(Rc::new(RefCell::new(val))) } @@ -55,6 +68,11 @@ impl State { /// state.get_mut().0 = 6; /// assert_eq!(state.get().0, 6); /// ``` + #[cfg(feature = "sync")] + pub fn get_mut(&self) -> std::sync::RwLockWriteGuard { + self.0.write().unwrap() + } + #[cfg(not(feature = "sync"))] pub fn get_mut(&self) -> std::cell::RefMut { RefCell::borrow_mut(&self.0) } @@ -68,6 +86,11 @@ impl State { /// let state = State::new(MyState(4)); /// assert_eq!(state.get().0, 4); /// ``` + #[cfg(feature = "sync")] + pub fn get(&self) -> std::sync::RwLockReadGuard { + self.0.read().unwrap() + } + #[cfg(not(feature = "sync"))] pub fn get(&self) -> std::cell::Ref { RefCell::borrow(&self.0) } @@ -88,10 +111,20 @@ impl FromContainer for State { /// A wrapper for resources stored within the app. This wrapper is returned /// when objects are injected into component functions and provide immutable /// access +#[cfg(feature = "sync")] +#[derive(Debug)] +pub struct Res(Arc); + +#[cfg(not(feature = "sync"))] #[derive(Debug)] pub struct Res(Rc); impl Res { + #[cfg(feature = "sync")] + pub fn new(val: T) -> Self { + Res(Arc::new(val)) + } + #[cfg(not(feature = "sync"))] pub fn new(val: T) -> Self { Res(Rc::new(val)) } @@ -109,6 +142,16 @@ impl Clone for Res { } } +#[cfg(feature = "sync")] +impl Deref for Res { + type Target = Arc; + + fn deref(&self) -> &Arc { + &self.0 + } +} + +#[cfg(not(feature = "sync"))] impl Deref for Res { type Target = Rc;