Skip to main content

src/etui@widgets@input.erl

-module(etui@widgets@input).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/etui/widgets/input.gleam").
-export([input_new/1, with_prompt/2, with_password/2, with_mask/2, with_validator/2, with_error_color/2, validate/2, with_max_length/2, with_colors/3, with_style/2, state_new/0, state_from_string/1, insert_char/3, backspace/1, move_cursor_left/1, move_cursor_right/1, move_to_start/1, move_to_end/1, delete_to_end/1, clear_state/1, render/4]).
-export_type([input_widget/0, input_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 input_widget() :: {input_widget,
        integer(),
        binary(),
        etui@style:color(),
        etui@style:color(),
        fun((binary()) -> {ok, nil} | {error, binary()}),
        etui@style:color(),
        binary(),
        boolean(),
        binary()}.

-type input_state() :: {input_state, binary(), integer()}.

-file("src/etui/widgets/input.gleam", 54).
?DOC(" New input widget with placeholder text. Default max length: 256 cells.\n").
-spec input_new(binary()) -> input_widget().
input_new(Placeholder) ->
    {input_widget,
        256,
        Placeholder,
        default,
        default,
        fun(_) -> {ok, nil} end,
        {indexed, 1},
        <<""/utf8>>,
        false,
        <<"*"/utf8>>}.

-file("src/etui/widgets/input.gleam", 69).
?DOC(" Set a prefix shown before the value (e.g. `\"> \"`).\n").
-spec with_prompt(input_widget(), binary()) -> input_widget().
with_prompt(I, Prompt) ->
    {input_widget,
        erlang:element(2, I),
        erlang:element(3, I),
        erlang:element(4, I),
        erlang:element(5, I),
        erlang:element(6, I),
        erlang:element(7, I),
        Prompt,
        erlang:element(9, I),
        erlang:element(10, I)}.

-file("src/etui/widgets/input.gleam", 74).
?DOC(" Render each value cell as the mask character. Use for password fields.\n").
-spec with_password(input_widget(), boolean()) -> input_widget().
with_password(I, Password) ->
    {input_widget,
        erlang:element(2, I),
        erlang:element(3, I),
        erlang:element(4, I),
        erlang:element(5, I),
        erlang:element(6, I),
        erlang:element(7, I),
        erlang:element(8, I),
        Password,
        erlang:element(10, I)}.

-file("src/etui/widgets/input.gleam", 79).
?DOC(" Mask character used when `password` is True. Default `\"*\"`.\n").
-spec with_mask(input_widget(), binary()) -> input_widget().
with_mask(I, Mask) ->
    {input_widget,
        erlang:element(2, I),
        erlang:element(3, I),
        erlang:element(4, I),
        erlang:element(5, I),
        erlang:element(6, I),
        erlang:element(7, I),
        erlang:element(8, I),
        erlang:element(9, I),
        Mask}.

-file("src/etui/widgets/input.gleam", 84).
?DOC(" Set a validation function. Call `validate/2` to run it.\n").
-spec with_validator(
    input_widget(),
    fun((binary()) -> {ok, nil} | {error, binary()})
) -> input_widget().
with_validator(I, V) ->
    {input_widget,
        erlang:element(2, I),
        erlang:element(3, I),
        erlang:element(4, I),
        erlang:element(5, I),
        V,
        erlang:element(7, I),
        erlang:element(8, I),
        erlang:element(9, I),
        erlang:element(10, I)}.

-file("src/etui/widgets/input.gleam", 89).
?DOC(" Set the color used to display validation errors.\n").
-spec with_error_color(input_widget(), etui@style:color()) -> input_widget().
with_error_color(I, Fg) ->
    {input_widget,
        erlang:element(2, I),
        erlang:element(3, I),
        erlang:element(4, I),
        erlang:element(5, I),
        erlang:element(6, I),
        Fg,
        erlang:element(8, I),
        erlang:element(9, I),
        erlang:element(10, I)}.

-file("src/etui/widgets/input.gleam", 94).
?DOC(" Run the validator on `value`. Returns Ok(Nil) or Error(message).\n").
-spec validate(input_widget(), binary()) -> {ok, nil} | {error, binary()}.
validate(I, Value) ->
    (erlang:element(6, I))(Value).

-file("src/etui/widgets/input.gleam", 99).
?DOC(" Maximum value width in cells (wide characters count as 2).\n").
-spec with_max_length(input_widget(), integer()) -> input_widget().
with_max_length(I, Len) ->
    {input_widget,
        Len,
        erlang:element(3, I),
        erlang:element(4, I),
        erlang:element(5, I),
        erlang:element(6, I),
        erlang:element(7, I),
        erlang:element(8, I),
        erlang:element(9, I),
        erlang:element(10, I)}.

-file("src/etui/widgets/input.gleam", 103).
-spec with_colors(input_widget(), etui@style:color(), etui@style:color()) -> input_widget().
with_colors(I, Fg, Bg) ->
    {input_widget,
        erlang:element(2, I),
        erlang:element(3, I),
        Fg,
        Bg,
        erlang:element(6, I),
        erlang:element(7, I),
        erlang:element(8, I),
        erlang:element(9, I),
        erlang:element(10, I)}.

-file("src/etui/widgets/input.gleam", 111).
-spec with_style(input_widget(), etui@style:style()) -> input_widget().
with_style(I, S) ->
    {input_widget,
        erlang:element(2, I),
        erlang:element(3, I),
        erlang:element(2, S),
        erlang:element(3, S),
        erlang:element(6, I),
        erlang:element(7, I),
        erlang:element(8, I),
        erlang:element(9, I),
        erlang:element(10, I)}.

-file("src/etui/widgets/input.gleam", 119).
?DOC(" Initial state: empty value, cursor at 0.\n").
-spec state_new() -> input_state().
state_new() ->
    {input_state, <<""/utf8>>, 0}.

-file("src/etui/widgets/input.gleam", 124).
?DOC(" State pre-populated with a string; cursor placed at the end.\n").
-spec state_from_string(binary()) -> input_state().
state_from_string(S) ->
    {input_state, S, etui@text:cell_width(S)}.

-file("src/etui/widgets/input.gleam", 132).
?DOC(" Insert character at cursor. Respects widget max_length.\n").
-spec insert_char(input_widget(), input_state(), binary()) -> input_state().
insert_char(Widget, State, Ch) ->
    case etui@text:cell_width(erlang:element(2, State)) >= erlang:element(
        2,
        Widget
    ) of
        true ->
            State;

        false ->
            Before = etui@text:truncate(
                erlang:element(2, State),
                erlang:element(3, State),
                <<""/utf8>>
            ),
            After = gleam@string:drop_start(
                erlang:element(2, State),
                string:length(Before)
            ),
            {input_state,
                <<<<Before/binary, Ch/binary>>/binary, After/binary>>,
                erlang:element(3, State) + etui@text:cell_width(Ch)}
    end.

-file("src/etui/widgets/input.gleam", 151).
?DOC(" Delete the character immediately before the cursor (backspace semantics).\n").
-spec backspace(input_state()) -> input_state().
backspace(State) ->
    case erlang:element(3, State) =< 0 of
        true ->
            State;

        false ->
            Before = etui@text:truncate(
                erlang:element(2, State),
                erlang:element(3, State) - 1,
                <<""/utf8>>
            ),
            Graphemes_at_cursor = string:length(
                etui@text:truncate(
                    erlang:element(2, State),
                    erlang:element(3, State),
                    <<""/utf8>>
                )
            ),
            After = gleam@string:drop_start(
                erlang:element(2, State),
                Graphemes_at_cursor
            ),
            {input_state,
                <<Before/binary, After/binary>>,
                etui@text:cell_width(Before)}
    end.

-file("src/etui/widgets/input.gleam", 165).
?DOC(" Move cursor one cell left, clamped to 0.\n").
-spec move_cursor_left(input_state()) -> input_state().
move_cursor_left(State) ->
    case erlang:element(3, State) =< 0 of
        true ->
            State;

        false ->
            New_cursor = etui@text:cell_width(
                etui@text:truncate(
                    erlang:element(2, State),
                    erlang:element(3, State) - 1,
                    <<""/utf8>>
                )
            ),
            {input_state, erlang:element(2, State), New_cursor}
    end.

-file("src/etui/widgets/input.gleam", 261).
-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/input.gleam", 177).
?DOC(" Move cursor one cell right, clamped to end of value.\n").
-spec move_cursor_right(input_state()) -> input_state().
move_cursor_right(State) ->
    case erlang:element(3, State) >= etui@text:cell_width(
        erlang:element(2, State)
    ) of
        true ->
            State;

        false ->
            Step = grapheme_width_at(
                erlang:element(2, State),
                erlang:element(3, State)
            ),
            {input_state,
                erlang:element(2, State),
                erlang:element(3, State) + Step}
    end.

-file("src/etui/widgets/input.gleam", 188).
?DOC(" Move cursor to beginning of value.\n").
-spec move_to_start(input_state()) -> input_state().
move_to_start(State) ->
    {input_state, erlang:element(2, State), 0}.

-file("src/etui/widgets/input.gleam", 193).
?DOC(" Move cursor to end of value.\n").
-spec move_to_end(input_state()) -> input_state().
move_to_end(State) ->
    {input_state,
        erlang:element(2, State),
        etui@text:cell_width(erlang:element(2, State))}.

-file("src/etui/widgets/input.gleam", 198).
?DOC(" Delete from cursor to end of value.\n").
-spec delete_to_end(input_state()) -> input_state().
delete_to_end(State) ->
    Before = etui@text:truncate(
        erlang:element(2, State),
        erlang:element(3, State),
        <<""/utf8>>
    ),
    {input_state, Before, erlang:element(3, State)}.

-file("src/etui/widgets/input.gleam", 204).
?DOC(" Reset value and cursor to empty.\n").
-spec clear_state(input_state()) -> input_state().
clear_state(_) ->
    {input_state, <<""/utf8>>, 0}.

-file("src/etui/widgets/input.gleam", 214).
?DOC(
    " Render the input field. Shows `state.value` (bold) or placeholder when empty.\n"
    " Text is truncated to fit `area.size.width` (minus one cell reserved for the\n"
    " trailing cursor). When `password` is True the value is masked.\n"
).
-spec render(
    etui@buffer:buffer(),
    etui@geometry:rect(),
    input_widget(),
    input_state()
) -> etui@buffer:buffer().
render(Buf, Area, Widget, State) ->
    case erlang:element(2, erlang:element(3, Area)) =< 0 of
        true ->
            Buf;

        false ->
            Has_value = erlang:element(2, State) /= <<""/utf8>>,
            Value_display = case erlang:element(9, Widget) andalso Has_value of
                true ->
                    gleam@string:repeat(
                        erlang:element(10, Widget),
                        etui@text:cell_width(erlang:element(2, State))
                    );

                false ->
                    erlang:element(2, State)
            end,
            Display_text = case Has_value of
                true ->
                    <<(erlang:element(8, Widget))/binary, Value_display/binary>>;

                false ->
                    <<(erlang:element(8, Widget))/binary,
                        (erlang:element(3, Widget))/binary>>
            end,
            C = etui@text:cell_width(erlang:element(8, Widget)) + erlang:element(
                3,
                State
            ),
            W_avail = gleam@int:max(
                1,
                erlang:element(2, erlang:element(3, Area))
            ),
            View_start = case C >= W_avail of
                true ->
                    (C - W_avail) + 1;

                false ->
                    0
            end,
            Prefix = etui@text:truncate(Display_text, View_start, <<""/utf8>>),
            Suffix = gleam@string:drop_start(
                Display_text,
                string:length(Prefix)
            ),
            Truncated = etui@text:truncate(Suffix, W_avail, <<""/utf8>>),
            Padded = etui@text:pad_right(Truncated, W_avail),
            Modifier = case Has_value of
                true ->
                    etui@style:bold();

                false ->
                    etui@style:none()
            end,
            etui@buffer:set_string(
                Buf,
                erlang:element(2, Area),
                Padded,
                erlang:element(4, Widget),
                erlang:element(5, Widget),
                Modifier
            )
    end.