Skip to main content

src/etui@widgets@table.erl

-module(etui@widgets@table).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/etui/widgets/table.gleam").
-export([table_new/1, with_col_widths/2, with_col_constraints/2, with_header/2, with_colors/3, with_highlight_style/2, with_style/2, with_blink/2, state_new/0, select_row/2, select_next_row/2, select_prev_row/1, clamp_state/2, effective_offset/2, render/3, render_stateful/4, render_animated/5]).
-export_type([table_widget/0, table_state/0]).

-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.

-type table_widget() :: {table_widget,
        list(list(binary())),
        list(integer()),
        list(etui@geometry:constraint()),
        boolean(),
        etui@style:color(),
        etui@style:color(),
        etui@style:style(),
        integer()}.

-type table_state() :: {table_state, integer(), integer()}.

-file("src/etui/widgets/table.gleam", 51).
?DOC(" New table. Column widths default to 10 cells each.\n").
-spec table_new(list(list(binary()))) -> table_widget().
table_new(Rows) ->
    Col_widths = case Rows of
        [] ->
            [];

        [First | _] ->
            gleam@list:repeat(10, erlang:length(First))
    end,
    {table_widget,
        Rows,
        Col_widths,
        [],
        false,
        default,
        default,
        {style, default, default, etui@style:reverse()},
        0}.

-file("src/etui/widgets/table.gleam", 73).
?DOC(" Override column widths (cell counts). Must match number of columns.\n").
-spec with_col_widths(table_widget(), list(integer())) -> table_widget().
with_col_widths(T, Widths) ->
    {table_widget,
        erlang:element(2, T),
        Widths,
        erlang:element(4, T),
        erlang:element(5, T),
        erlang:element(6, T),
        erlang:element(7, T),
        erlang:element(8, T),
        erlang:element(9, T)}.

-file("src/etui/widgets/table.gleam", 80).
?DOC(
    " Constraint-based column widths, resolved from area width at render time.\n"
    " When set, takes precedence over `col_widths`.\n"
    " Separator cells (│) are subtracted before resolving.\n"
).
-spec with_col_constraints(table_widget(), list(etui@geometry:constraint())) -> table_widget().
with_col_constraints(T, Constraints) ->
    {table_widget,
        erlang:element(2, T),
        erlang:element(3, T),
        Constraints,
        erlang:element(5, T),
        erlang:element(6, T),
        erlang:element(7, T),
        erlang:element(8, T),
        erlang:element(9, T)}.

-file("src/etui/widgets/table.gleam", 90).
?DOC(
    " When true, `t.rows[0]` is rendered as a bold header row (never selectable).\n"
    " `selected_row` uses absolute indices: 0 = header, 1 = first data row, etc.\n"
    " Initialize state with `select_row(state_new(), 1)` for the first data row.\n"
).
-spec with_header(table_widget(), boolean()) -> table_widget().
with_header(T, Show) ->
    {table_widget,
        erlang:element(2, T),
        erlang:element(3, T),
        erlang:element(4, T),
        Show,
        erlang:element(6, T),
        erlang:element(7, T),
        erlang:element(8, T),
        erlang:element(9, T)}.

-file("src/etui/widgets/table.gleam", 94).
-spec with_colors(table_widget(), etui@style:color(), etui@style:color()) -> table_widget().
with_colors(T, Fg, Bg) ->
    {table_widget,
        erlang:element(2, T),
        erlang:element(3, T),
        erlang:element(4, T),
        erlang:element(5, T),
        Fg,
        Bg,
        erlang:element(8, T),
        erlang:element(9, T)}.

-file("src/etui/widgets/table.gleam", 102).
-spec with_highlight_style(table_widget(), etui@style:style()) -> table_widget().
with_highlight_style(T, S) ->
    {table_widget,
        erlang:element(2, T),
        erlang:element(3, T),
        erlang:element(4, T),
        erlang:element(5, T),
        erlang:element(6, T),
        erlang:element(7, T),
        S,
        erlang:element(9, T)}.

