examples/minimal.rs

main()

fn main() -> Result<(), Error> {
    setup_logging()?;

    let config = Config::default();
    let theme = DarkTheme::new("Imperial".into(), IMPERIAL);
    let mut global = Global::new(config, theme);
    let mut state = Scenery::default();

    run_tui(
        init,
        render,
        event,
        error,
        &mut global,
        &mut state,
        RunConfig::default()?
            .poll(PollCrossterm) //
            .poll(PollRendered),
    )?;

    Ok(())
}

run_tui runs the event-loop and calls out to the 4 functions init, render, event and error.

RunConfig contains the configuration for the terminal and a list of event-sources that should be polled. You can add your own too.

The application state is divided into

  • Global: Everything that should be accessible throughout the application.

    • Config: I like to use this for program args and whatever permanent config I have.
    • Theme: A collection of widget-styles combined with a selection of color palettes.
  • Scenery: Contains the stateful half of the widget-tree. Plus any extra state you need for some component of your application to work.

Global

Global state that is shared independend from the state tree.

#![allow(unused)]
fn main() {
/// Globally accessible data/state.
#[derive(Debug)]
pub struct Global {
    ctx: SalsaAppContext<AppEvent, Error>,
    pub cfg: Config,
    pub theme: DarkTheme,
}

impl SalsaContext<AppEvent, Error> for Global {
    fn set_salsa_ctx(&mut self, app_ctx: SalsaAppContext<AppEvent, Error>) {
        self.ctx = app_ctx;
    }

    #[inline(always)]
    fn salsa_ctx(&self) -> &SalsaAppContext<AppEvent, Error> {
        &self.ctx
    }
}

impl Global {
    pub fn new(cfg: Config, theme: DarkTheme) -> Self {
        Self {
            ctx: Default::default(),
            cfg,
            theme,
        }
    }
}
}

rat-salsa provides some infrastructure of its own. Global implements SalsaContext to give access to this infrastructure. run_tui injects the concrete implementation via set_salsa_ctx(). This gives seamless access to all global state.

Config

Configuration data. Either start parameters or from some config. I like to keep those separate from other things.

/// Configuration.
#[derive(Debug, Default)]
pub struct Config {}

Event

Instead of rat-salsa defining some event-type, the application does it and provides conversions for every type one of the event-sources can produce.

You can also add any other messages you want to distribute via event-handling. This can replace most other forms of communication used, be it shared state or your own queues etc.

/// Application wide messages.
#[derive(Debug)]
pub enum AppEvent {
    Event(crossterm::event::Event),
    Rendered,
    Message(String),
    Status(usize, String),
}

impl From<RenderedEvent> for AppEvent {
    fn from(_: RenderedEvent) -> Self {
        Self::Rendered
    }
}

impl From<crossterm::event::Event> for AppEvent {
    fn from(value: crossterm::event::Event) -> Self {
        Self::Event(value)
    }
}

Application state

This state contains the states of any StatefulWidgets used. And everything else that is needed.

#[derive(Debug, Default)]
pub struct Minimal {
    pub menu: MenuLineState,
    pub status: StatusLineState,
    pub error_dlg: MsgDialogState,
}

Focus

Focus handling is sprinkled throughout the code. This macro defines which widgets in a container can get the focus and in what order.

impl_has_focus!(menu for Minimal);

render()

This function is the equivalent to Widget::render().

There is no trait or anything for this. If you need to structure your application just do so.

pub fn render(
    area: Rect,
    buf: &mut Buffer,
    state: &mut Minimal,
    ctx: &mut Global,
) -> Result<(), Error> {
    let t0 = SystemTime::now();

    let layout = Layout::vertical([
        Constraint::Fill(1), //
        Constraint::Length(1),
    ])
    .split(area);

    MenuLine::new()
        .styles(ctx.theme.menu_style())
        .item_parsed("_Quit")
        .render(layout[1], buf, &mut state.menu);

    if state.error_dlg.active() {
        MsgDialog::new()
            .styles(ctx.theme.msg_dialog_style())
            .render(layout[0], buf, &mut state.error_dlg);
    }

    let el = t0.elapsed().unwrap_or(Duration::from_nanos(0));
    state.status.status(1, format!("R {:.0?}", el).to_string());

    let status_layout = Layout::horizontal([
        Constraint::Fill(61), //
        Constraint::Fill(39),
    ])
    .split(layout[1]);

    StatusLine::new()
        .layout([
            Constraint::Fill(1),
            Constraint::Length(8),
            Constraint::Length(8),
        ])
        .styles(ctx.theme.statusline_style())
        .render(status_layout[1], buf, &mut state.status);

    Ok(())
}

