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

enum Msg {
    NodeClicked(GraphNodeEvent),
    NodeHovered(GraphNodeEvent),
}

#[derive(Default)]
struct State {
    last_clicked: Option<GraphNodeEvent>,
    last_hovered: Option<GraphNodeEvent>,
}

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

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

    fn view(&self, ctx: &Context<Self>) -> Element {
        let tree = sample_tree();
        let click = ctx.link().callback(Msg::NodeClicked);
        let hover = ctx.link().callback(Msg::NodeHovered);
        let hover_style = Style::new().fg(Color::Black).bg(Color::LightCyan);

        let top_down = Graph::new()
            .root(tree.clone())
            .direction(GraphDirection::TopDown)
            .gap_x(3)
            .gap_y(1)
            .node_border(true)
            .border(true)
            .padding(1)
            .width(Length::Auto)
            .height(Length::Auto)
            .on_node_click(click.clone())
            .on_node_hover(hover.clone())
            .node_hover_style(hover_style);

        let left_right = Graph::new()
            .root(tree.clone())
            .direction(GraphDirection::LeftRight)
            .gap_x(4)
            .gap_y(1)
            .node_border(true)
            .edge_style(Style::new().fg(Color::LightGreen))
            .node_style(Style::new().fg(Color::LightYellow))
            .border(true)
            .padding(1)
            .width(Length::Auto)
            .height(Length::Auto)
            .on_node_click(click.clone())
            .on_node_hover(hover.clone())
            .node_hover_style(hover_style);

        let compact_left_right = Graph::new()
            .root(tree.clone())
            .direction(GraphDirection::LeftRight)
            .gap_x(4)
            .gap_y(1)
            .node_border(false)
            .edge_style(Style::new().fg(Color::LightGreen))
            .node_style(Style::new().fg(Color::LightYellow))
            .border(true)
            .padding(1)
            .width(Length::Auto)
            .height(Length::Auto)
            .on_node_click(click.clone())
            .on_node_hover(hover.clone())
            .node_hover_style(Style::new().lighten_by(0.0));

        let compact = Graph::new()
            .root(tree.clone())
            .direction(GraphDirection::TopDown)
            .gap_x(2)
            .gap_y(1)
            .node_border(false)
            .edge_style(Style::new().fg(Color::Gray))
            .node_style(Style::new().fg(Color::White))
            .border(true)
            .padding(1)
            .width(Length::Auto)
            .height(Length::Auto)
            .on_node_click(click.clone())
            .on_node_hover(hover.clone())
            .node_hover_style(hover_style);

        let rounded_nodes = Graph::new()
            .root(tree.clone())
            .gap_x(3)
            .gap_y(1)
            .node_border_style(BorderStyle::Rounded)
            .edge_style(Style::new().fg(Color::Gray))
            .node_style(Style::new().fg(Color::LightBlue))
            .border(true)
            .padding(1)
            .width(Length::Auto)
            .height(Length::Auto)
            .on_node_click(click.clone())
            .on_node_hover(hover.clone())
            .node_hover_style(hover_style);

        let rounded_edges = Graph::new()
            .root(tree.clone())
            .gap_x(3)
            .gap_y(1)
            .edge_border_style(BorderStyle::Rounded)
            .edge_style(Style::new().fg(Color::LightCyan))
            .node_style(Style::new().fg(Color::White))
            .border(true)
            .padding(1)
            .width(Length::Auto)
            .height(Length::Auto)
            .on_node_click(click.clone())
            .on_node_hover(hover.clone())
            .node_hover_style(hover_style);

        let wrapped_labels = Graph::new()
            .root(wrapped_label_tree())
            .direction(GraphDirection::TopDown)
            .gap_x(3)
            .gap_y(1)
            .max_node_width(12)
            .node_border_style(BorderStyle::Rounded)
            .edge_border_style(BorderStyle::Rounded)
            .edge_style(Style::new().fg(Color::LightBlue))
            .node_style(Style::new().fg(Color::LightYellow))
            .border(true)
            .padding(1)
            .width(Length::Auto)
            .height(Length::Auto)
            .on_node_click(click.clone())
            .on_node_hover(hover.clone())
            .node_hover_style(hover_style);

        let rounded_both = Graph::new()
            .root(tree)
            .gap_x(3)
            .gap_y(1)
            .node_border_style(BorderStyle::Rounded)
            .edge_border_style(BorderStyle::Rounded)
            .edge_style(Style::new().fg(Color::LightMagenta))
            .node_style(Style::new().fg(Color::LightYellow))
            .border(true)
            .padding(1)
            .width(Length::Auto)
            .height(Length::Auto)
            .on_node_click(click)
            .on_node_hover(hover)
            .node_hover_style(hover_style);

        let clicked = describe_event("clicked", ctx.state.last_clicked.as_ref());
        let hovered = describe_event("hovered", ctx.state.last_hovered.as_ref());

        let showcase = VStack::new()
            .gap(1)
            .child(
                Frame::new()
                    .header_left("Theme-driven top-down tree")
                    .border(true)
                    .height(Length::Auto)
                    .padding(1)
                    .child(top_down),
            )
            .child(
                Frame::new()
                    .header_left("Left-right tree")
                    .border(true)
                    .height(Length::Auto)
                    .padding(1)
                    .child(left_right),
            )
            .child(
                Frame::new()
                    .header_left("Compact left-right tree")
                    .border(true)
                    .height(Length::Auto)
                    .padding(1)
                    .child(compact_left_right),
            )
            .child(
                Frame::new()
                    .header_left("Compact nodes")
                    .border(true)
                    .height(Length::Auto)
                    .padding(1)
                    .child(compact),
            )
            .child(
                Frame::new()
                    .header_left("Rounded nodes only")
                    .border(true)
                    .height(Length::Auto)
                    .padding(1)
                    .child(rounded_nodes),
            )
            .child(
                Frame::new()
                    .header_left("Rounded edges only")
                    .border(true)
                    .height(Length::Auto)
                    .padding(1)
                    .child(rounded_edges),
            )
            .child(
                Frame::new()
                    .header_left("Wrapped multi-line labels")
                    .border(true)
                    .height(Length::Auto)
                    .padding(1)
                    .child(wrapped_labels),
            )
            .child(
                Frame::new()
                    .header_left("Rounded nodes and edges")
                    .border(true)
                    .height(Length::Auto)
                    .padding(1)
                    .child(rounded_both),
            );

        VStack::new()
            .child(
                ScrollView::new()
                    .scrollbar(true)
                    .scroll_keys(ScrollKeymap::DEFAULT)
                    .show_scroll_indicators(false)
                    .padding(1)
                    .child(showcase),
            )
            .child(
                Frame::new()
                    .header_left("Pointer status")
                    .border(true)
                    .height(Length::Auto)
                    .padding((0, 1))
                    .child(Text::new(format!("{clicked} | {hovered}"))),
            )
            .into()
    }

    fn update(&mut self, msg: Self::Message, ctx: &mut Context<Self>) -> Update {
        match msg {
            Msg::NodeClicked(event) => ctx.state.last_clicked = Some(event),
            Msg::NodeHovered(event) => ctx.state.last_hovered = Some(event),
        }
        Update::full()
    }
}

