makeup 0.0.8

Stylish CLIs/TUIs for Rust!
Documentation
use std::marker::PhantomData;
use std::time::Duration;

use async_trait::async_trait;
use eyre::Result;

use crate::component::{DrawCommandBatch, Key, MakeupMessage, MakeupUpdate, RenderContext};
use crate::{check_mail, Component, Dimensions, DrawCommand};

/// A simple component that renders a spinner with the given text.
#[derive(Debug)]
pub struct Spinner<Message: std::fmt::Debug + Send + Sync + Clone> {
    text: String,
    spin_steps: Vec<char>,
    step: usize,
    key: Key,
    started: bool,
    interval: Duration,
    _phantom: PhantomData<Message>,
}

impl<Message: std::fmt::Debug + Send + Sync + Clone> Spinner<Message> {
    pub fn new<S: Into<String>>(text: S, spin_steps: Vec<char>, interval: Duration) -> Self {
        Self {
            text: text.into(),
            spin_steps,
            step: 0,
            key: crate::component::generate_key(),
            started: false,
            interval,
            _phantom: PhantomData,
        }
    }
}

#[async_trait]
impl<Message: std::fmt::Debug + Send + Sync + Clone + 'static> Component for Spinner<Message> {
    type Message = Message;

    fn children(&self) -> Option<Vec<&Box<dyn Component<Message = Self::Message>>>> {
        None
    }

    fn children_mut(&mut self) -> Option<Vec<&mut Box<dyn Component<Message = Self::Message>>>> {
        None
    }

    async fn update(&mut self, ctx: &mut MakeupUpdate<Self>) -> Result<()> {
        if !self.started {
            ctx.sender
                .send_makeup_message(self.key(), MakeupMessage::TimerTick(self.interval))?;
            self.started = true;
        }

        check_mail!(
            self,
            ctx,
            match _ {
                MakeupMessage::TimerTick(_) => {
                    self.step = (self.step + 1) % self.spin_steps.len();
                    #[cfg(not(test))]
                    ctx.sender.send_makeup_message_after(
                        self.key(),
                        MakeupMessage::TimerTick(self.interval),
                        self.interval,
                    )?;
                }
            }
        );

        Ok(())
    }

    async fn render(&self, _ctx: &RenderContext) -> Result<DrawCommandBatch> {
        self.batch(vec![
            DrawCommand::CharUnderCursor(self.spin_steps[self.step]),
            DrawCommand::CharUnderCursor(' '),
            DrawCommand::TextUnderCursor(self.text.clone()),
        ])
    }

    fn key(&self) -> Key {
        self.key
    }

    fn dimensions(&self) -> Result<Option<Dimensions>> {
        // +2 comes from the space and spinner character
        Ok(Some((self.text.len() as u64 + 2, 1)))
    }
}

#[cfg(test)]
mod tests {
    use std::time::Duration;

    use super::Spinner;
    use crate::component::{MakeupMessage, MessageSender, UpdateContext};
    use crate::post_office::PostOffice;
    use crate::test::assert_renders_many;
    use crate::{Component, DrawCommand};

    use eyre::Result;

    #[tokio::test]
    async fn test_it_works() -> Result<()> {
        let interval = Duration::from_millis(1);
        let mut root = Spinner::<()>::new("henol world", vec!['-', '\\', '|', '/'], interval);
        let mut post_office = PostOffice::<()>::new();
        let (tx, _rx) = tokio::sync::mpsc::unbounded_channel();

        assert_renders_many!(
            vec![
                DrawCommand::CharUnderCursor('-'),
                DrawCommand::CharUnderCursor(' '),
                DrawCommand::TextUnderCursor("henol world".into(),)
            ],
            root
        );

        post_office.send_makeup(root.key(), MakeupMessage::TimerTick(interval));

        let mut ctx = UpdateContext {
            post_office: &mut post_office,
            sender: MessageSender::new(tx.clone(), root.key()),
            focus: root.key(),
            dimensions: (100, 100),
        };
        root.update(&mut ctx).await?;

        assert_renders_many!(
            vec![
                DrawCommand::CharUnderCursor('\\'),
                DrawCommand::CharUnderCursor(' '),
                DrawCommand::TextUnderCursor("henol world".into(),)
            ],
            root
        );

        post_office.send_makeup(root.key(), MakeupMessage::TimerTick(interval));

        let mut ctx = UpdateContext {
            post_office: &mut post_office,
            sender: MessageSender::new(tx.clone(), root.key()),
            focus: root.key(),
            dimensions: (100, 100),
        };
        root.update(&mut ctx).await?;

        assert_renders_many!(
            vec![
                DrawCommand::CharUnderCursor('|'),
                DrawCommand::CharUnderCursor(' '),
                DrawCommand::TextUnderCursor("henol world".into(),)
            ],
            root
        );

        post_office.send_makeup(root.key(), MakeupMessage::TimerTick(interval));

        let mut ctx = UpdateContext {
            post_office: &mut post_office,
            sender: MessageSender::new(tx.clone(), root.key()),
            focus: root.key(),
            dimensions: (100, 100),
        };
        root.update(&mut ctx).await?;

        assert_renders_many!(
            vec![
                DrawCommand::CharUnderCursor('/'),
                DrawCommand::CharUnderCursor(' '),
                DrawCommand::TextUnderCursor("henol world".into(),)
            ],
            root
        );

        Ok(())
    }
}