tuirealm 4.0.0

A tui-rs framework to build tui interfaces, inspired by React and Elm.
Documentation
//! This Example showcases the use of [`AnyProp`](tuirealm::props::AnyProp) via [`PropPayload`] for use in [`Props`], [`query`](Component::query) and [`attr`](Component::attr).
//!
//! The data structs used in this example are very simple and could be done via other values in [`PropPayload`] / [`AttrValue`],
//! but imagine this for outside sources like [`tuirealm-tree-view`](https://github.com/veeso/tui-realm/tree/feature/main/crates/tuirealm-treeview)'s Tree data.
//!
//! The main section in this Example is [`StdLabel`] and [`OurLabel`].

use std::time::Duration;

use tuirealm::application::{Application, PollStrategy};
use tuirealm::command::{Cmd, CmdResult};
use tuirealm::component::{AppComponent, Component};
use tuirealm::event::{Event, Key, KeyEvent, NoUserEvent};
use tuirealm::listener::EventListenerCfg;
use tuirealm::props::{
    AttrValue, Attribute, Color, HorizontalAlignment, PropBound, PropPayload, Props, QueryResult,
    Style,
};
use tuirealm::ratatui::Frame;
use tuirealm::ratatui::layout::{Constraint, Direction, Layout, Rect};
use tuirealm::ratatui::widgets::Paragraph;
use tuirealm::state::State;
use tuirealm::terminal::{CrosstermTerminalAdapter, TerminalAdapter, TerminalResult};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let event_listener =
        EventListenerCfg::default().crossterm_input_listener(Duration::from_millis(10), 10);

    let mut app: Application<Id, Msg, NoUserEvent> = Application::init(event_listener);

    // subscribe component to clause
    app.mount(Id::Label, Box::new(OurLabel::default()), vec![])?;

    app.active(&Id::Label).expect("failed to active");

    let mut model = Model::new(app)?;
    // Main loop
    // NOTE: loop until quit; quit is set in update if AppClose is received from counter
    while !model.quit {
        // Tick
        match model
            .app
            .tick(PollStrategy::Once(Duration::from_millis(10)))
        {
            Err(err) => {
                panic!("application error {err}");
            }
            Ok(messages) if !messages.is_empty() => {
                // NOTE: redraw if at least one msg has been processed
                model.redraw = true;
                for msg in messages {
                    model.update(msg);
                }
            }
            _ => {}
        }
        // Redraw
        if model.redraw {
            model.view();
            model.redraw = false;
        }
    }

    model.terminal.restore()?;

    Ok(())
}

#[derive(Debug, PartialEq)]
pub enum Msg {
    AppClose,
    Redraw,
}

// Let's define the component ids for our application
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
pub enum Id {
    Label,
}

pub struct Model {
    /// Application
    pub app: Application<Id, Msg, NoUserEvent>,
    /// Indicates that the application must quit
    pub quit: bool,
    /// Tells whether to redraw interface
    pub redraw: bool,
    /// Used to draw to terminal
    pub terminal: CrosstermTerminalAdapter,
}

impl Model {
    fn init_adapter() -> TerminalResult<CrosstermTerminalAdapter> {
        let mut adapter = CrosstermTerminalAdapter::new()?;
        adapter.enable_raw_mode()?;
        adapter.enter_alternate_screen()?;

        Ok(adapter)
    }

    pub fn new(app: Application<Id, Msg, NoUserEvent>) -> TerminalResult<Self> {
        Ok(Self {
            app,
            quit: false,
            redraw: true,
            terminal: Self::init_adapter()?,
        })
    }

    pub fn view(&mut self) {
        assert!(
            self.terminal
                .draw(|f| {
                    let chunks = Layout::default()
                        .direction(Direction::Vertical)
                        .margin(1)
                        .constraints(
                            [
                                Constraint::Length(3), // Label
                            ]
                            .as_ref(),
                        )
                        .split(f.area());
                    self.app.view(&Id::Label, f, chunks[0]);
                })
                .is_ok()
        );
    }
}

// Let's implement Update for model

impl Model {
    fn update(&mut self, msg: Msg) {
        // Set redraw
        self.redraw = true;
        // Match message
        match msg {
            Msg::AppClose => {
                self.quit = true; // Terminate
            }
            Msg::Redraw => (),
        }
    }
}