-file("src/etui/widgets/table.gleam", 106).
-spec with_style(table_widget(), etui@style:style()) -> table_widget().
with_style(T, S) ->
    {table_widget,
        erlang:element(2, T),
        erlang:element(3, T),
        erlang:element(4, T),
        erlang:element(5, T),
        erlang:element(2, S),
        erlang:element(3, S),
        erlang:element(8, T),
        erlang:element(9, T)}.

-file("src/etui/widgets/table.gleam", 111).
?DOC(" Blink period in frames. 0 = steady (no blink). Use with `render_animated`.\n").
-spec with_blink(table_widget(), integer()) -> table_widget().
with_blink(T, Period) ->
    {table_widget,
        erlang:element(2, T),
        erlang:element(3, T),
        erlang:element(4, T),
        erlang:element(5, T),
        erlang:element(6, T),
        erlang:element(7, T),
        erlang:element(8, T),
        Period}.

-file("src/etui/widgets/table.gleam", 121).
?DOC(
    " Initial state: selected_row=0, no scroll offset.\n"
    " When using `with_header(True)`, row 0 is the header (not selectable).\n"
    " Use `select_row(state_new(), 1)` to start with the first data row highlighted.\n"
).
-spec state_new() -> table_state().
state_new() ->
    {table_state, 0, 0}.

-file("src/etui/widgets/table.gleam", 126).
?DOC(" Jump to a specific row (clamped to ≥ 0).\n").
-spec select_row(table_state(), integer()) -> table_state().
select_row(State, Idx) ->
    {table_state, gleam@int:max(0, Idx), erlang:element(3, State)}.

-file("src/etui/widgets/table.gleam", 131).
?DOC(" Move selection down by one, clamped to last row.\n").
-spec select_next_row(table_state(), integer()) -> table_state().
select_next_row(State, Row_count) ->
    Max_idx = gleam@int:max(0, Row_count - 1),
    {table_state,
        gleam@int:min(Max_idx, erlang:element(2, State) + 1),
        erlang:element(3, State)}.

-file("src/etui/widgets/table.gleam", 137).
?DOC(" Move selection up by one, clamped to 0.\n").
-spec select_prev_row(table_state()) -> table_state().
select_prev_row(State) ->
    {table_state,
        gleam@int:max(0, erlang:element(2, State) - 1),
        erlang:element(3, State)}.

-file("src/etui/widgets/table.gleam", 143).
?DOC(
    " Clamp `selected_row` to `[0, row_count - 1]`.\n"
    " Call after replacing the row list to avoid a stale selection index.\n"
).
-spec clamp_state(table_state(), integer()) -> table_state().
clamp_state(State, Row_count) ->
    Max = gleam@int:max(0, Row_count - 1),
    {table_state,
        gleam@int:min(erlang:element(2, State), Max),
        erlang:element(3, State)}.

-file("src/etui/widgets/table.gleam", 285).
-spec scroll_offset(integer(), integer(), integer()) -> integer().
scroll_offset(Selected, Offset, Height) ->
    case Selected < Offset of
        true ->
            Selected;

        false ->
            case Height =< 0 of
                true ->
                    Offset;

                false ->
                    case Selected >= (Offset + Height) of
                        true ->
                            (Selected - Height) + 1;

                        false ->
                            Offset
                    end
            end
    end.

-file("src/etui/widgets/table.gleam", 151).
?DOC(
    " Effective scroll offset for a viewport of `visible_data_h` data rows.\n"
    " When `show_header` is True, pass `area.size.height - 1`; otherwise pass `area.size.height`.\n"
    " Pass as `offset` to `scrollbar.scrollbar_new`.\n"
).
-spec effective_offset(table_state(), integer()) -> integer().
effective_offset(State, Visible_data_h) ->
    scroll_offset(
        erlang:element(2, State),
        erlang:element(3, State),
        Visible_data_h
    ).

