sketchbook 0.0.2

Interactive visual applications in Rust
Documentation
use core::cell::RefCell;
use core::marker::PhantomData;

use alloc::boxed::Box;
use alloc::rc::Rc;
use alloc::vec::Vec;

use crate::Environment;
use crate::Mill;
use crate::MillOf;
use crate::Page;
use crate::PageOf;
use crate::Pages;
use crate::Setup;
use crate::Sketch;
use crate::Status;

use super::Ream;

/// Value that can be stored and then made into the sketch type later.
struct ConcurrentCreate<S, I> {
    input: I,
    marker: PhantomData<fn() -> S>,
}

/// Type erased creation of boxed sketches.
trait CreateItem<E>
where
    E: Environment,
{
    fn create(self, page: PageOf<E>) -> Box<dyn Sketch<Env = E>>;
    fn create_from_box(self: Box<Self>, page: PageOf<E>) -> Box<dyn Sketch<Env = E>>;
}

impl<S, I> CreateItem<S::Env> for ConcurrentCreate<S, I>
where
    S: Sketch + Setup<I> + 'static,
{
    fn create(self, page: PageOf<S::Env>) -> Box<dyn Sketch<Env = S::Env>> {
        Box::new(S::setup(page, self.input))
    }

    fn create_from_box(self: Box<Self>, page: PageOf<S::Env>) -> Box<dyn Sketch<Env = S::Env>> {
        self.create(page)
    }
}

/// Concurrent ream.
///
/// Sketches in this ream are run concurrently.
/// This removes the need for threads to run multiple sketches, but does have the possibility of a
/// sketch blocking all other sketches.
///
/// ```
/// use sketchbook::*;
/// use sketchbook::ream::*;
///
/// #[apply(derive_sketch)]
/// #[sketch(env=minimal)]
/// struct App1 {
///     #[page]
///     page: minimal::Page,
/// }
///
/// impl Setup for App1 {
///     fn setup(page: minimal::Page, _: ()) -> Self {
///         Self { page }
///     }
/// }
///
/// #[apply(derive_sketch)]
/// #[sketch(env=minimal)]
/// struct App2 {
///     #[page]
///     page: minimal::Page,
/// }
///
/// impl Setup<ConcurrentProxy<minimal::Env>> for App2 {
///     fn setup(page: minimal::Page, proxy: ConcurrentProxy<minimal::Env>) -> Self {
///         // add an extra page after this sketch gets created
///         proxy.add_page::<App1>();
///     
///         Self { page }
///     }
/// }
///
/// // create book and add pages
/// let mut pages = Book::<Concurrent<_>>::pages();
/// pages.add_page::<App1>();
/// pages.add_page_with::<App2, _>(pages.ream_proxy());
/// pages.bind();
/// ```
#[cfg_attr(docsrs, doc(cfg(feature = "ream-concurrent")))]
pub struct Concurrent<E> {
    /// Shared queue that the proxies can use.
    to_create_queue: Rc<RefCell<Vec<Box<dyn CreateItem<E>>>>>,

    /// Queue of sketches to make from when this was created.
    /// This gets emptied on the first call to run the ream.
    to_creates: Vec<Box<dyn CreateItem<E>>>,

    /// Sketches that are running.
    sketches: Vec<Box<dyn Sketch<Env = E>>>,
}

impl<E> core::fmt::Debug for Concurrent<E> {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.debug_struct("Concurrent").finish()
    }
}

impl<E> Default for Concurrent<E>
where
    E: Environment,
{
    fn default() -> Self {
        Self {
            sketches: Vec::new(),
            to_creates: Vec::new(),
            to_create_queue: Rc::new(RefCell::new(Vec::new())),
        }
    }
}

