use std::any::Any;
use std::future::Future;
use std::pin::Pin;
use std::time::{Duration, Instant};
use super::{ExecConfig, ExecResult};
#[derive(Debug, Clone, Default)]
pub enum AppMsg {
WindowResize { width: u16, height: u16 },
KeyInput(String),
Tick(Instant),
FocusChanged(Option<String>),
Blur,
#[default]
None,
}
pub struct BoxedMsg(Box<dyn Any + Send + 'static>);
impl BoxedMsg {
pub fn new<M: Any + Send + 'static>(msg: M) -> Self {
BoxedMsg(Box::new(msg))
}
pub fn downcast<M: Any + 'static>(self) -> Result<M, Self> {
match self.0.downcast::<M>() {
Ok(msg) => Ok(*msg),
Err(boxed) => Err(BoxedMsg(boxed)),
}
}
pub fn downcast_ref<M: Any + 'static>(&self) -> Option<&M> {
self.0.downcast_ref::<M>()
}
pub fn is<M: Any + 'static>(&self) -> bool {
self.0.is::<M>()
}
}
impl std::fmt::Debug for BoxedMsg {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "BoxedMsg(...)")
}
}
#[derive(Default)]
pub enum TypedCmd<M = ()>
where
M: Send + 'static,
{
#[default]
None,
Batch(Vec<TypedCmd<M>>),
Sequence(Vec<TypedCmd<M>>),
Perform {
future: Pin<Box<dyn Future<Output = M> + Send + 'static>>,
},
Sleep {
duration: Duration,
then: Box<TypedCmd<M>>,
},
Tick {
duration: Duration,
msg_fn: Box<dyn FnOnce(Instant) -> M + Send + 'static>,
},
Every {
duration: Duration,
msg_fn: Box<dyn FnOnce(Instant) -> M + Send + 'static>,
},
Exec {
config: ExecConfig,
msg_fn: Box<dyn FnOnce(ExecResult) -> M + Send + 'static>,
},
}
impl<M> TypedCmd<M>
where
M: Send + 'static,
{
pub fn none() -> Self {
TypedCmd::None
}
pub fn batch(cmds: impl IntoIterator<Item = TypedCmd<M>>) -> Self {
let mut cmds: Vec<TypedCmd<M>> = cmds
.into_iter()
.filter(|cmd| !matches!(cmd, TypedCmd::None))
.collect();
match cmds.len() {
0 => TypedCmd::None,
1 => cmds.pop().unwrap(),
_ => TypedCmd::Batch(cmds),
}
}
pub fn sequence(cmds: impl IntoIterator<Item = TypedCmd<M>>) -> Self {
let mut cmds: Vec<TypedCmd<M>> = cmds
.into_iter()
.filter(|cmd| !matches!(cmd, TypedCmd::None))
.collect();
match cmds.len() {
0 => TypedCmd::None,
1 => cmds.pop().unwrap(),
_ => TypedCmd::Sequence(cmds),
}
}
pub fn perform<F, Fut>(f: F) -> Self
where
F: FnOnce() -> Fut + Send + 'static,
Fut: Future<Output = M> + Send + 'static,
{
TypedCmd::Perform {
future: Box::pin(async move { f().await }),
}
}
pub fn sleep(duration: Duration) -> Self {
TypedCmd::Sleep {
duration,
then: Box::new(TypedCmd::None),
}
}
pub fn tick<F>(duration: Duration, msg_fn: F) -> Self
where
F: FnOnce(Instant) -> M + Send + 'static,
{
TypedCmd::Tick {
duration,
msg_fn: Box::new(msg_fn),
}
}
pub fn every<F>(duration: Duration, msg_fn: F) -> Self
where
F: FnOnce(Instant) -> M + Send + 'static,
{
TypedCmd::Every {
duration,
msg_fn: Box::new(msg_fn),
}
}
pub fn exec<F>(config: ExecConfig, msg_fn: F) -> Self
where
F: FnOnce(ExecResult) -> M + Send + 'static,
{
TypedCmd::Exec {
config,
msg_fn: Box::new(msg_fn),
}
}
pub fn is_none(&self) -> bool {
matches!(self, TypedCmd::None)
}
pub fn and_then(self, next: TypedCmd<M>) -> Self {
match self {
TypedCmd::None => next,
TypedCmd::Sleep { duration, then } => {
let chained = then.and_then(next);
TypedCmd::Sleep {
duration,
then: Box::new(chained),
}
}
other => TypedCmd::batch(vec![other, next]),
}
}
pub fn map<N, F>(self, f: F) -> TypedCmd<N>
where
N: Send + 'static,
F: FnOnce(M) -> N + Send + 'static + Clone,
{
match self {
TypedCmd::None => TypedCmd::None,
TypedCmd::Batch(cmds) => {
TypedCmd::Batch(cmds.into_iter().map(|c| c.map(f.clone())).collect())
}
TypedCmd::Sequence(cmds) => {
TypedCmd::Sequence(cmds.into_iter().map(|c| c.map(f.clone())).collect())
}
TypedCmd::Perform { future } => TypedCmd::Perform {
future: Box::pin(async move {
let msg = future.await;
f(msg)
}),
},
TypedCmd::Sleep { duration, then } => TypedCmd::Sleep {
duration,
then: Box::new(then.map(f)),
},
TypedCmd::Tick { duration, msg_fn } => TypedCmd::Tick {
duration,
msg_fn: Box::new(move |t| f(msg_fn(t))),
},
TypedCmd::Every { duration, msg_fn } => TypedCmd::Every {
duration,
msg_fn: Box::new(move |t| f(msg_fn(t))),
},
TypedCmd::Exec { config, msg_fn } => TypedCmd::Exec {
config,
msg_fn: Box::new(move |r| f(msg_fn(r))),
},
}
}
}
impl<M> std::fmt::Debug for TypedCmd<M>
where
M: Send + 'static,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TypedCmd::None => write!(f, "TypedCmd::None"),
TypedCmd::Batch(cmds) => f.debug_tuple("TypedCmd::Batch").field(cmds).finish(),
TypedCmd::Sequence(cmds) => f.debug_tuple("TypedCmd::Sequence").field(cmds).finish(),
TypedCmd::Perform { .. } => write!(f, "TypedCmd::Perform {{ ... }}"),
TypedCmd::Sleep { duration, then } => f
.debug_struct("TypedCmd::Sleep")
.field("duration", duration)
.field("then", then)
.finish(),
TypedCmd::Tick { duration, .. } => f
.debug_struct("TypedCmd::Tick")
.field("duration", duration)
.finish(),
TypedCmd::Every { duration, .. } => f
.debug_struct("TypedCmd::Every")
.field("duration", duration)
.finish(),
TypedCmd::Exec { config, .. } => f
.debug_struct("TypedCmd::Exec")
.field("config", config)
.finish(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, PartialEq)]
enum TestMsg {
Loaded(String),
Tick(u64),
}
#[test]
fn test_typed_cmd_none() {
let cmd: TypedCmd<TestMsg> = TypedCmd::none();
assert!(cmd.is_none());
}
#[test]
fn test_typed_cmd_default() {
let cmd: TypedCmd<TestMsg> = TypedCmd::default();
assert!(cmd.is_none());
}
#[test]
fn test_typed_cmd_batch_empty() {
let cmd: TypedCmd<TestMsg> = TypedCmd::batch(vec![]);
assert!(cmd.is_none());
}
#[test]
fn test_typed_cmd_batch_single() {
let cmd: TypedCmd<TestMsg> = TypedCmd::batch(vec![TypedCmd::none()]);
assert!(cmd.is_none());
}
#[test]
fn test_typed_cmd_batch_multiple() {
let cmd: TypedCmd<TestMsg> = TypedCmd::batch(vec![
TypedCmd::sleep(Duration::from_secs(1)),
TypedCmd::sleep(Duration::from_secs(2)),
]);
assert!(matches!(cmd, TypedCmd::Batch(_)));
}
#[test]
fn test_typed_cmd_sequence_empty() {
let cmd: TypedCmd<TestMsg> = TypedCmd::sequence(vec![]);
assert!(cmd.is_none());
}
#[test]
fn test_typed_cmd_sequence_multiple() {
let cmd: TypedCmd<TestMsg> = TypedCmd::sequence(vec![
TypedCmd::sleep(Duration::from_secs(1)),
TypedCmd::sleep(Duration::from_secs(2)),
]);
assert!(matches!(cmd, TypedCmd::Sequence(_)));
}
#[test]
fn test_typed_cmd_perform() {
let cmd: TypedCmd<TestMsg> = TypedCmd::perform(|| async { TestMsg::Loaded("data".into()) });
assert!(matches!(cmd, TypedCmd::Perform { .. }));
}
#[test]
fn test_typed_cmd_tick() {
let cmd: TypedCmd<TestMsg> = TypedCmd::tick(Duration::from_secs(1), |_| TestMsg::Tick(1));
assert!(matches!(cmd, TypedCmd::Tick { .. }));
}
#[test]
fn test_typed_cmd_every() {
let cmd: TypedCmd<TestMsg> = TypedCmd::every(Duration::from_secs(1), |_| TestMsg::Tick(1));
assert!(matches!(cmd, TypedCmd::Every { .. }));
}
#[test]
fn test_typed_cmd_and_then() {
let cmd: TypedCmd<TestMsg> = TypedCmd::sleep(Duration::from_secs(1))
.and_then(TypedCmd::sleep(Duration::from_secs(2)));
assert!(matches!(cmd, TypedCmd::Sleep { .. }));
}
#[test]
fn test_typed_cmd_debug() {
let cmd: TypedCmd<TestMsg> = TypedCmd::none();
let debug_str = format!("{:?}", cmd);
assert_eq!(debug_str, "TypedCmd::None");
}
#[test]
fn test_app_msg_default() {
let msg = AppMsg::default();
assert!(matches!(msg, AppMsg::None));
}
#[test]
fn test_boxed_msg() {
let msg = BoxedMsg::new(TestMsg::Loaded("test".into()));
assert!(msg.is::<TestMsg>());
let result = msg.downcast::<TestMsg>();
assert!(result.is_ok());
assert_eq!(result.unwrap(), TestMsg::Loaded("test".into()));
}
#[test]
fn test_boxed_msg_wrong_type() {
let msg = BoxedMsg::new(TestMsg::Loaded("test".into()));
let result = msg.downcast::<String>();
assert!(result.is_err());
}
#[test]
fn test_boxed_msg_downcast_ref() {
let msg = BoxedMsg::new(TestMsg::Tick(42));
let ref_opt = msg.downcast_ref::<TestMsg>();
assert!(ref_opt.is_some());
assert_eq!(ref_opt.unwrap(), &TestMsg::Tick(42));
}
#[derive(Debug, PartialEq)]
enum ParentMsg {
Child(TestMsg),
}
#[test]
fn test_typed_cmd_map() {
let child_cmd: TypedCmd<TestMsg> =
TypedCmd::perform(|| async { TestMsg::Loaded("data".into()) });
let parent_cmd: TypedCmd<ParentMsg> = child_cmd.map(ParentMsg::Child);
assert!(matches!(parent_cmd, TypedCmd::Perform { .. }));
}
#[test]
fn test_typed_cmd_map_batch() {
let child_cmd: TypedCmd<TestMsg> = TypedCmd::batch(vec![
TypedCmd::sleep(Duration::from_secs(1)),
TypedCmd::sleep(Duration::from_secs(2)),
]);
let parent_cmd: TypedCmd<ParentMsg> = child_cmd.map(ParentMsg::Child);
assert!(matches!(parent_cmd, TypedCmd::Batch(_)));
}
#[test]
fn test_typed_cmd_map_none() {
let child_cmd: TypedCmd<TestMsg> = TypedCmd::none();
let parent_cmd: TypedCmd<ParentMsg> = child_cmd.map(ParentMsg::Child);
assert!(parent_cmd.is_none());
}
}