-file("src/etui/widgets/table.gleam", 354).
-spec render_empty_line(integer()) -> binary().
render_empty_line(Width) ->
    etui@text:pad_right(<<""/utf8>>, Width).

-file("src/etui/widgets/table.gleam", 337).
-spec render_cells(list(binary()), list(integer())) -> list(binary()).
render_cells(Row, Widths) ->
    case {Row, Widths} of
        {_, []} ->
            [];

        {[], [Width | Rest_widths]} ->
            Formatted = etui@text:pad_right(
                <<""/utf8>>,
                gleam@int:max(0, Width - 1)
            ),
            [Formatted | render_cells([], Rest_widths)];

        {[Cell | Rest_row], [Width@1 | Rest_widths@1]} ->
            Formatted@1 = begin
                _pipe = etui@text:truncate(Cell, Width@1 - 1, <<""/utf8>>),
                etui@text:pad_right(_pipe, Width@1 - 1)
            end,
            [Formatted@1 | render_cells(Rest_row, Rest_widths@1)]
    end.

-file("src/etui/widgets/table.gleam", 315).
-spec render_row_line(list(binary()), list(integer()), integer(), boolean()) -> binary().
render_row_line(Row, Col_widths, Max_width, Is_selected) ->
    Cells = render_cells(Row, Col_widths),
    Line = gleam@list:fold(Cells, <<""/utf8>>, fun(Acc, Cell) -> case Acc of
                <<""/utf8>> ->
                    Cell;

                _ ->
                    <<<<Acc/binary, "│"/utf8>>/binary, Cell/binary>>
            end end),
    Prefix = case Is_selected of
        true ->
            <<"▶"/utf8>>;

        false ->
            <<" "/utf8>>
    end,
    _pipe = etui@text:truncate(
        <<Prefix/binary, Line/binary>>,
        Max_width,
        <<""/utf8>>
    ),
    etui@text:pad_right(_pipe, Max_width).

-file("src/etui/widgets/table.gleam", 303).
-spec get_row_at(list(list(binary())), integer()) -> {ok, list(binary())} |
    {error, nil}.
get_row_at(Rows, Idx) ->
    case Idx of
        I when I < 0 ->
            {error, nil};

        0 ->
            case Rows of
                [H | _] ->
                    {ok, H};

                [] ->
                    {error, nil}
            end;

        _ ->
            get_row_at(gleam@list:drop(Rows, 1), Idx - 1)
    end.

-file("src/etui/widgets/table.gleam", 218).
-spec do_render(
    etui@buffer:buffer(),
    etui@geometry:rect(),
    table_widget(),
    integer(),
    integer(),
    integer()
) -> etui@buffer:buffer().
do_render(Buf, Area, T, Selected, Offset, Y_offset) ->
    Col_widths = case erlang:element(4, T) of
        [] ->
            erlang:element(3, T);

        Constraints ->
            etui@geometry:resolve_sizes(
                erlang:element(2, erlang:element(3, Area)),
                Constraints
            )
    end,
    case Y_offset >= erlang:element(3, erlang:element(3, Area)) of
        true ->
            Buf;

        false ->
            Y = erlang:element(3, erlang:element(2, Area)) + Y_offset,
            Is_header = erlang:element(5, T) andalso (Y_offset =:= 0),
            Row_idx = Offset + Y_offset,
            Is_selected = not Is_header andalso (Row_idx =:= Selected),
            Row_line = case Is_header of
                true ->
                    case get_row_at(erlang:element(2, T), 0) of
                        {ok, Row} ->
                            render_row_line(
                                Row,
                                Col_widths,
                                erlang:element(2, erlang:element(3, Area)),
                                false
                            );

                        {error, _} ->
                            render_empty_line(
                                erlang:element(2, erlang:element(3, Area))
                            )
                    end;

                false ->
                    case get_row_at(erlang:element(2, T), Row_idx) of
                        {ok, Row@1} ->
                            render_row_line(
                                Row@1,
                                Col_widths,
                                erlang:element(2, erlang:element(3, Area)),
                                Is_selected
                            );

                        {error, _} ->
                            render_empty_line(
                                erlang:element(2, erlang:element(3, Area))
                            )
                    end
            end,
            {Row_fg, Row_bg, Row_modifier} = case Is_header of
                true ->
                    {erlang:element(6, T),
                        erlang:element(7, T),
                        etui@style:bold()};

                false ->
                    case Is_selected of
                        true ->
                            {erlang:element(2, erlang:element(8, T)),
                                erlang:element(3, erlang:element(8, T)),
                                erlang:element(4, erlang:element(8, T))};

                        false ->
                            {erlang:element(6, T),
                                erlang:element(7, T),
                                etui@style:none()}
                    end
            end,
            Buf_new = etui@buffer:set_string(
                Buf,
                {position, erlang:element(2, erlang:element(2, Area)), Y},
                Row_line,
                Row_fg,
                Row_bg,
                Row_modifier
            ),
            do_render(Buf_new, Area, T, Selected, Offset, Y_offset + 1)
    end.

