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