Skip to main content

src/etui@widgets@form.erl

-module(etui@widgets@form).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/etui/widgets/form.gleam").
-export([form_new/0, add_field/5, add_required/4, add_optional/4, with_field_max_length/2, with_label_width/2, with_colors/3, with_focused_colors/3, with_error_color/2, focus_next/1, focus_prev/1, focus_index/2, type_char/2, backspace/1, clear_focused/1, set_value/3, validate/1, is_valid/1, submit/1, is_submitted/1, reset/1, get_value/2, values/1, render/3]).
-export_type([field/1, form/1]).

-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 field(FJR) :: {field,
        FJR,
        binary(),
        binary(),
        fun((binary()) -> {ok, nil} | {error, binary()}),
        binary(),
        integer()}.

-type form(FJS) :: {form,
        list(field(FJS)),
        integer(),
        boolean(),
        integer(),
        etui@style:color(),
        etui@style:color(),
        etui@style:color(),
        etui@style:color(),
        etui@style:color()}.

-file("src/etui/widgets/form.gleam", 80).
?DOC(" Empty form with default styles.\n").
-spec form_new() -> form(any()).
form_new() ->
    {form,
        [],
        0,
        false,
        0,
        default,
        default,
        default,
        {indexed, 4},
        {indexed, 1}}.

-file("src/etui/widgets/form.gleam", 95).
?DOC(" Append a field with a validator.\n").
-spec add_field(
    form(FJX),
    FJX,
    binary(),
    binary(),
    fun((binary()) -> {ok, nil} | {error, binary()})
) -> form(FJX).
add_field(F, Id, Label, Default_value, Validator) ->
    Field = {field, Id, Label, Default_value, Validator, <<""/utf8>>, 0},
    {form,
        lists:append(erlang:element(2, F), [Field]),
        erlang:element(3, F),
        erlang:element(4, F),
        erlang:element(5, F),
        erlang:element(6, F),
        erlang:element(7, F),
        erlang:element(8, F),
        erlang:element(9, F),
        erlang:element(10, F)}.

-file("src/etui/widgets/form.gleam", 115).
?DOC(" Append a required text field (non-empty validator).\n").
-spec add_required(form(FKA), FKA, binary(), binary()) -> form(FKA).
add_required(F, Id, Label, Default_value) ->
    add_field(F, Id, Label, Default_value, fun(V) -> case V of
                <<""/utf8>> ->
                    {error, <<"required"/utf8>>};

                _ ->
                    {ok, nil}
            end end).

-file("src/etui/widgets/form.gleam", 130).
?DOC(" Append an optional field (always valid).\n").
-spec add_optional(form(FKD), FKD, binary(), binary()) -> form(FKD).
add_optional(F, Id, Label, Default_value) ->
    add_field(F, Id, Label, Default_value, fun(_) -> {ok, nil} end).

-file("src/etui/widgets/form.gleam", 140).
?DOC(" Set max grapheme length for the most-recently added field.\n").
-spec with_field_max_length(form(FKG), integer()) -> form(FKG).
with_field_max_length(F, Max) ->
    Fields = gleam@list:index_map(
        erlang:element(2, F),
        fun(Field, I) -> case I =:= (erlang:length(erlang:element(2, F)) - 1) of
                true ->
                    {field,
                        erlang:element(2, Field),
                        erlang:element(3, Field),
                        erlang:element(4, Field),
                        erlang:element(5, Field),
                        erlang:element(6, Field),
                        Max};

                false ->
                    Field
            end end
    ),
    {form,
        Fields,
        erlang:element(3, F),
        erlang:element(4, F),
        erlang:element(5, F),
        erlang:element(6, F),
        erlang:element(7, F),
        erlang:element(8, F),
        erlang:element(9, F),
        erlang:element(10, F)}.

-file("src/etui/widgets/form.gleam", 152).
?DOC(" Override label column width (auto-computed from labels if 0).\n").
-spec with_label_width(form(FKJ), integer()) -> form(FKJ).
with_label_width(F, W) ->
    {form,
        erlang:element(2, F),
        erlang:element(3, F),
        erlang:element(4, F),
        W,
        erlang:element(6, F),
        erlang:element(7, F),
        erlang:element(8, F),
        erlang:element(9, F),
        erlang:element(10, F)}.