-file("src/etui/widgets/table.gleam", 159).
?DOC(" Render without selection highlight.\n").
-spec render(etui@buffer:buffer(), etui@geometry:rect(), table_widget()) -> etui@buffer:buffer().
render(Buf, Area, T) ->
    case erlang:element(3, erlang:element(3, Area)) =< 0 of
        true ->
            Buf;

        false ->
            do_render(Buf, Area, T, -1, 0, 0)
    end.

-file("src/etui/widgets/table.gleam", 171).
?DOC(" Render with selection and auto-scrolling from state.\n").
-spec render_stateful(
    etui@buffer:buffer(),
    etui@geometry:rect(),
    table_widget(),
    table_state()
) -> etui@buffer:buffer().
render_stateful(Buf, Area, T, State) ->
    case erlang:element(3, erlang:element(3, Area)) =< 0 of
        true ->
            Buf;

        false ->
            Visible_data = case erlang:element(5, T) of
                true ->
                    gleam@int:max(
                        0,
                        erlang:element(3, erlang:element(3, Area)) - 1
                    );

                false ->
                    erlang:element(3, erlang:element(3, Area))
            end,
            Offset = scroll_offset(
                erlang:element(2, State),
                erlang:element(3, State),
                Visible_data
            ),
            do_render(Buf, Area, T, erlang:element(2, State), Offset, 0)
    end.

-file("src/etui/widgets/table.gleam", 193).
?DOC(
    " Like `render_stateful` but supports blinking selection via `t.blink_period`.\n"
    " Pass the current `AnimState.frame`; use `with_blink(t, period)` to configure.\n"
).
-spec render_animated(
    etui@buffer:buffer(),
    etui@geometry:rect(),
    table_widget(),
    table_state(),
    integer()
) -> etui@buffer:buffer().
render_animated(Buf, Area, T, State, Frame) ->
    case erlang:element(3, erlang:element(3, Area)) =< 0 of
        true ->
            Buf;

        false ->
            Visible_data = case erlang:element(5, T) of
                true ->
                    gleam@int:max(
                        0,
                        erlang:element(3, erlang:element(3, Area)) - 1
                    );

                false ->
                    erlang:element(3, erlang:element(3, Area))
            end,
            Offset = scroll_offset(
                erlang:element(2, State),
                erlang:element(3, State),
                Visible_data
            ),
            Show = etui@anim:blink(Frame, erlang:element(9, T)),
            Sel = case Show of
                true ->
                    erlang:element(2, State);

                false ->
                    -1
            end,
            do_render(Buf, Area, T, Sel, Offset, 0)
    end.