diff --git a/examples/tutorial/src/main.rs b/examples/tutorial/src/main.rs index 7ef4e27..5cc0178 100644 --- a/examples/tutorial/src/main.rs +++ b/examples/tutorial/src/main.rs @@ -1,6 +1,6 @@ use std::collections::BTreeMap; -use ravel::{with, with_local}; +use ravel::{adapt_ref, with, with_local}; use ravel_web::{ any, attr, collections::{btree_map, slice}, @@ -123,11 +123,22 @@ fn local_state() -> View!(Model) { // We provide an initialization callback, which is only run when the // component is constructed for the first time. || 0, + // Inside the body, we have a reference to the current local state. + // Because we now have access to both the global and local state types + // we need to produce a `View!((Model, usize))`. |cx, local_count| { - // Inside the body, we have a reference to the current local state. + /// Displays the value. Since this returns `View!(Model)` (like any + /// other component in our application), we can no longer call it + /// directly here. + fn display_counter(count: usize) -> View!(Model) { + el::p(("Local count: ", display(count))) + } + cx.build(( el::h2("Local state"), - el::p(("Local count: ", display(*local_count))), + // To use [`display_counter`], we need to "adapt" it to the + // correct type. + adapt_ref(display_counter(*local_count), |(model, _)| model), el::p(el::button(( "Increment local count", // Although we have a reference to the current value, we diff --git a/ravel-web/src/lib.rs b/ravel-web/src/lib.rs index d5c392d..4c82ae3 100644 --- a/ravel-web/src/lib.rs +++ b/ravel-web/src/lib.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use atomic_waker::AtomicWaker; use dom::Position; -use ravel::{Builder, Cx, CxRep, WithLocalState}; +use ravel::{AdaptState, Builder, Cx, CxRep, WithLocalState}; mod any; pub mod attr; @@ -45,6 +45,7 @@ pub struct RebuildCx<'cx> { pub trait ViewMarker {} impl ViewMarker for WithLocalState {} +impl ViewMarker for AdaptState {} macro_rules! tuple_state { ($($a:ident),*) => { diff --git a/ravel/src/adapt.rs b/ravel/src/adapt.rs new file mode 100644 index 0000000..35d5685 --- /dev/null +++ b/ravel/src/adapt.rs @@ -0,0 +1,109 @@ +use std::marker::PhantomData; + +use crate::{Builder, CxRep, State}; + +/// A [`Builder`] created from [`adapt`]. +pub struct Adapt { + builder: B, + f: F, + phantom: PhantomData<(S, Output)>, +} + +impl Builder for Adapt +where + B: Builder, +{ + type State = AdaptState; + + fn build(self, cx: R::BuildCx<'_>) -> Self::State { + AdaptState { + inner: self.builder.build(cx), + f: self.f, + } + } + + fn rebuild(self, cx: R::RebuildCx<'_>, state: &mut Self::State) { + state.f = self.f; + self.builder.rebuild(cx, &mut state.inner) + } +} + +/// A reference to a [`State`] which must be [`State::run`]. +pub struct Thunk<'s, S> { + state: &'s mut S, +} + +/// The result of [`Thunk::run`]. +pub struct ThunkResult { + phantom: PhantomData, +} + +impl<'s, S> Thunk<'s, S> { + /// Consumes the [`Thunk`], invoking [`State::run`] on `S` with `output`. + pub fn run(self, output: &mut Output) -> ThunkResult + where + S: State, + { + self.state.run(output); + ThunkResult { + phantom: PhantomData, + } + } +} + +/// The state of an [`Adapt`]. +pub struct AdaptState { + inner: S, + f: F, +} + +impl State for AdaptState +where + F: 'static + FnMut(Thunk, &mut Output) -> ThunkResult, +{ + fn run(&mut self, output: &mut Output) { + (self.f)( + Thunk { + state: &mut self.inner, + }, + output, + ); + } +} + +/// Adapts a [`Builder`] so that its [`State`] is compatible with a different +/// `Output` type. +/// +/// The provided callback must call [`Thunk::run`] with an adapted reference. +pub fn adapt(builder: B, f: F) -> Adapt +where + F: 'static + FnMut(Thunk, &mut Output) -> ThunkResult, +{ + Adapt { + builder, + f, + phantom: PhantomData, + } +} + +/// Adapts a [`Builder`] so that its [`State`] is compatible with a different +/// `Output` type. +/// +/// This is a simpler variant of [`adapt`] for the common case where the adapted +/// reference can be borrowed from `Output`. +pub fn adapt_ref( + builder: B, + mut f: F, +) -> Adapt< + B, + // TODO: Remove this impl trait + impl 'static + FnMut(Thunk, &mut Output) -> ThunkResult, + S, + Output, +> +where + F: 'static + FnMut(&mut Output) -> &mut R, + S: State, +{ + adapt(builder, move |thunk, output| thunk.run(f(output))) +} diff --git a/ravel/src/lib.rs b/ravel/src/lib.rs index 5481546..0938929 100644 --- a/ravel/src/lib.rs +++ b/ravel/src/lib.rs @@ -7,9 +7,11 @@ use std::{marker::PhantomData, mem::MaybeUninit}; use paste::paste; +mod adapt; mod any; mod local; +pub use adapt::*; pub use any::*; pub use local::*;