tui-lipan 0.1.0

Opinionated, component-based TUI framework for Rust - declarative components, reconciliation, layout engine, focus, overlays, and rich widgets on top of ratatui.
Documentation
use tui_lipan::prelude::*;

struct InlineDemo;

#[derive(Default)]
struct State {
    draft: TextInput,
    inserted: u32,
    entries: Vec<String>,
}

#[derive(Clone, Debug)]
enum Msg {
    DraftChanged(InputEvent),
    Insert,
}

impl Component for InlineDemo {
    type Message = Msg;
    type Properties = ();
    type State = State;

    fn create_state(&self, _props: &Self::Properties) -> Self::State {
        State::default()
    }

    fn update(&mut self, msg: Self::Message, ctx: &mut Context<Self>) -> Update {
        match msg {
            Msg::DraftChanged(event) => {
                ctx.state.draft.set_text(event.value.as_ref());
                ctx.state.draft.set_cursor_keep_anchor(event.cursor);
                ctx.state.draft.set_anchor(event.anchor);
                Update::full()
            }
            Msg::Insert => {
                let mut text = ctx.state.draft.text().trim().to_string();
                if text.is_empty() {
                    text = "generated log entry".to_string();
                }

                let next = ctx.state.inserted.saturating_add(1);
                ctx.state.entries.push(format!("[{next:03}] {text}"));

                ctx.state.inserted = next;
                ctx.state.draft.clear();
                Update::full()
            }
        }
    }

    fn on_key(&mut self, key: KeyEvent, ctx: &mut Context<Self>) -> KeyUpdate {
        match key.code {
            KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => {
                ctx.quit();
                KeyUpdate::handled(Update::full())
            }
            KeyCode::Char('m') | KeyCode::Char('M') => {
                ctx.toggle_mouse_capture();
                KeyUpdate::handled(Update::full())
            }
            _ => KeyUpdate::unhandled(Update::none()),
        }
    }

    fn view(&self, ctx: &Context<Self>) -> Element {
        Frame::new()
            .title("Inline mode")
            .border(true)
            .border_style(BorderStyle::Rounded)
            .padding(1)
            .status(format!(
                "Enter records a line | m toggles mouse ({}) | q/Esc quits",
                if ctx.mouse_capture_enabled() {
                    "on"
                } else {
                    "off"
                }
            ))
            .child(
                VStack::new()
                    .gap(1)
                    .child(
                        Input::new(ctx.state.draft.text().to_string())
                            .cursor(ctx.state.draft.cursor())
                            .anchor(ctx.state.draft.anchor())
                            .placeholder("Type text and press Enter")
                            .border(true)
                            .on_change(ctx.link().callback(Msg::DraftChanged))
                            .on_key(ctx.link().key_handler(|key| match key.code {
                                KeyCode::Enter => Some(Msg::Insert),
                                _ => None,
                            })),
                    )
                    .child(
                        Button::filled("Insert")
                            .on_click(ctx.link().callback(|_| Msg::Insert))
                            .focusable(true),
                    )
                    .child(
                        Text::new(
                            ctx.state
                                .entries
                                .iter()
                                .rev()
                                .take(4)
                                .cloned()
                                .collect::<Vec<_>>()
                                .join("\n"),
                        )
                        .overflow(Overflow::Wrap),
                    )
                    .child(Text::new(format!("Inserted lines: {}", ctx.state.inserted))),
            )
            .into()
    }
}

fn main() -> Result<()> {
    App::new()
        .inline_ephemeral(8)
        .mouse(true)
        .mount(InlineDemo)
        .exit_view(|_component, ctx| {
            VStack::new()
                .gap(1)
                .child(Text::new("Inline session ended"))
                .child(Text::new(format!("Inserted lines: {}", ctx.state.inserted)))
                .into()
        })
        .run()
}