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