use std::future::Future;
use std::pin::Pin;
use std::time::Duration;
use frus_widgets::{ScrollTo, SheetTo};
pub(crate) struct Parts<Msg> {
pub(crate) tasks: Vec<Task<Msg>>,
pub(crate) async_tasks: Vec<AsyncTask<Msg>>,
pub(crate) timers: Vec<(Duration, Msg)>,
pub(crate) focus: Vec<u64>,
pub(crate) scrolls: Vec<(u64, ScrollTo)>,
pub(crate) sheets: Vec<(u64, SheetTo)>,
}
type Task<Msg> = Box<dyn FnOnce() -> Option<Msg> + Send + 'static>;
#[cfg(not(web))]
type AsyncTask<Msg> = Pin<Box<dyn Future<Output = Option<Msg>> + Send + 'static>>;
#[cfg(web)]
type AsyncTask<Msg> = Pin<Box<dyn Future<Output = Option<Msg>> + 'static>>;
pub struct Command<Msg> {
tasks: Vec<Task<Msg>>,
async_tasks: Vec<AsyncTask<Msg>>,
timers: Vec<(Duration, Msg)>,
focus: Vec<u64>,
scrolls: Vec<(u64, ScrollTo)>,
sheets: Vec<(u64, SheetTo)>,
}
fn focus_key(key: impl std::hash::Hash) -> u64 {
use std::hash::Hasher;
let mut hasher = std::collections::hash_map::DefaultHasher::new();
key.hash(&mut hasher);
hasher.finish()
}
impl<Msg: Send + 'static> Command<Msg> {
pub fn none() -> Self {
Self {
tasks: Vec::new(),
async_tasks: Vec::new(),
timers: Vec::new(),
focus: Vec::new(),
scrolls: Vec::new(),
sheets: Vec::new(),
}
}
pub fn batch(commands: impl IntoIterator<Item = Command<Msg>>) -> Self {
let mut tasks = Vec::new();
let mut async_tasks = Vec::new();
let mut timers = Vec::new();
let mut focus = Vec::new();
let mut scrolls = Vec::new();
let mut sheets = Vec::new();
for command in commands {
tasks.extend(command.tasks);
async_tasks.extend(command.async_tasks);
timers.extend(command.timers);
focus.extend(command.focus);
scrolls.extend(command.scrolls);
sheets.extend(command.sheets);
}
Self {
tasks,
async_tasks,
timers,
focus,
scrolls,
sheets,
}
}
pub fn after(delay: Duration, message: Msg) -> Self {
Self {
timers: vec![(delay, message)],
..Self::none()
}
}
pub fn perform(task: impl FnOnce() -> Msg + Send + 'static) -> Self {
Self {
tasks: vec![Box::new(move || Some(task()))],
..Self::none()
}
}
pub fn run(task: impl FnOnce() -> Option<Msg> + Send + 'static) -> Self {
Self {
tasks: vec![Box::new(task)],
..Self::none()
}
}
#[cfg(not(web))]
pub fn perform_async<F>(future: F) -> Self
where
F: Future<Output = Msg> + Send + 'static,
{
Self {
async_tasks: vec![Box::pin(async move { Some(future.await) })],
..Self::none()
}
}
#[cfg(web)]
pub fn perform_async<F>(future: F) -> Self
where
F: Future<Output = Msg> + 'static,
{
Self {
async_tasks: vec![Box::pin(async move { Some(future.await) })],
..Self::none()
}
}
#[cfg(not(web))]
pub fn run_async<F>(future: F) -> Self
where
F: Future<Output = Option<Msg>> + Send + 'static,
{
Self {
async_tasks: vec![Box::pin(future)],
..Self::none()
}
}
#[cfg(web)]
pub fn run_async<F>(future: F) -> Self
where
F: Future<Output = Option<Msg>> + 'static,
{
Self {
async_tasks: vec![Box::pin(future)],
..Self::none()
}
}
pub fn focus(key: impl std::hash::Hash) -> Self {
Self {
focus: vec![focus_key(key)],
..Self::none()
}
}
pub fn scroll(key: impl std::hash::Hash, to: ScrollTo) -> Self {
Self {
scrolls: vec![(focus_key(key), to)],
..Self::none()
}
}
pub fn sheet(key: impl std::hash::Hash, to: SheetTo) -> Self {
Self {
sheets: vec![(focus_key(key), to)],
..Self::none()
}
}
pub(crate) fn focus_hashed(key: u64) -> Self {
Self {
focus: vec![key],
..Self::none()
}
}
pub(crate) fn scroll_hashed(key: u64, to: ScrollTo) -> Self {
Self {
scrolls: vec![(key, to)],
..Self::none()
}
}
pub(crate) fn sheet_hashed(key: u64, to: SheetTo) -> Self {
Self {
sheets: vec![(key, to)],
..Self::none()
}
}
pub fn is_empty(&self) -> bool {
self.tasks.is_empty()
&& self.async_tasks.is_empty()
&& self.timers.is_empty()
&& self.focus.is_empty()
&& self.scrolls.is_empty()
&& self.sheets.is_empty()
}
pub(crate) fn into_parts(self) -> Parts<Msg> {
Parts {
tasks: self.tasks,
async_tasks: self.async_tasks,
timers: self.timers,
focus: self.focus,
scrolls: self.scrolls,
sheets: self.sheets,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn none_is_empty() {
assert!(Command::<u32>::none().is_empty());
}
#[test]
fn a_batch_keeps_its_sheet_requests() {
let sheet = Command::<u32>::sheet("places", SheetTo::size(1.0));
assert!(!sheet.is_empty());
let batch = Command::batch([
sheet,
Command::none(),
Command::sheet("other", SheetTo::initial()),
]);
assert_eq!(batch.into_parts().sheets.len(), 2);
}
#[test]
fn perform_yields_a_message() {
let command = Command::perform(|| 42u32);
let tasks = command.into_parts().tasks;
assert_eq!(tasks.len(), 1);
let produced = (tasks.into_iter().next().unwrap())();
assert_eq!(produced, Some(42));
}
#[test]
fn run_may_produce_nothing() {
let command = Command::run(|| -> Option<u32> { None });
let tasks = command.into_parts().tasks;
assert_eq!((tasks.into_iter().next().unwrap())(), None);
}
#[test]
fn batch_flattens_and_drops_empties() {
let command = Command::batch([
Command::perform(|| 1u32),
Command::none(),
Command::run(|| Some(2u32)),
]);
assert_eq!(command.into_parts().tasks.len(), 2);
}
#[test]
fn focus_carries_a_key_and_no_task() {
let f = Command::<u32>::focus("email");
assert!(!f.is_empty());
let parts = f.into_parts();
assert!(parts.tasks.is_empty());
assert!(parts.async_tasks.is_empty());
assert_eq!(parts.focus, vec![focus_key("email")]);
}
#[cfg(not(web))]
#[test]
fn perform_async_yields_a_message() {
let command = Command::perform_async(async { 7u32 });
let mut parts = command.into_parts();
assert!(parts.tasks.is_empty());
assert_eq!(parts.async_tasks.len(), 1);
let produced = pollster::block_on(parts.async_tasks.remove(0));
assert_eq!(produced, Some(7));
}
#[cfg(not(web))]
#[test]
fn run_async_may_produce_nothing() {
let command = Command::run_async(async { None::<u32> });
let mut parts = command.into_parts();
assert_eq!(pollster::block_on(parts.async_tasks.remove(0)), None);
}
#[cfg(not(web))]
#[test]
fn batch_combines_sync_and_async_tasks() {
let command = Command::batch([
Command::perform(|| 1u32),
Command::perform_async(async { 2u32 }),
]);
let parts = command.into_parts();
assert_eq!(parts.tasks.len(), 1);
assert_eq!(parts.async_tasks.len(), 1);
}
#[test]
fn after_carries_its_delay_and_is_not_empty() {
let command = Command::after(Duration::from_millis(250), 9u32);
assert!(!command.is_empty(), "a pending timer is an effect");
let parts = command.into_parts();
assert!(parts.tasks.is_empty() && parts.async_tasks.is_empty());
assert_eq!(parts.timers, vec![(Duration::from_millis(250), 9u32)]);
}
#[test]
fn batch_carries_timers_through() {
let command = Command::batch([
Command::after(Duration::from_millis(10), 1u32),
Command::perform(|| 2u32),
Command::after(Duration::from_millis(20), 3u32),
]);
let parts = command.into_parts();
assert_eq!(parts.tasks.len(), 1);
assert_eq!(parts.timers.len(), 2, "a batch must not swallow a timer");
}
}