-file("src/etui/widgets/form.gleam", 157).
?DOC(" Set base foreground/background.\n").
-spec with_colors(form(FKM), etui@style:color(), etui@style:color()) -> form(FKM).
with_colors(F, Fg, Bg) ->
    {form,
        erlang:element(2, F),
        erlang:element(3, F),
        erlang:element(4, F),
        erlang:element(5, F),
        Fg,
        Bg,
        erlang:element(8, F),
        erlang:element(9, F),
        erlang:element(10, F)}.

-file("src/etui/widgets/form.gleam", 162).
?DOC(" Set focused field highlight colors.\n").
-spec with_focused_colors(form(FKP), etui@style:color(), etui@style:color()) -> form(FKP).
with_focused_colors(F, Fg, Bg) ->
    {form,
        erlang:element(2, F),
        erlang:element(3, F),
        erlang:element(4, F),
        erlang:element(5, F),
        erlang:element(6, F),
        erlang:element(7, F),
        Fg,
        Bg,
        erlang:element(10, F)}.

-file("src/etui/widgets/form.gleam", 171).
?DOC(" Set validation error text color.\n").
-spec with_error_color(form(FKS), etui@style:color()) -> form(FKS).
with_error_color(F, Fg) ->
    {form,
        erlang:element(2, F),
        erlang:element(3, F),
        erlang:element(4, F),
        erlang:element(5, F),
        erlang:element(6, F),
        erlang:element(7, F),
        erlang:element(8, F),
        erlang:element(9, F),
        Fg}.

-file("src/etui/widgets/form.gleam", 179).
?DOC(" Move focus to the next field (wraps around).\n").
-spec focus_next(form(FKV)) -> form(FKV).
focus_next(F) ->
    N = erlang:length(erlang:element(2, F)),
    case N of
        0 ->
            F;

        _ ->
            {form, erlang:element(2, F), case N of
                    0 -> 0;
                    Gleam@denominator -> (erlang:element(3, F) + 1) rem Gleam@denominator
                end, erlang:element(4, F), erlang:element(5, F), erlang:element(
                    6,
                    F
                ), erlang:element(7, F), erlang:element(8, F), erlang:element(
                    9,
                    F
                ), erlang:element(10, F)}
    end.

-file("src/etui/widgets/form.gleam", 188).
?DOC(" Move focus to the previous field (wraps around).\n").
-spec focus_prev(form(FKY)) -> form(FKY).
focus_prev(F) ->
    N = erlang:length(erlang:element(2, F)),
    case N of
        0 ->
            F;

        _ ->
            {form,
                erlang:element(2, F),
                begin
                    Prev = erlang:element(3, F) - 1,
                    case Prev < 0 of
                        true ->
                            N - 1;

                        false ->
                            Prev
                    end
                end,
                erlang:element(4, F),
                erlang:element(5, F),
                erlang:element(6, F),
                erlang:element(7, F),
                erlang:element(8, F),
                erlang:element(9, F),
                erlang:element(10, F)}
    end.

-file("src/etui/widgets/form.gleam", 204).
?DOC(" Move focus to a specific field by index.\n").
-spec focus_index(form(FLB), integer()) -> form(FLB).
focus_index(F, Idx) ->
    N = erlang:length(erlang:element(2, F)),
    case (Idx >= 0) andalso (Idx < N) of
        true ->
            {form,
                erlang:element(2, F),
                Idx,
                erlang:element(4, F),
                erlang:element(5, F),
                erlang:element(6, F),
                erlang:element(7, F),
                erlang:element(8, F),
                erlang:element(9, F),
                erlang:element(10, F)};

        false ->
            F
    end.

