-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.