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 SplitterDemo;

#[derive(Default)]
struct State {
    mode_tab: usize,
    list_selected: usize,
    table_selected: usize,
}

#[derive(Clone, Copy, Debug)]
enum Msg {
    ModeTabChanged(TabsEvent),
    ListSelect(ListEvent),
    TableSelect(TableEvent),
}

impl Component for SplitterDemo {
    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::ModeTabChanged(ev) => {
                ctx.state.mode_tab = ev.index.min(1);
                Update::full()
            }
            Msg::ListSelect(ev) => {
                ctx.state.list_selected = ev.index;
                Update::full()
            }
            Msg::TableSelect(ev) => {
                ctx.state.table_selected = ev.index;
                Update::full()
            }
        }
    }

    fn view(&self, ctx: &Context<Self>) -> Element {
        let joined = ctx.state.mode_tab == 1;
        let handle_mode = if joined {
            SplitterHandleMode::Border
        } else {
            SplitterHandleMode::Gutter
        };

        let mode_label = if joined { "Frame Join" } else { "Classic" };
        let editor_hint = if joined {
            "// Drag merged frame borders to resize panes."
        } else {
            "// Drag the splitter gutter to resize panes."
        };

        let items = [
            "Cargo.toml",
            "src/main.rs",
            "src/lib.rs",
            "src/widgets/splitter.rs",
            "docs/WIDGET_REPORTS.md",
            "README.md",
        ]
        .into_iter()
        .map(ListItem::new)
        .collect::<Vec<_>>();

        let list = List::new()
            .items(items)
            .selected(ctx.state.list_selected)
            .selection_symbol(Some("> "))
            .scrollbar(true)
            .on_select(ctx.link().callback(Msg::ListSelect));

        let editor = TextArea::new(format!(
            "fn main() {{\n    println!(\"Hello, Splitter!\");\n}}\n\n{}",
            editor_hint
        ))
        .line_numbers(true)
        .wrap(true)
        .border(false);

        let table = Table::new()
            .header(TableRow::new(["Key", "Value"]).style(Style::new().bold()))
            .rows([
                TableRow::new(["Language", "Rust"]),
                TableRow::new(["Target", "tui-lipan"]),
                TableRow::new(["Mode", mode_label]),
            ])
            .widths([ColumnWidth::Fixed(10), ColumnWidth::Fill(1)])
            .column_spacing(1)
            .selected(ctx.state.table_selected)
            .selection_symbol(Some("> "))
            .scrollbar(true)
            .on_select(ctx.link().callback(Msg::TableSelect));

        let right = Splitter::horizontal()
            .handle_mode(handle_mode)
            .weights(vec![0.65, 0.35])
            .child(
                Frame::new()
                    .header_left("Editor")
                    .join_frame(joined)
                    .padding(1)
                    .border(true)
                    .child(editor),
            )
            .child(
                Frame::new()
                    .header_left("Metadata")
                    .join_frame(joined)
                    .padding(1)
                    .border(true)
                    .child(table),
            );

        let root = Splitter::vertical()
            .handle_mode(handle_mode)
            .weights(vec![0.3, 0.7])
            .child(
                Frame::new()
                    .header_left("Files")
                    .join_frame(joined)
                    .padding(1)
                    .border(true)
                    .child(list),
            )
            .child(right);

        let tabs = Tabs::new()
            .tabs(vec![Tab::new("Classic"), Tab::new("Frame Join")])
            .active(ctx.state.mode_tab.min(1))
            .on_change(ctx.link().callback(Msg::ModeTabChanged));

        Frame::new()
            .header_left("Splitter Demo")
            .border(true)
            .padding(1)
            .child(VStack::new().gap(1).child(tabs).child(root))
            .into()
    }
}

fn main() -> Result<()> {
    App::new().title("Splitter Demo").mount(SplitterDemo).run()
}