-file("src/etui/widgets/form.gleam", 443).
-spec update_focused(form(FMR), fun((field(FMR)) -> field(FMR))) -> form(FMR).
update_focused(F, Updater) ->
    Fields = gleam@list:index_map(
        erlang:element(2, F),
        fun(Field, I) -> case I =:= erlang:element(3, F) of
                true ->
                    Updater(Field);

                false ->
                    Field
            end end
    ),
    {form,
        Fields,
        erlang:element(3, F),
        erlang:element(4, F),
        erlang:element(5, F),
        erlang:element(6, F),
        erlang:element(7, F),
        erlang:element(8, F),
        erlang:element(9, F),
        erlang:element(10, F)}.

-file("src/etui/widgets/form.gleam", 216).
?DOC(" Type a character into the currently focused field.\n").
-spec type_char(form(FLE), binary()) -> form(FLE).
type_char(F, Ch) ->
    update_focused(
        F,
        fun(Field) ->
            case (erlang:element(7, Field) > 0) andalso (etui@text:cell_width(
                erlang:element(4, Field)
            )
            >= erlang:element(7, Field)) of
                true ->
                    Field;

                false ->
                    {field,
                        erlang:element(2, Field),
                        erlang:element(3, Field),
                        <<(erlang:element(4, Field))/binary, Ch/binary>>,
                        erlang:element(5, Field),
                        <<""/utf8>>,
                        erlang:element(7, Field)}
            end
        end
    ).

-file("src/etui/widgets/form.gleam", 228).
?DOC(" Backspace on the currently focused field.\n").
-spec backspace(form(FLH)) -> form(FLH).
backspace(F) ->
    update_focused(F, fun(Field) -> case erlang:element(4, Field) of
                <<""/utf8>> ->
                    Field;

                V ->
                    Graphemes = etui@text:graphemes(V),
                    Dropped = gleam@list:take(
                        Graphemes,
                        erlang:length(Graphemes) - 1
                    ),
                    {field,
                        erlang:element(2, Field),
                        erlang:element(3, Field),
                        erlang:list_to_binary(Dropped),
                        erlang:element(5, Field),
                        <<""/utf8>>,
                        erlang:element(7, Field)}
            end end).

-file("src/etui/widgets/form.gleam", 242).
?DOC(" Clear the currently focused field's value.\n").
-spec clear_focused(form(FLK)) -> form(FLK).
clear_focused(F) ->
    update_focused(
        F,
        fun(Field) ->
            {field,
                erlang:element(2, Field),
                erlang:element(3, Field),
                <<""/utf8>>,
                erlang:element(5, Field),
                <<""/utf8>>,
                erlang:element(7, Field)}
        end
    ).

-file("src/etui/widgets/form.gleam", 247).
?DOC(" Set a field's value by id.\n").
-spec set_value(form(FLN), FLN, binary()) -> form(FLN).
set_value(F, Id, Value) ->
    Fields = gleam@list:map(
        erlang:element(2, F),
        fun(Field) -> case erlang:element(2, Field) =:= Id of
                true ->
                    {field,
                        erlang:element(2, Field),
                        erlang:element(3, Field),
                        Value,
                        erlang:element(5, Field),
                        <<""/utf8>>,
                        erlang:element(7, Field)};

                false ->
                    Field
            end end
    ),
    {form,
        Fields,
        erlang:element(3, F),
        erlang:element(4, F),
        erlang:element(5, F),
        erlang:element(6, F),
        erlang:element(7, F),
        erlang:element(8, F),
        erlang:element(9, F),
        erlang:element(10, F)}.

-file("src/etui/widgets/form.gleam", 262).
?DOC(" Validate all fields. Returns form with error messages populated.\n").
-spec validate(form(FLQ)) -> form(FLQ).
validate(F) ->
    Fields = gleam@list:map(
        erlang:element(2, F),
        fun(Field) ->
            case (erlang:element(5, Field))(erlang:element(4, Field)) of
                {ok, _} ->
                    {field,
                        erlang:element(2, Field),
                        erlang:element(3, Field),
                        erlang:element(4, Field),
                        erlang:element(5, Field),
                        <<""/utf8>>,
                        erlang:element(7, Field)};

                {error, Msg} ->
                    {field,
                        erlang:element(2, Field),
                        erlang:element(3, Field),
                        erlang:element(4, Field),
                        erlang:element(5, Field),
                        Msg,
                        erlang:element(7, Field)}
            end
        end
    ),
    {form,
        Fields,
        erlang:element(3, F),
        erlang:element(4, F),
        erlang:element(5, F),
        erlang:element(6, F),
        erlang:element(7, F),
        erlang:element(8, F),
        erlang:element(9, F),
        erlang:element(10, F)}.

