-module(etui@widgets@textarea). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/etui/widgets/textarea.gleam"). -export([textarea_new/0, with_max_lines/2, with_max_line_length/2, with_colors/3, with_style/2, with_cursor_style/2, state_new/0, state_from_string/1, value/1, line_count/1, effective_offset/2, cursor_screen_pos/2, insert_char/3, backspace/1, newline/2, move_cursor_left/1, move_cursor_right/1, move_cursor_up/1, move_cursor_down/1, move_to_line_start/1, move_to_line_end/1, delete_to_line_end/1, render/4]). -export_type([text_area/0, text_area_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 text_area() :: {text_area, integer(), integer(), etui@style:color(), etui@style:color(), etui@style:style()}. -type text_area_state() :: {text_area_state, list(binary()), integer(), integer()}. -file("src/etui/widgets/textarea.gleam", 72). ?DOC(" New textarea with default styles and no limits.\n"). -spec textarea_new() -> text_area(). textarea_new() -> {text_area, 0, 0, default, default, {style, default, default, etui@style:reverse()}}. -file("src/etui/widgets/textarea.gleam", 86). -spec with_max_lines(text_area(), integer()) -> text_area(). with_max_lines(W, N) -> {text_area, N, erlang:element(3, W), erlang:element(4, W), erlang:element(5, W), erlang:element(6, W)}. -file("src/etui/widgets/textarea.gleam", 90). -spec with_max_line_length(text_area(), integer()) -> text_area(). with_max_line_length(W, N) -> {text_area, erlang:element(2, W), N, erlang:element(4, W), erlang:element(5, W), erlang:element(6, W)}. -file("src/etui/widgets/textarea.gleam", 94). -spec with_colors(text_area(), etui@style:color(), etui@style:color()) -> text_area(). with_colors(W, Fg, Bg) -> {text_area, erlang:element(2, W), erlang:element(3, W), Fg, Bg, erlang:element(6, W)}. -file("src/etui/widgets/textarea.gleam", 98). -spec with_style(text_area(), etui@style:style()) -> text_area(). with_style(W, S) -> {text_area, erlang:element(2, W), erlang:element(3, W), erlang:element(2, S), erlang:element(3, S), erlang:element(6, W)}. -file("src/etui/widgets/textarea.gleam", 102). -spec with_cursor_style(text_area(), etui@style:style()) -> text_area(). with_cursor_style(W, S) -> {text_area, erlang:element(2, W), erlang:element(3, W), erlang:element(4, W), erlang:element(5, W), S}. -file("src/etui/widgets/textarea.gleam", 110). ?DOC(" Empty state: one empty line, cursor at top-left.\n"). -spec state_new() -> text_area_state(). state_new() -> {text_area_state, [<<""/utf8>>], 0, 0}. -file("src/etui/widgets/textarea.gleam", 115). ?DOC(" State pre-populated from a string (splits on `\\n`).\n"). -spec state_from_string(binary()) -> text_area_state(). state_from_string(S) -> Lines = gleam@string:split(S, <<"\n"/utf8>>), Row = erlang:length(Lines) - 1, Last_line = case gleam@list:last(Lines) of {ok, L} -> L; {error, _} -> <<""/utf8>> end, {text_area_state, Lines, etui@text:cell_width(Last_line), gleam@int:max(0, Row)}. -file("src/etui/widgets/textarea.gleam", 133). ?DOC(" All lines joined with `\"\\n\"`.\n"). -spec value(text_area_state()) -> binary(). value(State) -> gleam@string:join(erlang:element(2, State), <<"\n"/utf8>>). -file("src/etui/widgets/textarea.gleam", 138). ?DOC(" Number of lines.\n"). -spec line_count(text_area_state()) -> integer(). line_count(State) -> erlang:length(erlang:element(2, State)). -file("src/etui/widgets/textarea.gleam", 482). -spec scroll_offset(integer(), integer()) -> integer(). scroll_offset(Cursor_y, Visible_h) -> case Visible_h =< 0 of true -> 0; false -> case Cursor_y < Visible_h of true -> 0; false -> (Cursor_y - Visible_h) + 1 end end. -file("src/etui/widgets/textarea.gleam", 145). ?DOC( " Effective scroll offset for a viewport of `visible_h` rows.\n" " Returns the index of the first visible line so the cursor stays in view.\n" " Pass as `offset` to `scrollbar.scrollbar_new`.\n" ). -spec effective_offset(text_area_state(), integer()) -> integer(). effective_offset(State, Visible_h) -> scroll_offset(erlang:element(4, State), Visible_h). -file("src/etui/widgets/textarea.gleam", 152). ?DOC( " Screen position of the hardware cursor within `area`.\n" " Returns `Error(Nil)` when the cursor column is beyond the area width\n" " (mirrors the render rule: no cursor cell is drawn off-screen).\n" ). -spec cursor_screen_pos(text_area_state(), etui@geometry:rect()) -> {ok, etui@geometry:position()} | {error, nil}. cursor_screen_pos(State, Area) -> case erlang:element(3, State) >= erlang:element(2, erlang:element(3, Area)) of true -> {error, nil}; false -> Scroll = scroll_offset( erlang:element(4, State), erlang:element(3, erlang:element(3, Area)) ), {ok, {position, erlang:element(2, erlang:element(2, Area)) + erlang:element( 3, State ), (erlang:element(3, erlang:element(2, Area)) + erlang:element( 4, State )) - Scroll}} end. -file("src/etui/widgets/textarea.gleam", 437). -spec set_line(list(binary()), integer(), binary()) -> list(binary()). set_line(Lines, Idx, New_val) -> gleam@list:index_map(Lines, fun(Line, I) -> case I =:= Idx of true -> New_val; false -> Line end end). -file("src/etui/widgets/textarea.gleam", 426). -spec get_line(list(binary()), integer()) -> binary(). get_line(Lines, Idx) -> case Idx < 0 of true -> <<""/utf8>>; false -> case gleam@list:drop(Lines, Idx) of [H | _] -> H; [] -> <<""/utf8>> end end. -file("src/etui/widgets/textarea.gleam", 172). ?DOC(" Insert a character at the current cursor position.\n"). -spec insert_char(text_area(), text_area_state(), binary()) -> text_area_state(). insert_char(W, State, Ch) -> Line = get_line(erlang:element(2, State), erlang:element(4, State)), Line_cells = etui@text:cell_width(Line), case (erlang:element(3, W) > 0) andalso (Line_cells >= erlang:element(3, W)) of true -> State; false -> Before = etui@text:truncate( Line, erlang:element(3, State), <<""/utf8>> ), After = gleam@string:drop_start(Line, string:length(Before)), New_line = <<<>/binary, After/binary>>, {text_area_state, set_line( erlang:element(2, State), erlang:element(4, State), New_line ), erlang:element(3, State) + etui@text:cell_width(Ch), erlang:element(4, State)} end. -file("src/etui/widgets/textarea.gleam", 446). -spec delete_line(list(binary()), integer()) -> list(binary()). delete_line(Lines, Idx) -> gleam@list:index_fold(Lines, [], fun(Acc, Line, I) -> case I =:= Idx of true -> Acc; false -> lists:append(Acc, [Line]) end end). -file("src/etui/widgets/textarea.gleam", 196). ?DOC( " Delete the character immediately before the cursor.\n" " If at column 0, merges the current line with the previous line.\n" ). -spec backspace(text_area_state()) -> text_area_state(). backspace(State) -> case erlang:element(3, State) > 0 of true -> Line = get_line(erlang:element(2, State), erlang:element(4, State)), Before = etui@text:truncate( Line, erlang:element(3, State) - 1, <<""/utf8>> ), Graphemes_before = string:length( etui@text:truncate(Line, erlang:element(3, State), <<""/utf8>>) ), After = gleam@string:drop_start(Line, Graphemes_before), {text_area_state, set_line( erlang:element(2, State), erlang:element(4, State), <> ), etui@text:cell_width(Before), erlang:element(4, State)}; false -> case erlang:element(4, State) > 0 of false -> State; true -> Prev = get_line( erlang:element(2, State), erlang:element(4, State) - 1 ), Curr = get_line( erlang:element(2, State), erlang:element(4, State) ), Merged = <>, New_x = etui@text:cell_width(Prev), New_lines = begin _pipe = delete_line( erlang:element(2, State), erlang:element(4, State) ), set_line(_pipe, erlang:element(4, State) - 1, Merged) end, {text_area_state, New_lines, New_x, erlang:element(4, State) - 1} end end. -file("src/etui/widgets/textarea.gleam", 463). -spec insert_line_after_loop( list(binary()), integer(), binary(), integer(), list(binary()) ) -> list(binary()). insert_line_after_loop(Lines, Idx, New_line, I, Acc) -> case Lines of [] -> lists:reverse(Acc); [H | Rest] -> Acc2 = case I =:= Idx of true -> [New_line, H | Acc]; false -> [H | Acc] end, insert_line_after_loop(Rest, Idx, New_line, I + 1, Acc2) end. -file("src/etui/widgets/textarea.gleam", 455). -spec insert_line_after(list(binary()), integer(), binary()) -> list(binary()). insert_line_after(Lines, Idx, New_line) -> insert_line_after_loop(Lines, Idx, New_line, 0, []). -file("src/etui/widgets/textarea.gleam", 232). ?DOC(" Insert a newline at the cursor. Splits the current line.\n"). -spec newline(text_area(), text_area_state()) -> text_area_state(). newline(W, State) -> N_lines = erlang:length(erlang:element(2, State)), case (erlang:element(2, W) > 0) andalso (N_lines >= erlang:element(2, W)) of true -> State; false -> Line = get_line(erlang:element(2, State), erlang:element(4, State)), Before = etui@text:truncate( Line, erlang:element(3, State), <<""/utf8>> ), After = gleam@string:drop_start(Line, string:length(Before)), New_lines = begin _pipe = set_line( erlang:element(2, State), erlang:element(4, State), Before ), insert_line_after(_pipe, erlang:element(4, State), After) end, {text_area_state, New_lines, 0, erlang:element(4, State) + 1} end. -file("src/etui/widgets/textarea.gleam", 252). ?DOC(" Move cursor one cell left. Wraps to end of previous line.\n"). -spec move_cursor_left(text_area_state()) -> text_area_state(). move_cursor_left(State) -> case erlang:element(3, State) > 0 of true -> Line = get_line(erlang:element(2, State), erlang:element(4, State)), New_x = etui@text:cell_width( etui@text:truncate( Line, erlang:element(3, State) - 1, <<""/utf8>> ) ), {text_area_state, erlang:element(2, State), New_x, erlang:element(4, State)}; false -> case erlang:element(4, State) > 0 of false -> State; true -> Prev = get_line( erlang:element(2, State), erlang:element(4, State) - 1 ), {text_area_state, erlang:element(2, State), etui@text:cell_width(Prev), erlang:element(4, State) - 1} end end. -file("src/etui/widgets/textarea.gleam", 500). -spec grapheme_width_at(binary(), integer()) -> integer(). grapheme_width_at(S, Cell_pos) -> Prefix = etui@text:truncate(S, Cell_pos, <<""/utf8>>), Rest = gleam@string:drop_start(S, string:length(Prefix)), case gleam@string:to_graphemes(Rest) of [G | _] -> etui@text:cell_width(G); [] -> 1 end. -file("src/etui/widgets/textarea.gleam", 275). ?DOC(" Move cursor one cell right. Wraps to start of next line.\n"). -spec move_cursor_right(text_area_state()) -> text_area_state(). move_cursor_right(State) -> Line = get_line(erlang:element(2, State), erlang:element(4, State)), case erlang:element(3, State) < etui@text:cell_width(Line) of true -> Step = grapheme_width_at(Line, erlang:element(3, State)), {text_area_state, erlang:element(2, State), erlang:element(3, State) + Step, erlang:element(4, State)}; false -> N_lines = erlang:length(erlang:element(2, State)), case erlang:element(4, State) < (N_lines - 1) of false -> State; true -> {text_area_state, erlang:element(2, State), 0, erlang:element(4, State) + 1} end end. -file("src/etui/widgets/textarea.gleam", 495). -spec snap_to_boundary(binary(), integer()) -> integer(). snap_to_boundary(S, Cell_pos) -> Clamped = gleam@int:min(Cell_pos, etui@text:cell_width(S)), etui@text:cell_width(etui@text:truncate(S, Clamped, <<""/utf8>>)). -file("src/etui/widgets/textarea.gleam", 294). ?DOC(" Move cursor up one line, clamping x to the new line's width.\n"). -spec move_cursor_up(text_area_state()) -> text_area_state(). move_cursor_up(State) -> case erlang:element(4, State) > 0 of false -> State; true -> New_y = erlang:element(4, State) - 1, Prev = get_line(erlang:element(2, State), New_y), {text_area_state, erlang:element(2, State), snap_to_boundary(Prev, erlang:element(3, State)), New_y} end. -file("src/etui/widgets/textarea.gleam", 310). ?DOC(" Move cursor down one line, clamping x to the new line's width.\n"). -spec move_cursor_down(text_area_state()) -> text_area_state(). move_cursor_down(State) -> N_lines = erlang:length(erlang:element(2, State)), case erlang:element(4, State) < (N_lines - 1) of false -> State; true -> New_y = erlang:element(4, State) + 1, Next = get_line(erlang:element(2, State), New_y), {text_area_state, erlang:element(2, State), snap_to_boundary(Next, erlang:element(3, State)), New_y} end. -file("src/etui/widgets/textarea.gleam", 327). ?DOC(" Move cursor to beginning of current line.\n"). -spec move_to_line_start(text_area_state()) -> text_area_state(). move_to_line_start(State) -> {text_area_state, erlang:element(2, State), 0, erlang:element(4, State)}. -file("src/etui/widgets/textarea.gleam", 332). ?DOC(" Move cursor to end of current line.\n"). -spec move_to_line_end(text_area_state()) -> text_area_state(). move_to_line_end(State) -> Line = get_line(erlang:element(2, State), erlang:element(4, State)), {text_area_state, erlang:element(2, State), etui@text:cell_width(Line), erlang:element(4, State)}. -file("src/etui/widgets/textarea.gleam", 338). ?DOC(" Delete from cursor to end of current line.\n"). -spec delete_to_line_end(text_area_state()) -> text_area_state(). delete_to_line_end(State) -> Line = get_line(erlang:element(2, State), erlang:element(4, State)), Before = etui@text:truncate(Line, erlang:element(3, State), <<""/utf8>>), {text_area_state, set_line(erlang:element(2, State), erlang:element(4, State), Before), erlang:element(3, State), erlang:element(4, State)}. -file("src/etui/widgets/textarea.gleam", 509). -spec grapheme_at_cell(binary(), integer()) -> binary(). grapheme_at_cell(S, Cell_pos) -> Prefix = etui@text:truncate(S, Cell_pos, <<""/utf8>>), Rest = gleam@string:drop_start(S, string:length(Prefix)), case gleam@string:to_graphemes(Rest) of [G | _] -> G; [] -> <<" "/utf8>> end. -file("src/etui/widgets/textarea.gleam", 386). -spec render_line( etui@buffer:buffer(), etui@geometry:rect(), text_area(), text_area_state(), binary(), integer(), boolean() ) -> etui@buffer:buffer(). render_line(Buf, Area, W, State, Line, Y, Is_cursor_row) -> Width = erlang:element(2, erlang:element(3, Area)), Truncated = etui@text:truncate(Line, Width, <<""/utf8>>), Padded = etui@text:pad_right(Truncated, Width), Buf2 = etui@buffer:set_string( Buf, {position, erlang:element(2, erlang:element(2, Area)), Y}, Padded, erlang:element(4, W), erlang:element(5, W), etui@style:none() ), case Is_cursor_row andalso (erlang:element(3, State) < Width) of false -> Buf2; true -> Cursor_ch = grapheme_at_cell(Line, erlang:element(3, State)), etui@buffer:set_string( Buf2, {position, erlang:element(2, erlang:element(2, Area)) + erlang:element( 3, State ), Y}, Cursor_ch, erlang:element(2, erlang:element(6, W)), erlang:element(3, erlang:element(6, W)), erlang:element(4, erlang:element(6, W)) ) end. -file("src/etui/widgets/textarea.gleam", 365). -spec render_lines( etui@buffer:buffer(), etui@geometry:rect(), text_area(), text_area_state(), integer(), integer() ) -> etui@buffer:buffer(). render_lines(Buf, Area, W, State, Scroll, Row_offset) -> case Row_offset >= erlang:element(3, erlang:element(3, Area)) of true -> Buf; false -> Line_idx = Scroll + Row_offset, Line = get_line(erlang:element(2, State), Line_idx), Y = erlang:element(3, erlang:element(2, Area)) + Row_offset, Is_cursor_row = Line_idx =:= erlang:element(4, State), Buf2 = render_line(Buf, Area, W, State, Line, Y, Is_cursor_row), render_lines(Buf2, Area, W, State, Scroll, Row_offset + 1) end. -file("src/etui/widgets/textarea.gleam", 349). ?DOC( " Render the textarea. Scrolls vertically so the cursor line is visible.\n" " Highlights the cursor cell with `cursor_style`.\n" ). -spec render( etui@buffer:buffer(), etui@geometry:rect(), text_area(), text_area_state() ) -> etui@buffer:buffer(). render(Buf, Area, W, State) -> case (erlang:element(3, erlang:element(3, Area)) =< 0) orelse (erlang:element( 2, erlang:element(3, Area) ) =< 0) of true -> Buf; false -> Visible_h = erlang:element(3, erlang:element(3, Area)), Scroll = scroll_offset(erlang:element(4, State), Visible_h), render_lines(Buf, Area, W, State, Scroll, 0) end.