diff --git a/Cargo.toml b/Cargo.toml index b4a85e96..f8838b88 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,10 +6,37 @@ license = "MIT OR Apache-2.0" description = "Cross-platform UI framework" repository = "https://github.com/concoct-rs/concoct" +[workspace] +members = [ + ".", + "web_examples" +] + [features] -full = [] +html = ["dep:js-sys", "dep:wasm-bindgen", "dep:web-sys"] +full = ["html"] [dependencies] futures = "0.3.29" slotmap = "1.0.6" -tokio = { version = "1.34.0", features = ["full"] } + +js-sys = { version = "0.3.65", optional = true } +wasm-bindgen = { version = "0.2.88", optional = true } + +[dependencies.web-sys] +version = "0.3.65" +optional = true +features = [ + "Event", + "EventTarget", + "KeyboardEvent", + "InputEvent", + "MouseEvent", + "Node", + "Document", + "HtmlCollection", + "HtmlElement", + "HtmlInputElement", + "Window", + "Text", +] diff --git a/examples/counter.rs b/examples/counter.rs index 3e6d0fbb..0aca8d83 100644 --- a/examples/counter.rs +++ b/examples/counter.rs @@ -1,6 +1,5 @@ use concoct::{use_future, use_state, Composable, Composition, IntoComposable}; -use std::time::Duration; -use tokio::time; +use futures::executor::block_on; #[derive(PartialEq)] struct Counter { @@ -13,7 +12,7 @@ impl Composable for Counter { use_future(|| async move { loop { - time::sleep(Duration::from_millis(500)).await; + //time::sleep(Duration::from_millis(500)).await; count += 1; } }); @@ -26,11 +25,13 @@ fn app() -> impl IntoComposable { (Counter { initial_value: 0 }, Counter { initial_value: 100 }) } -#[tokio::main] -async fn main() { +fn main() { let mut composition = Composition::new(app); composition.build(); - loop { - composition.rebuild().await; - } + + block_on(async move { + loop { + composition.rebuild().await; + } + }); } diff --git a/src/composition.rs b/src/composition.rs index 60fc1542..7648cd49 100644 --- a/src/composition.rs +++ b/src/composition.rs @@ -2,16 +2,17 @@ use crate::{ composable::IntoComposable, AnyComposable, BuildContext, LocalContext, Node, Scope, TaskContext, BUILD_CONTEXT, GLOBAL_CONTEXT, TASK_CONTEXT, }; -use futures::pending; +use futures::channel::mpsc; +use futures::executor::LocalPool; +use futures::StreamExt; use slotmap::DefaultKey; use std::{any::Any, cell::RefCell, rc::Rc}; -use tokio::{sync::mpsc, task::LocalSet}; /// A composition of composables. pub struct Composition { build_cx: Rc>, root: DefaultKey, - local_set: LocalSet, + task_cx: TaskContext, rx: mpsc::UnboundedReceiver>, } @@ -22,8 +23,12 @@ impl Composition { where C: IntoComposable + 'static, { - let local_set = LocalSet::new(); - local_set.enter(); + let local_set = LocalPool::new(); + let (tx, rx) = mpsc::unbounded(); + let task_cx = TaskContext { + tx, + local_pool: Rc::new(RefCell::new(local_set)), + }; { let build_cx = Rc::new(RefCell::new(BuildContext::default())); @@ -47,13 +52,11 @@ impl Composition { .borrow_mut() .nodes .insert(Rc::new(RefCell::new(node))); - let (tx, rx) = mpsc::unbounded_channel(); Self { build_cx, root, - local_set: LocalSet::new(), - task_cx: TaskContext { tx }, + task_cx, rx, } } @@ -74,8 +77,6 @@ impl Composition { .try_with(|cx| *cx.borrow_mut() = Some(self.build_cx.clone())) .unwrap(); - let g = self.local_set.enter(); - { let cx = self.build_cx.borrow_mut(); let node = cx.nodes[key].borrow_mut(); @@ -127,8 +128,6 @@ impl Composition { }; composable.borrow_mut().any_build(); - drop(g); - self.build_cx.borrow().children.get(key).cloned() }; @@ -157,22 +156,14 @@ impl Composition { loop { let fut = async { - self.rx.recv().await; + self.rx.next().await; }; if futures::poll!(Box::pin(fut)).is_ready() { break; } - let fut = async { - let fut = &mut self.local_set; - fut.await; - }; - if futures::poll!(Box::pin(fut)).is_pending() { - pending!() - } else { - break; - } + self.task_cx.local_pool.borrow_mut().run_until_stalled(); } self.compose(self.root); diff --git a/src/html/mod.rs b/src/html/mod.rs new file mode 100644 index 00000000..99ad579f --- /dev/null +++ b/src/html/mod.rs @@ -0,0 +1,64 @@ +use std::{cell::RefCell, rc::Rc}; +use web_sys::{Document, HtmlElement}; + +use crate::{Composable, Composition, IntoComposable}; + +thread_local! { + static HTML_CONTEXT: RefCell> = RefCell::default(); +} + +struct Inner { + document: Document, + body: HtmlElement, +} + +#[derive(Clone)] +pub struct HtmlContext { + inner: Rc>, +} + +impl HtmlContext { + pub fn new() -> Self { + let window = web_sys::window().unwrap(); + let document = window.document().unwrap(); + let body = document.body().unwrap(); + Self { + inner: Rc::new(RefCell::new(Inner { document, body })), + } + } + + pub fn current() -> Self { + HTML_CONTEXT + .try_with(|cx| cx.borrow().as_ref().unwrap().clone()) + .unwrap() + } + + pub fn enter(self) { + HTML_CONTEXT + .try_with(|cx| *cx.borrow_mut() = Some(self)) + .unwrap() + } +} + +#[derive(PartialEq, Eq)] +pub struct Html {} + +impl Composable for Html { + fn compose(&mut self) -> impl IntoComposable { + let cx = HtmlContext::current(); + let inner = cx.inner.borrow_mut(); + let element = inner.document.create_element("div").unwrap(); + inner.body.append_child(&element).unwrap(); + } +} + +pub fn run(content: fn() -> C) +where + C: IntoComposable + 'static, +{ + let cx = HtmlContext::new(); + cx.enter(); + + let mut composition = Composition::new(content); + composition.build() +} diff --git a/src/lib.rs b/src/lib.rs index 3264e139..7e1c1594 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ +use futures::channel::mpsc; +use futures::executor::LocalPool; use slotmap::{DefaultKey, SlotMap, SparseSecondaryMap}; use std::{any::Any, cell::RefCell, collections::HashSet, rc::Rc}; -use tokio::sync::mpsc; mod any_composable; pub use any_composable::AnyComposable; @@ -18,11 +19,14 @@ mod use_ref; pub use use_ref::{use_ref, Ref, RefMut, UseRef}; mod use_future; -pub use use_future::{use_future, UseFuture}; +pub use use_future::use_future; mod use_state; pub use use_state::{use_state, UseState}; +#[cfg(feature = "html")] +pub mod html; + #[derive(Default)] struct GlobalContext { values: SlotMap>>, @@ -35,6 +39,7 @@ thread_local! { #[derive(Clone)] struct TaskContext { + local_pool: Rc>, tx: mpsc::UnboundedSender>, } diff --git a/src/use_future.rs b/src/use_future.rs index 6661709a..48aadcd7 100644 --- a/src/use_future.rs +++ b/src/use_future.rs @@ -1,44 +1,24 @@ -use crate::{use_ref, UseRef, TASK_CONTEXT}; +use crate::{use_ref, TASK_CONTEXT}; +use futures::task::LocalSpawnExt; use futures::Future; -use tokio::task::{self, JoinHandle}; /// A hook that spawns a local future on current thread. -pub fn use_future(f: impl FnOnce() -> F) -> UseFuture +pub fn use_future(f: impl FnOnce() -> F) where F: Future + 'static, { - let handle = use_ref(|| { + let _handle = use_ref(|| { let future = f(); TASK_CONTEXT .try_with(|cx| { let guard = cx.borrow_mut(); let cx = guard.as_ref().unwrap(); let tx = cx.tx.clone(); - task::spawn_local(async move { - let output = future.await; - tx.send(Box::new(())).unwrap(); - output - }) + cx.local_pool.borrow().spawner().spawn_local(async move { + let _output = future.await; + tx.unbounded_send(Box::new(())).unwrap(); + }); }) .unwrap() }); - UseFuture { handle } } - -pub struct UseFuture { - handle: UseRef>, -} - -impl UseFuture { - pub fn abort(&self) { - self.handle.get().abort() - } -} - -impl Clone for UseFuture { - fn clone(&self) -> Self { - *self - } -} - -impl Copy for UseFuture {} diff --git a/src/use_state.rs b/src/use_state.rs index 7fbd823f..f045150e 100644 --- a/src/use_state.rs +++ b/src/use_state.rs @@ -61,7 +61,7 @@ impl UseState { let guard = cx.borrow_mut(); let cx = guard.as_ref().unwrap(); let tx = cx.tx.clone(); - tx.send(Box::new(())).unwrap(); + tx.unbounded_send(Box::new(())).unwrap(); }) .unwrap(); } diff --git a/web_examples/Cargo.toml b/web_examples/Cargo.toml new file mode 100644 index 00000000..5f004e9d --- /dev/null +++ b/web_examples/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "concoct-web-examples" +version = "0.1.0" +edition = "2021" + +[dependencies] +concoct = { path = "../", features = ["html"] } diff --git a/web_examples/index.html b/web_examples/index.html new file mode 100644 index 00000000..5171c271 --- /dev/null +++ b/web_examples/index.html @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/web_examples/src/main.rs b/web_examples/src/main.rs new file mode 100644 index 00000000..754cf1dd --- /dev/null +++ b/web_examples/src/main.rs @@ -0,0 +1,9 @@ +use concoct::{html::Html, IntoComposable}; + +fn app() -> impl IntoComposable { + Html {} +} + +fn main() { + concoct::html::run(app) +}