-file("src/etui/widgets/form.gleam", 274).
?DOC(" True if all fields are valid (no errors after validation).\n").
-spec is_valid(form(any())) -> boolean().
is_valid(F) ->
    gleam@list:all(
        erlang:element(2, F),
        fun(Field) ->
            case (erlang:element(5, Field))(erlang:element(4, Field)) of
                {ok, _} ->
                    true;

                {error, _} ->
                    false
            end
        end
    ).

-file("src/etui/widgets/form.gleam", 284).
?DOC(" Validate then mark as submitted if valid. Returns the form.\n").
-spec submit(form(FLV)) -> form(FLV).
submit(F) ->
    Validated = validate(F),
    case is_valid(Validated) of
        true ->
            {form,
                erlang:element(2, Validated),
                erlang:element(3, Validated),
                true,
                erlang:element(5, Validated),
                erlang:element(6, Validated),
                erlang:element(7, Validated),
                erlang:element(8, Validated),
                erlang:element(9, Validated),
                erlang:element(10, Validated)};

        false ->
            Validated
    end.

-file("src/etui/widgets/form.gleam", 293).
?DOC(" True if the form was successfully submitted.\n").
-spec is_submitted(form(any())) -> boolean().
is_submitted(F) ->
    erlang:element(4, F).

-file("src/etui/widgets/form.gleam", 298).
?DOC(" Reset all fields to empty, clear errors and submitted flag.\n").
-spec reset(form(FMA)) -> form(FMA).
reset(F) ->
    Fields = gleam@list:map(
        erlang:element(2, F),
        fun(Field) ->
            {field,
                erlang:element(2, Field),
                erlang:element(3, Field),
                <<""/utf8>>,
                erlang:element(5, Field),
                <<""/utf8>>,
                erlang:element(7, Field)}
        end
    ),
    {form,
        Fields,
        0,
        false,
        erlang:element(5, F),
        erlang:element(6, F),
        erlang:element(7, F),
        erlang:element(8, F),
        erlang:element(9, F),
        erlang:element(10, F)}.

-file("src/etui/widgets/form.gleam", 305).
?DOC(" Get a field's current value by id. Returns \"\" if not found.\n").
-spec get_value(form(FMD), FMD) -> binary().
get_value(F, Id) ->
    case gleam@list:find(
        erlang:element(2, F),
        fun(Field) -> erlang:element(2, Field) =:= Id end
    ) of
        {ok, Field@1} ->
            erlang:element(4, Field@1);

        {error, _} ->
            <<""/utf8>>
    end.

-file("src/etui/widgets/form.gleam", 313).
?DOC(" Get all field values as `#(id, value)` pairs.\n").
-spec values(form(FMF)) -> list({FMF, binary()}).
values(F) ->
    gleam@list:map(
        erlang:element(2, F),
        fun(Field) -> {erlang:element(2, Field), erlang:element(4, Field)} end
    ).