impl<E> Ream for Concurrent<E>
where
    E: Environment,
{
    type Env = E;

    type Error = core::convert::Infallible;

    fn update(&mut self, mill: &mut MillOf<Self::Env>) -> Result<Status, Self::Error> {
        // create starting sketches
        if !self.to_creates.is_empty() {
            let to_creates = core::mem::take(&mut self.to_creates);
            for to_create in to_creates {
                self.sketches
                    .push(to_create.create_from_box(mill.new_page()));
            }
        }

        // create proxy created sketches
        if !self.to_create_queue.borrow().is_empty() {
            let queue: Vec<Box<dyn CreateItem<E>>> =
                core::mem::take(self.to_create_queue.borrow_mut().as_mut());
            for to_create in queue {
                self.sketches
                    .push(to_create.create_from_box(mill.new_page()));
            }
        }

        // run each sketch in order
        for sketch in &mut self.sketches {
            sketch.get_page_mut().start_group();
            if let Some(event) = sketch.as_mut().get_page_mut().next_event_in_group() {
                sketch.get_page_mut().before_event(&event);
                sketch.handle_environment_event(&event);
                sketch.get_page_mut().finish_event(event);
            }
            sketch.get_page_mut().end_group();
        }

        // remove any sketches that have stopped
        self.sketches
            .retain(|sketch| sketch.get_page().status() == Status::Continue);

        // if there are no more sketches then signal stop
        if self.sketches.is_empty() {
            Ok(Status::Stop)
        } else {
            Ok(Status::Continue)
        }
    }
}

impl<E> Concurrent<E>
where
    E: Environment,
{
    /// Create proxy value that can create sketches.
    ///
    /// Multiple proxies can be crated at a time.
    pub fn create_proxy(&self) -> ConcurrentProxy<E> {
        ConcurrentProxy {
            to_create_queue: self.to_create_queue.clone(),
        }
    }
}

/// Proxy to [`Concurrent`] ream.
///
/// Can create sketches on the ream.
#[cfg_attr(docsrs, doc(cfg(feature = "ream-concurrent")))]
pub struct ConcurrentProxy<E> {
    to_create_queue: Rc<RefCell<Vec<Box<dyn CreateItem<E>>>>>,
}

impl<E> core::fmt::Debug for ConcurrentProxy<E> {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.debug_struct("ConcurrentProxy").finish()
    }
}

impl<E> ConcurrentProxy<E>
where
    E: Environment,
{
    /// Add page to ream.
    ///
    /// The page will have a new sketch of the given type `S`.
    pub fn add_page<S>(&self)
    where
        S: Sketch<Env = E> + Setup<()> + 'static,
    {
        self.add_page_with::<S, ()>(())
    }

    /// Add page to ream with some input value.
    ///
    /// The page will have a new sketch of the given type `S`.
    pub fn add_page_with<S, I>(&self, input: I)
    where
        S: Sketch<Env = E> + Setup<I> + 'static,
        I: 'static,
    {
        let mut queue = self.to_create_queue.borrow_mut();
        queue.push(Box::new(ConcurrentCreate::<S, I> {
            marker: PhantomData,
            input,
        }));
    }
}

/// Extension trait for adding pages to book.
#[cfg_attr(docsrs, doc(cfg(feature = "ream-concurrent")))]
pub trait ConcurrentBookExt<E>
where
    E: Environment,
{
    /// Create proxy to ream.
    fn ream_proxy(&self) -> ConcurrentProxy<E>;

    /// Add page to ream.
    ///
    /// The page will have a new sketch of the given type `S`.
    fn add_page<S>(&mut self)
    where
        S: Setup<()> + Sketch<Env = E> + 'static;

    /// Add page to ream with some input value.
    ///
    /// The page will have a new sketch of the given type `S`.
    fn add_page_with<S, I>(&mut self, input: I)
    where
        S: Setup<I> + Sketch<Env = E> + 'static,
        I: 'static;
}

impl<E> ConcurrentBookExt<E> for Pages<Concurrent<E>>
where
    E: Environment,
{
    fn ream_proxy(&self) -> ConcurrentProxy<E> {
        self.ream.create_proxy()
    }

    fn add_page<S>(&mut self)
    where
        S: Setup<()> + Sketch<Env = E> + 'static,
    {
        self.add_page_with::<S, ()>(())
    }

    fn add_page_with<S, I>(&mut self, input: I)
    where
        S: Setup<I> + Sketch<Env = E> + 'static,
        I: 'static,
    {
        self.ream
            .to_creates
            .push(Box::new(ConcurrentCreate::<S, I> {
                marker: PhantomData,
                input,
            }));
    }
}