init()

Init is one of the functions given to run_tui(). It is called once before the event-loop starts and after the SalsaAppContext is initialized.

This creates the Focus for the application and sets the focus to the first possible widget.

pub fn init(state: &mut Minimal, ctx: &mut Global) -> Result<(), Error> {
    ctx.set_focus(FocusBuilder::build_for(state));
    ctx.focus().first();
    Ok(())
}

event()

The event function is called for every event that occurs.

It returns a Control that determines what happens next on the event-loop.

pub fn event(
    event: &AppEvent,
    state: &mut Minimal,
    ctx: &mut Global,
) -> Result<Control<AppEvent>, Error> {

match is your friend here.

    match event {
        AppEvent::Event(event) => {

The Control enum comes with perks. One is the try_flow! macro, which breaks event-handling an returns early when it finds that the event has been processed. Every value but Control::Continue means the event has been processed.

There is a second macro ct_event! for crossterm-event. It creates a pattern for crossterm events with a much nicer syntax compared to raw rust.

            try_flow!(match &event {
                ct_event!(resized) => Control::Changed,
                ct_event!(key press CONTROL-'q') => Control::Quit,
                _ => Control::Continue,
            });

            try_flow!({
                if state.error_dlg.active() {
                    state.error_dlg.handle(event, Dialog).into()
                } else {
                    Control::Continue
                }
            });

Focus handling is so essential, it has its own place in SalsaAppContext. And focus handling has some quirks that are hidden behind this function.

            ctx.handle_focus(event);
            

The widgets in rat-widget all implement HandleEvent. It defines a handle() function that manages all event-handling for the specific widget. The second parameter qualifies what kind of event-handling should happen. Regular is what you normally want, but there is MouseOnly, that only deals with mouse events, and a few more.

handle() also allows for a widget-specific return type, that can communicate at a high level what has happened. Here we have the outcome that the first menu-item has been activated, whatever that means we quit. With some From magic all the other outcomes are converted to their corresponding Control enum.

            try_flow!(match state.menu.handle(event, Regular) {
                MenuOutcome::Activated(0) => Control::Quit,
                v => v.into(),
            });

            Ok(Control::Continue)
        }

Another event from a different event-source. This one is generated by run_tui() and sent immediately after rendering a frame.

This is a good point to update the Focus. All rat-widgets store their areas when rendering. And as any of them might have changed, a renewed Focus with correct areas is a good thing.

        AppEvent::Rendered => {
            ctx.set_focus(FocusBuilder::rebuild_for(state, ctx.take_focus()));
            Ok(Control::Continue)
        }

An application defined event. Instead of accessing the widget state for the error-dialog or the statusbar you can send a message and react at one point.

        AppEvent::Message(s) => {
            state.error_dlg.append(s.as_str());
            Ok(Control::Changed)
        }
        AppEvent::Status(n, s) => {
            state.status.status(*n, s);
            Ok(Control::Changed)
        }
    }
}

error()

The last of the functions given to run_tui().

At this point it can't do any better that logging and displaying any error.

pub fn error(
    event: Error,
    state: &mut Minimal,
    _ctx: &mut Global,
) -> Result<Control<AppEvent>, Error> {
    error!("{:?}", event);
    state.error_dlg.append(format!("{:?}", &*event).as_str());
    Ok(Control::Changed)
}

References

[rat-event][https://docs.rs/rat-event/] [rat-focus][https://docs.rs/rat-focus/] [rat-widget][https://docs.rs/rat-widget]

This example

[minimal.rs][https://github.com/thscharler/rat-salsa/blob/master/rat-salsa2/examples/minimal.rs]

Another minimal example, with app-level components.

[nominal.rs][https://github.com/thscharler/rat-salsa/blob/master/rat-salsa2/examples/nominal.rs]