tui-lipan 0.2.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 CommandPaletteDemo;

#[derive(Default)]
struct State {
    show_palette: bool,
    active_doc: String,
    devtools_on: bool,
    wraps_enabled: bool,
}

#[derive(Clone, Debug)]
enum Msg {
    TogglePalette,
    ClosePalette,
    OpenDoc(&'static str),
    ToggleDevtools,
    ToggleWrap,
    Editor(EditorMsg),
}

#[derive(Clone, Debug)]
enum EditorMsg {
    Save,
    SaveAll,
}

#[derive(Clone, PartialEq)]
struct EditorProps {
    wraps_enabled: bool,
    active_doc: String,
    on_event: Callback<EditorMsg>,
}

struct EditorPanel;

impl Component for EditorPanel {
    type Message = ();
    type Properties = EditorProps;
    type State = ();

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

    fn init(&mut self, ctx: &mut Context<Self>) -> Option<Command> {
        let on_save = ctx.props.on_event.clone();
        ctx.register_command(
            CommandEntry::builder("editor.save")
                .label("Editor: Save")
                .description("Save current file")
                .category("Editor")
                .keybinding_hint("ctrl-s")
                .enabled(true)
                .handler(Callback::new(move |_| on_save.emit(EditorMsg::Save)))
                .build(),
        );

        let on_save_all = ctx.props.on_event.clone();
        ctx.register_command(
            CommandEntry::builder("editor.save-all")
                .label("Editor: Save all")
                .description("Save all open files")
                .category("Editor")
                .keybinding_hint("ctrl-shift-s")
                .enabled(false)
                .handler(Callback::new(move |_| on_save_all.emit(EditorMsg::SaveAll)))
                .build(),
        );

        None
    }

    fn view(&self, ctx: &Context<Self>) -> Element {
        Frame::new()
            .header_left("Editor")
            .border(true)
            .padding(1)
            .child(
                VStack::new()
                    .gap(1)
                    .child(Text::new(format!(
                        "Active document: {}",
                        ctx.props.active_doc
                    )))
                    .child(Text::new(format!(
                        "Word wrap: {}",
                        if ctx.props.wraps_enabled {
                            "enabled"
                        } else {
                            "disabled"
                        }
                    )))
                    .child(Text::new("Commands: p opens command palette"))
                    .child(Text::new(
                        "Use palette entries to run app + editor commands",
                    )),
            )
            .into()
    }

    fn update(&mut self, _msg: Self::Message, _ctx: &mut Context<Self>) -> Update {
        Update::none()
    }
}

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

    fn create_state(&self, _props: &Self::Properties) -> Self::State {
        State {
            active_doc: "src/main.rs".to_string(),
            ..State::default()
        }
    }

    fn init(&mut self, ctx: &mut Context<Self>) -> Option<Command> {
        let registry = ctx.command_registry();

        let link = ctx.link().clone();
        registry.register(
            CommandEntry::builder("app.open-main")
                .label("Open src/main.rs")
                .description("Switch active file to src/main.rs")
                .category("Application")
                .enabled(true)
                .handler(Callback::new(move |_| {
                    link.send(Msg::OpenDoc("src/main.rs"))
                }))
                .build(),
        );

        let link = ctx.link().clone();
        registry.register(
            CommandEntry::builder("app.open-lib")
                .label("Open src/lib.rs")
                .description("Switch active file to src/lib.rs")
                .category("Application")
                .enabled(true)
                .handler(Callback::new(move |_| {
                    link.send(Msg::OpenDoc("src/lib.rs"))
                }))
                .build(),
        );

        let link = ctx.link().clone();
        registry.register(
            CommandEntry::builder("app.toggle-wrap")
                .label("Toggle word wrap")
                .description("Enable or disable editor wrapping")
                .category("Application")
                .enabled(true)
                .handler(Callback::new(move |_| link.send(Msg::ToggleWrap)))
                .build(),
        );

        let link = ctx.link().clone();
        registry.register(
            CommandEntry::builder("app.toggle-devtools-local")
                .label("Toggle DevTools (demo)")
                .description("Toggle devtools and demo indicator")
                .category("Application")
                .enabled(true)
                .handler(Callback::new(move |_| link.send(Msg::ToggleDevtools)))
                .build(),
        );

        None
    }

    fn update(&mut self, msg: Self::Message, ctx: &mut Context<Self>) -> Update {
        match msg {
            Msg::TogglePalette => {
                ctx.state.show_palette = !ctx.state.show_palette;
                Update::full()
            }
            Msg::ClosePalette => {
                ctx.state.show_palette = false;
                Update::full()
            }
            Msg::OpenDoc(path) => {
                ctx.state.active_doc = path.to_string();
                ctx.state.show_palette = false;
                Update::full()
            }
            Msg::ToggleDevtools => {
                ctx.state.devtools_on = !ctx.state.devtools_on;
                ctx.toggle_devtools();
                ctx.state.show_palette = false;
                Update::full()
            }
            Msg::ToggleWrap => {
                ctx.state.wraps_enabled = !ctx.state.wraps_enabled;
                ctx.state.show_palette = false;
                Update::full()
            }
            Msg::Editor(EditorMsg::Save) => {
                ctx.toast().push(Toast::new("Saved file"));
                ctx.state.show_palette = false;
                Update::full()
            }
            Msg::Editor(EditorMsg::SaveAll) => {
                ctx.toast()
                    .push(Toast::new("Save all is disabled in this demo"));
                ctx.state.show_palette = false;
                Update::full()
            }
        }
    }

    fn on_key(&mut self, key: KeyEvent, ctx: &mut Context<Self>) -> KeyUpdate {
        if key.code == KeyCode::Char('p') && key.mods == KeyMods::default() {
            ctx.link().send(Msg::TogglePalette);
            return KeyUpdate::handled(Update::full());
        }

        if key.code == KeyCode::Char('q') && key.mods == KeyMods::default() {
            ctx.quit();
            return KeyUpdate::handled(Update::full());
        }

        KeyUpdate::unhandled(Update::none())
    }

    fn view(&self, ctx: &Context<Self>) -> Element {
        let mut root = VStack::new()
            .padding(1)
            .gap(1)
            .child(Text::new("p: open palette | q: quit").style(Style::new().fg(Color::DarkGray)))
            .child(
                Frame::new()
                    .header_left("App state")
                    .border(true)
                    .padding(1)
                    .child(
                        VStack::new()
                            .gap(1)
                            .child(Text::new(format!("Active: {}", ctx.state.active_doc)))
                            .child(Text::new(format!(
                                "DevTools: {}",
                                if ctx.state.devtools_on { "on" } else { "off" }
                            )))
                            .child(Text::new(format!(
                                "Word wrap: {}",
                                if ctx.state.wraps_enabled {
                                    "enabled"
                                } else {
                                    "disabled"
                                }
                            ))),
                    ),
            )
            .child(child(
                || EditorPanel,
                EditorProps {
                    wraps_enabled: ctx.state.wraps_enabled,
                    active_doc: ctx.state.active_doc.clone(),
                    on_event: ctx.link().callback(Msg::Editor),
                },
            ));

        if ctx.state.show_palette {
            root = root.child(
                CommandPalette::new()
                    .title("Commands")
                    .show_disabled(true)
                    .on_close(ctx.link().callback(|_| Msg::ClosePalette)),
            );
        }

        root.into()
    }
}

fn main() -> Result<()> {
    App::new()
        .title("Command Palette")
        .mount(CommandPaletteDemo)
        .run()
}