/// This is our custom data we want to store on [`Props`] and communicate via [`query`](Component::query) and [`attr`](Component::attr).
#[derive(Debug, Clone, PartialEq)]
struct CustomState {
    text: String,
}

impl Default for CustomState {
    fn default() -> Self {
        Self {
            text: "Default text".to_string(),
        }
    }
}

/// Simple label component; just renders a text
/// NOTE: since I need just one label, I'm not going to use different object; I will directly implement Component for Label.
/// This is not ideal actually and in a real app you should differentiate Components from Application Components.
#[derive(Debug)]
pub struct StdLabel {
    props: Props,
}

impl Default for StdLabel {
    fn default() -> Self {
        let mut props = Props::default();
        props.set(
            Attribute::Value,
            AttrValue::Payload(PropPayload::Any(CustomState::default().to_any_prop())),
        );
        Self { props }
    }
}

impl Component for StdLabel {
    fn view(&mut self, frame: &mut Frame, area: Rect) {
        // Check if visible
        if matches!(
            self.props.get(Attribute::Display),
            Some(AttrValue::Flag(false))
        ) {
            return;
        }

        // Get properties
        let text = self
            .props
            .get(Attribute::Value)
            .and_then(AttrValue::as_payload)
            .and_then(PropPayload::as_any)
            .and_then(|v| v.downcast_ref::<CustomState>())
            .map(|v| v.text.as_str())
            .unwrap_or("Unavailable; this is a bug");
        let alignment = self
            .props
            .get(Attribute::TextAlign)
            .and_then(AttrValue::as_alignment_horizontal)
            .unwrap_or(HorizontalAlignment::Left);
        let foreground = self
            .props
            .get(Attribute::Foreground)
            .and_then(AttrValue::as_color)
            .unwrap_or(Color::Reset);
        let background = self
            .props
            .get(Attribute::Background)
            .and_then(AttrValue::as_color)
            .unwrap_or(Color::Reset);
        let modifiers = self
            .props
            .get(Attribute::TextProps)
            .and_then(AttrValue::as_text_modifiers)
            .unwrap_or_default();

        let [chunk1, chunk2] = Layout::new(
            Direction::Vertical,
            [Constraint::Length(1), Constraint::Min(1)],
        )
        .areas(area);

        frame.render_widget(
            Paragraph::new("The following text should be changing when pressing <TAB>:"),
            chunk1,
        );

        frame.render_widget(
            Paragraph::new(text)
                .style(
                    Style::default()
                        .fg(foreground)
                        .bg(background)
                        .add_modifier(modifiers),
                )
                .alignment(alignment),
            chunk2,
        );
    }

    fn query<'a>(&'a self, attr: Attribute) -> Option<QueryResult<'a>> {
        self.props.get_for_query(attr)
    }

    fn attr(&mut self, attr: Attribute, value: AttrValue) {
        self.props.set(attr, value);
    }

    fn state(&self) -> State {
        State::None
    }

    fn perform(&mut self, cmd: Cmd) -> CmdResult {
        CmdResult::Invalid(cmd)
    }
}

#[derive(Debug, Component, Default)]
struct OurLabel {
    component: StdLabel,
}

impl AppComponent<Msg, NoUserEvent> for OurLabel {
    fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
        // Does nothing
        match ev {
            Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => Some(Msg::AppClose),
            Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
                let mut existing_attr = self.query(Attribute::Value).map_or_else(
                    || AttrValue::Payload(PropPayload::Any(CustomState::default().to_any_prop())),
                    QueryResult::into_attr,
                );
                let tmp = existing_attr
                    .as_payload_mut()
                    .and_then(|v| v.as_any_mut())
                    .and_then(|v| v
                    .downcast_mut::<CustomState>())
                    .expect("Unexpected type in Attribute::Value! Expected PropPayload::Any + CustomState!");
                tmp.text = match tmp.text.as_str() {
                    "Default text" => "Some other text".to_string(),
                    _ => CustomState::default().text,
                };
                self.attr(Attribute::Value, existing_attr);
                Some(Msg::Redraw)
            }
            _ => None,
        }
    }
}