fn describe_event(label: &str, event: Option<&GraphNodeEvent>) -> String {
    match event {
        Some(event) => format!("{label}: {} @ {:?}", event.label, event.path.segments()),
        None => format!("{label}: none"),
    }
}

fn sample_tree() -> GraphNode {
    GraphNode::new("App")
        .style(Style::new().fg(Color::LightMagenta))
        .child(
            GraphNode::new("UI")
                .child(GraphNode::new("Header"))
                .child(GraphNode::new("Panels"))
                .child(GraphNode::new("Status")),
        )
        .child(
            GraphNode::new("Data")
                .child(GraphNode::new("Cache"))
                .child(GraphNode::new("API").style(Style::new().fg(Color::LightGreen))),
        )
        .child(GraphNode::new("Tasks").child(GraphNode::new("Worker")))
}

fn wrapped_label_tree() -> GraphNode {
    GraphNode::new("Application Runtime")
        .style(Style::new().fg(Color::LightMagenta))
        .child(
            GraphNode::new("Terminal renderer with cached cells")
                .child(GraphNode::new("Diff buffer"))
                .child(GraphNode::new("Paint\ncommands")),
        )
        .child(
            GraphNode::new("Input routing")
                .child(GraphNode::new("Mouse hit testing"))
                .child(GraphNode::new("Keyboard focus")),
        )
        .child(GraphNode::new("Component update loop"))
}

fn main() -> Result<()> {
    App::new()
        .title("tui-lipan - Graph Showcase")
        .mount(GraphShowcase)
        .run()
}