-file("src/etui/widgets/form.gleam", 365).
-spec render_field_row(
    etui@buffer:buffer(),
    etui@geometry:rect(),
    field(FMO),
    form(FMO),
    integer(),
    integer(),
    integer()
) -> etui@buffer:buffer().
render_field_row(Buf, Area, Field, F, Lw, Field_idx, Y) ->
    Is_focused = Field_idx =:= erlang:element(3, F),
    Label_text = <<(etui@text:pad_right(
            etui@text:truncate(erlang:element(3, Field), Lw, <<""/utf8>>),
            Lw
        ))/binary,
        " "/utf8>>,
    Value_x = (erlang:element(2, erlang:element(2, Area)) + Lw) + 1,
    Value_w = (erlang:element(2, erlang:element(3, Area)) - Lw) - 1,
    Value_w@1 = case Value_w < 0 of
        true ->
            0;

        false ->
            Value_w
    end,
    {Val_fg, Val_bg} = case Is_focused of
        true ->
            {erlang:element(8, F), erlang:element(9, F)};

        false ->
            {erlang:element(6, F), erlang:element(7, F)}
    end,
    Label_modifier = case Is_focused of
        true ->
            etui@style:bold();

        false ->
            etui@style:none()
    end,
    Buf1 = case Lw > 0 of
        false ->
            Buf;

        true ->
            etui@buffer:set_string(
                Buf,
                {position, erlang:element(2, erlang:element(2, Area)), Y},
                Label_text,
                erlang:element(6, F),
                erlang:element(7, F),
                Label_modifier
            )
    end,
    Padded_value = etui@text:pad_right(
        etui@text:truncate(erlang:element(4, Field), Value_w@1, <<""/utf8>>),
        Value_w@1
    ),
    Buf2 = case Value_w@1 > 0 of
        false ->
            Buf1;

        true ->
            etui@buffer:set_string(
                Buf1,
                {position, Value_x, Y},
                Padded_value,
                Val_fg,
                Val_bg,
                etui@style:none()
            )
    end,
    Error_y = Y + 1,
    case ((erlang:element(6, Field) /= <<""/utf8>>) andalso (Error_y < (erlang:element(
        3,
        erlang:element(2, Area)
    )
    + erlang:element(3, erlang:element(3, Area)))))
    andalso (Value_w@1 > 0) of
        false ->
            Buf2;

        true ->
            etui@buffer:set_string(
                Buf2,
                {position, Value_x, Error_y},
                etui@text:truncate(
                    <<"  "/utf8, (erlang:element(6, Field))/binary>>,
                    Value_w@1,
                    <<""/utf8>>
                ),
                erlang:element(10, F),
                erlang:element(7, F),
                etui@style:none()
            )
    end.

-file("src/etui/widgets/form.gleam", 341).
-spec render_fields(
    etui@buffer:buffer(),
    etui@geometry:rect(),
    list(field(FMK)),
    form(FMK),
    integer(),
    integer(),
    integer()
) -> etui@buffer:buffer().
render_fields(Buf, Area, Fields, F, Lw, Field_idx, Row) ->
    Row_height = 2,
    case Fields of
        [] ->
            Buf;

        [Field | Rest] ->
            Y = erlang:element(3, erlang:element(2, Area)) + Row,
            Fits = Y < (erlang:element(3, erlang:element(2, Area)) + erlang:element(
                3,
                erlang:element(3, Area)
            )),
            Buf2 = case Fits of
                false ->
                    Buf;

                true ->
                    render_field_row(Buf, Area, Field, F, Lw, Field_idx, Y)
            end,
            render_fields(
                Buf2,
                Area,
                Rest,
                F,
                Lw,
                Field_idx + 1,
                Row + Row_height
            )
    end.

-file("src/etui/widgets/form.gleam", 457).
-spec compute_label_width(list(field(any()))) -> integer().
compute_label_width(Fields) ->
    gleam@list:fold(
        Fields,
        0,
        fun(Acc, Field) ->
            W = etui@text:cell_width(erlang:element(3, Field)),
            case W > Acc of
                true ->
                    W;

                false ->
                    Acc
            end
        end
    ).

-file("src/etui/widgets/form.gleam", 322).
?DOC(
    " Render all fields as label + value rows. Each field takes 2 rows\n"
    " (value row + optional error row). Focused field is highlighted.\n"
).
-spec render(etui@buffer:buffer(), etui@geometry:rect(), form(any())) -> etui@buffer:buffer().
render(Buf, Area, F) ->
    case ((erlang:element(2, erlang:element(3, Area)) =< 0) orelse (erlang:element(
        3,
        erlang:element(3, Area)
    )
    =< 0))
    orelse gleam@list:is_empty(erlang:element(2, F)) of
        true ->
            Buf;

        false ->
            Lw = case erlang:element(5, F) of
                0 ->
                    compute_label_width(erlang:element(2, F));

                W ->
                    W
            end,
            render_fields(Buf, Area, erlang:element(2, F), F, Lw, 0, 0)
    end.