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::*;
use tui_lipan::{HexArea, HexAreaChangeEvent, HexAreaCursorEvent, HexAreaEditEvent};

struct HexAreaDemo;

#[derive(Default)]
struct State {
    bytes: Vec<u8>,
    cursor: usize,
    anchor: Option<usize>,
    scroll_offset: usize,
    status: String,
}

#[derive(Clone, Debug)]
enum Msg {
    Cursor(HexAreaCursorEvent),
    Changed(HexAreaChangeEvent),
    Edited(HexAreaEditEvent),
    Scrolled(ScrollEvent),
}

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

    fn create_state(&self, _props: &Self::Properties) -> Self::State {
        let bytes = (0u8..=255).collect::<Vec<_>>();
        State {
            bytes,
            cursor: 0,
            anchor: None,
            scroll_offset: 0,
            status: "Arrows move, hex keys edit, Insert/Delete modify bytes".to_string(),
        }
    }

    fn view(&self, ctx: &Context<Self>) -> Element {
        let selected_count = ctx
            .state
            .anchor
            .map(|anchor| {
                let start = anchor.min(ctx.state.cursor);
                let end = anchor.max(ctx.state.cursor);
                end.saturating_sub(start).saturating_add(1)
            })
            .unwrap_or(0);

        Frame::new()
            .header_left("HexArea Binary Inspector")
            .border(true)
            .padding(1)
            .child(
                VStack::new()
                    .gap(1)
                    .child(
                        HexArea::new(ctx.state.bytes.clone())
                            .cursor(ctx.state.cursor)
                            .anchor(ctx.state.anchor)
                            .scroll_offset(Some(ctx.state.scroll_offset))
                            .bytes_per_row(16)
                            .read_only(false)
                            .show_ascii(true)
                            .show_offsets(true)
                            .uppercase_hex(true)
                            .height(Length::Flex(1))
                            .on_cursor_change(ctx.link().callback(Msg::Cursor))
                            .on_change(ctx.link().callback(Msg::Changed))
                            .on_edit(ctx.link().callback(Msg::Edited))
                            .on_scroll(ctx.link().callback(Msg::Scrolled)),
                    )
                    .child(Text::new(format!(
                        "Cursor: {}  Anchor: {:?}  Selected: {}  Bytes: {}",
                        ctx.state.cursor,
                        ctx.state.anchor,
                        selected_count,
                        ctx.state.bytes.len()
                    )))
                    .child(Text::new(format!("Status: {}", ctx.state.status))),
            )
            .into()
    }

    fn update(&mut self, msg: Self::Message, ctx: &mut Context<Self>) -> Update {
        match msg {
            Msg::Cursor(event) => {
                ctx.state.cursor = event.cursor;
                ctx.state.anchor = event.anchor;
                Update::full()
            }
            Msg::Changed(event) => {
                ctx.state.bytes = event.bytes.to_vec();
                ctx.state.cursor = event.cursor;
                ctx.state.anchor = event.anchor;
                Update::full()
            }
            Msg::Edited(event) => {
                ctx.state.status = format!(
                    "Edit {:?} at {}: {:?} -> {:?}",
                    event.kind, event.index, event.before, event.after
                );
                Update::full()
            }
            Msg::Scrolled(event) => {
                ctx.state.scroll_offset = event.offset;
                Update::full()
            }
        }
    }

    fn on_key(&mut self, key: KeyEvent, ctx: &mut Context<Self>) -> KeyUpdate {
        if matches!(key.code, KeyCode::Char('q')) {
            ctx.quit();
            return KeyUpdate::handled(Update::full());
        }
        KeyUpdate::unhandled(Update::none())
    }
}

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