Skip to main content

src/etui@widgets@tree.erl

-module(etui@widgets@tree).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/etui/widgets/tree.gleam").
-export([default_glyphs/0, ascii_glyphs/0, leaf/2, node/3, leaf_with_count/3, node_with_count/4, with_count/2, tree_new/1, with_glyphs/2, with_highlight_style/2, with_colors/3, with_style/2, state_new/0, state_from_tree/1, selected/1, is_expanded/2, expand/2, collapse/2, toggle_selected/2, select_next/2, select_prev/2, visible_row_count/2, effective_offset/3, render/4]).
-export_type([tree_node/0, tree_widget/0, tree_glyphs/0, tree_state/0, visible_row/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 tree_node() :: {tree_node,
        binary(),
        binary(),
        list(tree_node()),
        {ok, integer()} | {error, nil}}.

-type tree_widget() :: {tree_widget,
        list(tree_node()),
        etui@style:color(),
        etui@style:color(),
        etui@style:style(),
        tree_glyphs()}.

-type tree_glyphs() :: {tree_glyphs, binary(), binary(), binary(), binary()}.

-type tree_state() :: {tree_state, list(binary()), binary()}.

-type visible_row() :: {visible_row,
        binary(),
        integer(),
        binary(),
        boolean(),
        {ok, integer()} | {error, nil}}.

-file("src/etui/widgets/tree.gleam", 89).
?DOC(" Default Unicode glyphs (â–¶ â–¼ and box-drawing indent).\n").
-spec default_glyphs() -> tree_glyphs().
default_glyphs() ->
    {tree_glyphs, <<"â–¶ "/utf8>>, <<"â–¼ "/utf8>>, <<"  "/utf8>>, <<"  "/utf8>>}.

-file("src/etui/widgets/tree.gleam", 94).
?DOC(" ASCII-safe glyphs for terminals without Unicode support.\n").
-spec ascii_glyphs() -> tree_glyphs().
ascii_glyphs() ->
    {tree_glyphs, <<"+ "/utf8>>, <<"- "/utf8>>, <<"  "/utf8>>, <<"  "/utf8>>}.

-file("src/etui/widgets/tree.gleam", 102).
?DOC(" Create a leaf node (no children).\n").
-spec leaf(binary(), binary()) -> tree_node().
leaf(Id, Label) ->
    {tree_node, Id, Label, [], {error, nil}}.

-file("src/etui/widgets/tree.gleam", 107).
?DOC(" Create an internal node with children.\n").
-spec node(binary(), binary(), list(tree_node())) -> tree_node().
node(Id, Label, Children) ->
    {tree_node, Id, Label, Children, {error, nil}}.

-file("src/etui/widgets/tree.gleam", 112).
?DOC(" Leaf with a right-aligned count.\n").
-spec leaf_with_count(binary(), binary(), integer()) -> tree_node().
leaf_with_count(Id, Label, Count) ->
    {tree_node, Id, Label, [], {ok, Count}}.

-file("src/etui/widgets/tree.gleam", 117).
?DOC(" Internal node with a right-aligned count.\n").
-spec node_with_count(binary(), binary(), integer(), list(tree_node())) -> tree_node().
node_with_count(Id, Label, Count, Children) ->
    {tree_node, Id, Label, Children, {ok, Count}}.

-file("src/etui/widgets/tree.gleam", 127).
?DOC(" Attach a count to an existing node.\n").
-spec with_count(tree_node(), integer()) -> tree_node().
with_count(N, Count) ->
    {tree_node,
        erlang:element(2, N),
        erlang:element(3, N),
        erlang:element(4, N),
        {ok, Count}}.

-file("src/etui/widgets/tree.gleam", 135).
?DOC(" New tree widget. The first root node is selected initially.\n").
-spec tree_new(list(tree_node())) -> tree_widget().
tree_new(Roots) ->
    {tree_widget,
        Roots,
        default,
        default,
        {style, default, default, etui@style:reverse()},
        default_glyphs()}.

-file("src/etui/widgets/tree.gleam", 149).
-spec with_glyphs(tree_widget(), tree_glyphs()) -> tree_widget().
with_glyphs(T, G) ->
    {tree_widget,
        erlang:element(2, T),
        erlang:element(3, T),
        erlang:element(4, T),
        erlang:element(5, T),
        G}.

-file("src/etui/widgets/tree.gleam", 153).
-spec with_highlight_style(tree_widget(), etui@style:style()) -> tree_widget().
with_highlight_style(T, S) ->
    {tree_widget,
        erlang:element(2, T),
        erlang:element(3, T),
        erlang:element(4, T),
        S,
        erlang:element(6, T)}.

-file("src/etui/widgets/tree.gleam", 157).
-spec with_colors(tree_widget(), etui@style:color(), etui@style:color()) -> tree_widget().
with_colors(T, Fg, Bg) ->
    {tree_widget,
        erlang:element(2, T),
        Fg,
        Bg,
        erlang:element(5, T),
        erlang:element(6, T)}.

-file("src/etui/widgets/tree.gleam", 165).
-spec with_style(tree_widget(), etui@style:style()) -> tree_widget().
with_style(T, S) ->
    {tree_widget,
        erlang:element(2, T),
        erlang:element(2, S),
        erlang:element(3, S),
        erlang:element(5, T),
        erlang:element(6, T)}.

-file("src/etui/widgets/tree.gleam", 173).
?DOC(" Initial state: first root node selected, all nodes collapsed.\n").
-spec state_new() -> tree_state().
state_new() ->
    {tree_state, [], <<""/utf8>>}.

-file("src/etui/widgets/tree.gleam", 178).
?DOC(" Initial state with first root pre-selected.\n").
-spec state_from_tree(tree_widget()) -> tree_state().
state_from_tree(T) ->
    First_id = case erlang:element(2, T) of
        [N | _] ->
            erlang:element(2, N);

        [] ->
            <<""/utf8>>
    end,
    {tree_state, [], First_id}.

-file("src/etui/widgets/tree.gleam", 190).
?DOC(" ID of the currently selected node. `Error(Nil)` if nothing selected.\n").
-spec selected(tree_state()) -> {ok, binary()} | {error, nil}.
selected(State) ->
    case erlang:element(3, State) of
        <<""/utf8>> ->
            {error, nil};

        Id ->
            {ok, Id}
    end.

-file("src/etui/widgets/tree.gleam", 198).
?DOC(" `True` if the node with the given `id` is expanded.\n").
-spec is_expanded(tree_state(), binary()) -> boolean().
is_expanded(State, Id) ->
    gleam@list:contains(erlang:element(2, State), Id).

-file("src/etui/widgets/tree.gleam", 206).
?DOC(" Expand a node (show children).\n").
-spec expand(binary(), tree_state()) -> tree_state().
expand(Id, State) ->
    case gleam@list:contains(erlang:element(2, State), Id) of
        true ->
            State;

        false ->
            {tree_state,
                [Id | erlang:element(2, State)],
                erlang:element(3, State)}
    end.

-file("src/etui/widgets/tree.gleam", 214).
?DOC(" Collapse a node (hide children).\n").
-spec collapse(binary(), tree_state()) -> tree_state().
collapse(Id, State) ->
    {tree_state,
        gleam@list:filter(erlang:element(2, State), fun(E) -> E /= Id end),
        erlang:element(3, State)}.

-file("src/etui/widgets/tree.gleam", 406).
-spec node_has_children(list(tree_node()), binary()) -> boolean().
node_has_children(Nodes, Target_id) ->
    case Nodes of
        [] ->
            false;

        [N | Rest] ->
            case erlang:element(2, N) =:= Target_id of
                true ->
                    not gleam@list:is_empty(erlang:element(4, N));

                false ->
                    node_has_children(erlang:element(4, N), Target_id) orelse node_has_children(
                        Rest,
                        Target_id
                    )
            end
    end.

-file("src/etui/widgets/tree.gleam", 219).
?DOC(" Toggle expand/collapse on the currently selected node.\n").
-spec toggle_selected(tree_state(), tree_widget()) -> tree_state().
toggle_selected(State, T) ->
    case erlang:element(3, State) of
        <<""/utf8>> ->
            State;

        Id ->
            Has_children = node_has_children(erlang:element(2, T), Id),
            case Has_children of
                false ->
                    State;

                true ->
                    case is_expanded(State, Id) of
                        true ->
                            collapse(Id, State);

                        false ->
                            expand(Id, State)
                    end
            end
    end.

-file("src/etui/widgets/tree.gleam", 419).
-spec find_next(list(binary()), binary()) -> {ok, binary()} | {error, nil}.
find_next(Ids, Current) ->
    case Ids of
        [] ->
            {error, nil};

        [_] ->
            {error, nil};

        [H, Next | Rest] ->
            case H =:= Current of
                true ->
                    {ok, Next};

                false ->
                    find_next([Next | Rest], Current)
            end
    end.

-file("src/etui/widgets/tree.gleam", 310).
-spec flatten_visible(list(tree_node()), tree_state(), integer()) -> list(visible_row()).
flatten_visible(Nodes, State, Depth) ->
    gleam@list:flat_map(
        Nodes,
        fun(N) ->
            Has_ch = not gleam@list:is_empty(erlang:element(4, N)),
            Row = {visible_row,
                erlang:element(2, N),
                Depth,
                erlang:element(3, N),
                Has_ch,
                erlang:element(5, N)},
            Child_rows = case Has_ch andalso is_expanded(
                State,
                erlang:element(2, N)
            ) of
                true ->
                    flatten_visible(erlang:element(4, N), State, Depth + 1);

                false ->
                    []
            end,
            [Row | Child_rows]
        end
    ).

-file("src/etui/widgets/tree.gleam", 240).
?DOC(" Move selection to the next visible node.\n").
-spec select_next(tree_state(), tree_widget()) -> tree_state().
select_next(State, T) ->
    Visible = flatten_visible(erlang:element(2, T), State, 0),
    Ids = gleam@list:map(Visible, fun(Row) -> erlang:element(2, Row) end),
    case find_next(Ids, erlang:element(3, State)) of
        {ok, Next_id} ->
            {tree_state, erlang:element(2, State), Next_id};

        {error, _} ->
            State
    end.

-file("src/etui/widgets/tree.gleam", 435).
-spec find_prev_loop(list(binary()), binary(), {ok, binary()} | {error, nil}) -> {ok,
        binary()} |
    {error, nil}.
find_prev_loop(Ids, Current, Prev) ->
    case Ids of
        [] ->
            {error, nil};

        [H | Rest] ->
            case H =:= Current of
                true ->
                    Prev;

                false ->
                    find_prev_loop(Rest, Current, {ok, H})
            end
    end.

-file("src/etui/widgets/tree.gleam", 431).
-spec find_prev(list(binary()), binary()) -> {ok, binary()} | {error, nil}.
find_prev(Ids, Current) ->
    find_prev_loop(Ids, Current, {error, nil}).

-file("src/etui/widgets/tree.gleam", 250).
?DOC(" Move selection to the previous visible node.\n").
-spec select_prev(tree_state(), tree_widget()) -> tree_state().
select_prev(State, T) ->
    Visible = flatten_visible(erlang:element(2, T), State, 0),
    Ids = gleam@list:map(Visible, fun(Row) -> erlang:element(2, Row) end),
    case find_prev(Ids, erlang:element(3, State)) of
        {ok, Prev_id} ->
            {tree_state, erlang:element(2, State), Prev_id};

        {error, _} ->
            State
    end.

-file("src/etui/widgets/tree.gleam", 261).
?DOC(
    " Number of visible rows (respects expand/collapse state).\n"
    " Use as `total` when building a scrollbar.\n"
).
-spec visible_row_count(tree_state(), tree_widget()) -> integer().
visible_row_count(State, T) ->
    erlang:length(flatten_visible(erlang:element(2, T), State, 0)).

-file("src/etui/widgets/tree.gleam", 462).
-spec find_row_index(list(visible_row()), binary(), integer()) -> integer().
find_row_index(Rows, Id, Acc) ->
    case Rows of
        [] ->
            0;

        [R | Rest] ->
            case erlang:element(2, R) =:= Id of
                true ->
                    Acc;

                false ->
                    find_row_index(Rest, Id, Acc + 1)
            end
    end.

-file("src/etui/widgets/tree.gleam", 450).
-spec visible_scroll(list(visible_row()), binary(), integer()) -> integer().
visible_scroll(Rows, Selected, Height) ->
    Idx = find_row_index(Rows, Selected, 0),
    case Idx < Height of
        true ->
            0;

        false ->
            (Idx - Height) + 1
    end.

-file("src/etui/widgets/tree.gleam", 267).
?DOC(
    " Effective scroll offset for a viewport of `height` rows.\n"
    " Use as `offset` when building a scrollbar.\n"
).
-spec effective_offset(tree_state(), tree_widget(), integer()) -> integer().
effective_offset(State, T, Height) ->
    case Height =< 0 of
        true ->
            0;

        false ->
            Rows = flatten_visible(erlang:element(2, T), State, 0),
            visible_scroll(Rows, erlang:element(3, State), Height)
    end.

-file("src/etui/widgets/tree.gleam", 477).
-spec repeat_string_loop(binary(), integer(), binary()) -> binary().
repeat_string_loop(S, N, Acc) ->
    case N =< 0 of
        true ->
            Acc;

        false ->
            repeat_string_loop(S, N - 1, <<Acc/binary, S/binary>>)
    end.

-file("src/etui/widgets/tree.gleam", 473).
-spec repeat_string(binary(), integer()) -> binary().
repeat_string(S, N) ->
    repeat_string_loop(S, N, <<""/utf8>>).

-file("src/etui/widgets/tree.gleam", 333).
-spec render_rows(
    etui@buffer:buffer(),
    etui@geometry:rect(),
    tree_widget(),
    tree_state(),
    list(visible_row()),
    integer(),
    integer()
) -> etui@buffer:buffer().
render_rows(Buf, Area, T, State, Rows, Scroll, Row_offset) ->
    case Row_offset >= erlang:element(3, erlang:element(3, Area)) of
        true ->
            Buf;

        false ->
            Visible_idx = Scroll + Row_offset,
            case gleam@list:drop(Rows, Visible_idx) of
                [] ->
                    Buf;

                [Row | _] ->
                    Y = erlang:element(3, erlang:element(2, Area)) + Row_offset,
                    Is_sel = erlang:element(2, Row) =:= erlang:element(3, State),
                    Prefix = <<(repeat_string(
                            erlang:element(5, erlang:element(6, T)),
                            erlang:element(3, Row)
                        ))/binary,
                        (case erlang:element(5, Row) of
                            true ->
                                case is_expanded(State, erlang:element(2, Row)) of
                                    true ->
                                        erlang:element(3, erlang:element(6, T));

                                    false ->
                                        erlang:element(2, erlang:element(6, T))
                                end;

                            false ->
                                erlang:element(4, erlang:element(6, T))
                        end)/binary>>,
                    Count_str = case erlang:element(6, Row) of
                        {ok, N} ->
                            erlang:integer_to_binary(N);

                        {error, _} ->
                            <<""/utf8>>
                    end,
                    Count_w = etui@text:cell_width(Count_str),
                    Label_budget = case Count_w of
                        0 ->
                            erlang:element(2, erlang:element(3, Area));

                        _ ->
                            gleam@int:max(
                                0,
                                (erlang:element(2, erlang:element(3, Area)) - Count_w)
                                - 1
                            )
                    end,
                    Label_raw = <<Prefix/binary,
                        (erlang:element(4, Row))/binary>>,
                    Label_part = etui@text:truncate(
                        Label_raw,
                        Label_budget,
                        <<""/utf8>>
                    ),
                    Label_padded = etui@text:pad_right(Label_part, Label_budget),
                    Padded = case Count_w of
                        0 ->
                            etui@text:pad_right(
                                Label_padded,
                                erlang:element(2, erlang:element(3, Area))
                            );

                        _ ->
                            <<<<Label_padded/binary, " "/utf8>>/binary,
                                Count_str/binary>>
                    end,
                    Truncated = etui@text:truncate(
                        Padded,
                        erlang:element(2, erlang:element(3, Area)),
                        <<""/utf8>>
                    ),
                    Padded@1 = etui@text:pad_right(
                        Truncated,
                        erlang:element(2, erlang:element(3, Area))
                    ),
                    {Fg, Bg, Modifier} = case Is_sel of
                        true ->
                            {erlang:element(2, erlang:element(5, T)),
                                erlang:element(3, erlang:element(5, T)),
                                erlang:element(4, erlang:element(5, T))};

                        false ->
                            {erlang:element(3, T),
                                erlang:element(4, T),
                                etui@style:none()}
                    end,
                    Buf2 = etui@buffer:set_string(
                        Buf,
                        {position,
                            erlang:element(2, erlang:element(2, Area)),
                            Y},
                        Padded@1,
                        Fg,
                        Bg,
                        Modifier
                    ),
                    render_rows(
                        Buf2,
                        Area,
                        T,
                        State,
                        Rows,
                        Scroll,
                        Row_offset + 1
                    )
            end
    end.

-file("src/etui/widgets/tree.gleam", 281).
?DOC(" Render the tree, scrolling so the selected node is visible.\n").
-spec render(
    etui@buffer:buffer(),
    etui@geometry:rect(),
    tree_widget(),
    tree_state()
) -> etui@buffer:buffer().
render(Buf, Area, T, State) ->
    case (erlang:element(3, erlang:element(3, Area)) =< 0) orelse (erlang:element(
        2,
        erlang:element(3, Area)
    )
    =< 0) of
        true ->
            Buf;

        false ->
            Rows = flatten_visible(erlang:element(2, T), State, 0),
            Scroll = visible_scroll(
                Rows,
                erlang:element(3, State),
                erlang:element(3, erlang:element(3, Area))
            ),
            render_rows(Buf, Area, T, State, Rows, Scroll, 0)
    end.