Radish alpha
r
Radicle terminal user interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
lib: Improve rmUI and imUI lifecycle traits
Erik Kundt committed 1 year ago
commit 678f1e5579f6a0024348550a7b267a4c4ceced7c
parent 75cfeae169a5099976f692a16c7dfd6158f7226c
7 files changed +92 -105
modified examples/basic.rs
@@ -25,19 +25,20 @@ mollit anim id est laborum.
"#;

#[derive(Clone, Debug)]
-
struct State {
+
struct App {
    content: String,
}

+
#[derive(Clone, Debug)]
enum Message {
    Quit,
    ReverseContent,
}

-
impl store::State<()> for State {
-
    type Message = Message;
+
impl store::Update<Message> for App {
+
    type Return = ();

-
    fn update(&mut self, message: Self::Message) -> Option<tui::Exit<()>> {
+
    fn update(&mut self, message: Message) -> Option<tui::Exit<()>> {
        match message {
            Message::Quit => Some(Exit { value: None }),
            Message::ReverseContent => {
@@ -52,48 +53,49 @@ impl store::State<()> for State {
pub async fn main() -> Result<()> {
    let channel = Channel::default();
    let sender = channel.tx.clone();
-
    let state = State {
+
    let app = App {
        content: CONTENT.to_string(),
    };

-
    let page = Page::default()
-
        .content(
-
            Container::default()
-
                .header(Header::default().to_widget(sender.clone()).on_update(|_| {
-
                    HeaderProps::default()
-
                        .columns(vec![
-
                            Column::new("", Constraint::Length(0)),
-
                            Column::new(
-
                                "The standard Lorem Ipsum passage, used since the 1500s",
-
                                Constraint::Fill(1),
-
                            ),
-
                        ])
-
                        .to_boxed_any()
-
                        .into()
-
                }))
-
                .content(TextView::default().to_widget(sender.clone()).on_update(
-
                    |state: &State| {
-
                        let content = state.content.clone();
-
                        TextViewProps::default()
-
                            .state(Some(TextViewState::default().content(content)))
-
                            .handle_keys(false)
+
    let page =
+
        Page::default()
+
            .content(
+
                Container::default()
+
                    .header(Header::default().to_widget(sender.clone()).on_update(|_| {
+
                        HeaderProps::default()
+
                            .columns(vec![
+
                                Column::new("", Constraint::Length(0)),
+
                                Column::new(
+
                                    "The standard Lorem Ipsum passage, used since the 1500s",
+
                                    Constraint::Fill(1),
+
                                ),
+
                            ])
+
                            .to_boxed_any()
+
                            .into()
+
                    }))
+
                    .content(TextView::default().to_widget(sender.clone()).on_update(
+
                        |app: &App| {
+
                            let content = app.content.clone();
+
                            TextViewProps::default()
+
                                .state(Some(TextViewState::default().content(content)))
+
                                .handle_keys(false)
+
                                .to_boxed_any()
+
                                .into()
+
                        },
+
                    ))
+
                    .to_widget(sender.clone()),
+
            )
+
            .shortcuts(
+
                Shortcuts::default()
+
                    .to_widget(sender.clone())
+
                    .on_update(|_| {
+
                        ShortcutsProps::default()
+
                            .shortcuts(&[("q", "quit"), ("r", "reverse")])
                            .to_boxed_any()
                            .into()
-
                    },
-
                ))
-
                .to_widget(sender.clone()),
-
        )
-
        .shortcuts(
-
            Shortcuts::default()
-
                .to_widget(sender.clone())
-
                .on_update(|_| {
-
                    ShortcutsProps::default()
-
                        .shortcuts(&[("q", "quit"), ("r", "reverse")])
-
                        .to_boxed_any()
-
                        .into()
-
                }),
-
        )
-
        .to_widget(sender.clone());
+
                    }),
+
            )
+
            .to_widget(sender.clone());

    let window = Window::default()
        .page(0, page)
@@ -105,7 +107,7 @@ pub async fn main() -> Result<()> {
        })
        .on_update(|_| WindowProps::default().current_page(0).to_boxed_any().into());

-
    tui::rm(channel, state, window).await?;
+
    tui::rm(channel, app, window).await?;

    Ok(())
}
modified examples/hello.rs
@@ -28,18 +28,19 @@ const ALIEN: &str = r#"
"#;

#[derive(Clone, Debug)]
-
struct State {
+
struct App {
    alien: String,
}

+
#[derive(Clone, Debug)]
enum Message {
    Quit,
}

-
impl store::State<()> for State {
-
    type Message = Message;
+
impl store::Update<Message> for App {
+
    type Return = ();

-
    fn update(&mut self, message: Self::Message) -> Option<tui::Exit<()>> {
+
    fn update(&mut self, message: Message) -> Option<tui::Exit<()>> {
        match message {
            Message::Quit => Some(Exit { value: None }),
        }
@@ -50,7 +51,7 @@ impl store::State<()> for State {
pub async fn main() -> Result<()> {
    let channel = Channel::default();
    let sender = channel.tx.clone();
-
    let state = State {
+
    let app = App {
        alien: ALIEN.to_string(),
    };

@@ -60,15 +61,15 @@ pub async fn main() -> Result<()> {
            Key::Char('q') => Some(Message::Quit),
            _ => None,
        })
-
        .on_update(|state: &State| {
+
        .on_update(|app: &App| {
            TextAreaProps::default()
-
                .content(Text::styled(state.alien.clone(), Color::Rgb(85, 85, 255)))
+
                .content(Text::styled(app.alien.clone(), Color::Rgb(85, 85, 255)))
                .handle_keys(false)
                .to_boxed_any()
                .into()
        });

-
    tui::rm(channel, state, scene).await?;
+
    tui::rm(channel, app, scene).await?;

    Ok(())
}
modified examples/hello_im.rs
@@ -7,8 +7,8 @@ use ratatui::Frame;
use radicle_tui as tui;

use tui::store;
-
use tui::ui::im;
use tui::ui::im::widget::Window;
+
use tui::ui::im::Show;
use tui::ui::im::{Borders, Context};
use tui::{Channel, Exit};

@@ -28,35 +28,29 @@ const ALIEN: &str = r#"
"#;

#[derive(Clone, Debug)]
-
struct State {
+
struct App {
    alien: String,
}

-
#[derive(Clone)]
+
#[derive(Clone, Debug)]
enum Message {
    Quit,
}

-
impl store::State<()> for State {
-
    type Message = Message;
+
impl store::Update<Message> for App {
+
    type Return = ();

-
    fn update(&mut self, message: Self::Message) -> Option<tui::Exit<()>> {
+
    fn update(&mut self, message: Message) -> Option<tui::Exit<()>> {
        match message {
            Message::Quit => Some(Exit { value: None }),
        }
    }
}

-
#[derive(Default)]
-
struct App {}
-

-
impl im::App for App {
-
    type State = State;
-
    type Message = Message;
-

-
    fn update(&self, ctx: &Context<Message>, frame: &mut Frame, state: &State) -> Result<()> {
+
impl Show<Message> for App {
+
    fn show(&self, ctx: &Context<Message>, frame: &mut Frame) -> Result<()> {
        Window::default().show(ctx, |ui| {
-
            ui.text_view(frame, state.alien.clone(), &mut (0, 0), Some(Borders::None));
+
            ui.text_view(frame, self.alien.clone(), &mut (0, 0), Some(Borders::None));

            if ui.input_global(|key| key == Key::Char('q')) {
                ui.send_message(Message::Quit);
@@ -69,10 +63,11 @@ impl im::App for App {

#[tokio::main]
pub async fn main() -> Result<()> {
-
    let state = State {
+
    let app = App {
        alien: ALIEN.to_string(),
    };
-
    tui::im(Channel::default(), state, App::default()).await?;
+

+
    tui::im(Channel::default(), app).await?;

    Ok(())
}
modified src/lib.rs
@@ -13,9 +13,10 @@ use serde::ser::{Serialize, SerializeStruct, Serializer};

use anyhow::Result;

-
use store::State;
+
use store::Update;
use task::Interrupted;
use ui::im;
+
use ui::im::Show;
use ui::rm;

/// An optional return value.
@@ -156,8 +157,8 @@ pub async fn rm<S, M, P>(
    root: rm::widget::Widget<S, M>,
) -> Result<Option<P>>
where
-
    S: State<P, Message = M> + Clone + Debug + Send + Sync + 'static,
-
    M: 'static,
+
    S: Update<M, Return = P> + Clone + Debug + Send + Sync + 'static,
+
    M: Debug + Send + Sync + 'static,
    P: Clone + Debug + Send + Sync + 'static,
{
    let (terminator, mut interrupt_rx) = task::create_termination();
@@ -186,11 +187,11 @@ where
pub async fn im<S, M, P>(
    channel: Channel<M>,
    state: S,
-
    app: impl im::App<State = S, Message = M>,
+
    // app: impl im::App<State = S, Message = M>,
) -> Result<Option<P>>
where
-
    S: State<P, Message = M> + Clone + Debug + Send + Sync + 'static,
-
    M: Clone + 'static,
+
    S: Update<M, Return = P> + Show<M> + Clone + Debug + Send + Sync + 'static,
+
    M: Clone + Debug + Send + Sync + 'static,
    P: Clone + Debug + Send + Sync + 'static,
{
    let (terminator, mut interrupt_rx) = task::create_termination();
@@ -201,7 +202,7 @@ where

    tokio::try_join!(
        store.run(state, terminator, channel.rx, interrupt_rx.resubscribe()),
-
        frontend.run(app, state_tx, state_rx, interrupt_rx.resubscribe()),
+
        frontend.run(state_tx, state_rx, interrupt_rx.resubscribe()),
    )?;

    if let Ok(reason) = interrupt_rx.recv().await {
modified src/store.rs
@@ -13,15 +13,12 @@ const STORE_TICK_RATE: Duration = Duration::from_millis(1000);

/// The `State` known to the application store. It handles user-defined
/// application messages as well as ticks.
-
pub trait State<P>
-
where
-
    P: Clone + Debug + Send + Sync,
-
{
-
    type Message;
+
pub trait Update<M> {
+
    type Return;

    /// Handle a user-defined application message and return an `Exit` object
    /// in case the received message requested the application to also quit.
-
    fn update(&mut self, message: Self::Message) -> Option<Exit<P>>;
+
    fn update(&mut self, message: M) -> Option<Exit<Self::Return>>;

    /// Handle recurring tick.
    fn tick(&mut self) {}
@@ -31,7 +28,7 @@ where
/// messages coming from the frontend and updates the state accordingly.
pub struct Store<S, M, P>
where
-
    S: State<P> + Clone + Send + Sync,
+
    S: Update<M, Return = P> + Clone + Send + Sync,
    P: Clone + Debug + Send + Sync,
{
    state_tx: UnboundedSender<S>,
@@ -40,7 +37,7 @@ where

impl<S, M, P> Store<S, M, P>
where
-
    S: State<P> + Clone + Send + Sync,
+
    S: Update<M, Return = P> + Clone + Send + Sync,
    P: Clone + Debug + Send + Sync,
{
    pub fn new() -> (Self, UnboundedReceiver<S>) {
@@ -58,7 +55,7 @@ where

impl<S, M, P> Store<S, M, P>
where
-
    S: State<P, Message = M> + Clone + Debug + Send + Sync + 'static,
+
    S: Update<M, Return = P> + Clone + Debug + Send + Sync + 'static,
    P: Clone + Debug + Send + Sync + 'static,
{
    /// By calling `main_loop`, the store will wait for new messages coming
modified src/ui/im.rs
@@ -17,7 +17,7 @@ use ratatui::layout::{Constraint, Rect};
use ratatui::Frame;

use crate::event::Event;
-
use crate::store::State;
+
use crate::store::Update;
use crate::task::Interrupted;
use crate::terminal;
use crate::ui::theme::Theme;
@@ -28,16 +28,8 @@ use crate::ui::im::widget::{HeaderedTable, Widget};
const RENDERING_TICK_RATE: Duration = Duration::from_millis(250);
const INLINE_HEIGHT: usize = 20;

-
pub trait App {
-
    type State;
-
    type Message;
-

-
    fn update(
-
        &self,
-
        ctx: &Context<Self::Message>,
-
        frame: &mut Frame,
-
        state: &Self::State,
-
    ) -> Result<()>;
+
pub trait Show<M> {
+
    fn show(&self, ctx: &Context<M>, frame: &mut Frame) -> Result<()>;
}

#[derive(Default)]
@@ -46,14 +38,13 @@ pub struct Frontend {}
impl Frontend {
    pub async fn run<S, M, P>(
        self,
-
        app: impl App<State = S, Message = M>,
        state_tx: UnboundedSender<M>,
        mut state_rx: UnboundedReceiver<S>,
        mut interrupt_rx: broadcast::Receiver<Interrupted<P>>,
    ) -> anyhow::Result<Interrupted<P>>
    where
-
        S: State<P> + 'static,
-
        M: Clone + 'static,
+
        S: Update<M, Return = P> + Show<M>,
+
        M: Clone,
        P: Clone + Send + Sync + Debug,
    {
        let mut ticker = tokio::time::interval(RENDERING_TICK_RATE);
@@ -87,7 +78,7 @@ impl Frontend {
            terminal.draw(|frame| {
                let ctx = ctx.clone().with_frame_size(frame.size());

-
                if let Err(err) = app.update(&ctx, frame, &state) {
+
                if let Err(err) = state.show(&ctx, frame) {
                    log::warn!("Drawing failed: {}", err);
                }
            })?;
modified src/ui/rm.rs
@@ -7,7 +7,7 @@ use tokio::sync::broadcast;
use tokio::sync::mpsc::UnboundedReceiver;

use crate::event::Event;
-
use crate::store::State;
+
use crate::store::Update;
use crate::task::Interrupted;
use crate::terminal;
use crate::ui::rm::widget::RenderProps;
@@ -39,16 +39,16 @@ impl Frontend {
    ///
    /// Interrupt messages are being sent to broadcast channel for retrieving the
    /// application kill signal.
-
    pub async fn run<S, M, P>(
+
    pub async fn run<S, M, R>(
        self,
        mut root: Widget<S, M>,
        mut state_rx: UnboundedReceiver<S>,
-
        mut interrupt_rx: broadcast::Receiver<Interrupted<P>>,
-
    ) -> anyhow::Result<Interrupted<P>>
+
        mut interrupt_rx: broadcast::Receiver<Interrupted<R>>,
+
    ) -> anyhow::Result<Interrupted<R>>
    where
-
        S: State<P> + 'static,
+
        S: Update<M, Return = R> + 'static,
        M: 'static,
-
        P: Clone + Send + Sync + Debug,
+
        R: Clone + Send + Sync + Debug,
    {
        let mut ticker = tokio::time::interval(RENDERING_TICK_RATE);

@@ -62,7 +62,7 @@ impl Frontend {
            root
        };

-
        let result: anyhow::Result<Interrupted<P>> = loop {
+
        let result: anyhow::Result<Interrupted<R>> = loop {
            tokio::select! {
                // Tick to terminate the select every N milliseconds
                _ = ticker.tick() => (),