-module(telega@dialog@widget). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/telega/dialog/widget.gleam"). -export([current_page/1, pager/3, select/4, radio_value/1, radio/3, multiselect_values/1, multiselect/5, paged_select/5, store_data_key/2, stash_stores/1, clear_stash/0, widget_store/3, seed_store/3]). -export_type([select_item/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. ?MODULEDOC( " Built-in managed keyboard widgets for dialogs.\n" "\n" " A widget renders extra button rows for a window and handles their events\n" " itself (see `KeyboardWidget` in `telega/dialog/types`): the dialog engine\n" " appends widget rows after the window's own buttons and routes widget\n" " button presses (`w::` action ids) to the widget, bypassing\n" " the window's `on_action`. Widget state lives in a per-widget `WidgetStore`\n" " persisted with the dialog instance, so selections and page positions\n" " survive restarts.\n" "\n" " Built-ins:\n" "\n" " - `pager` — page navigation row (`‹ 2/5 ›`); read the page with\n" " `current_page` to slice content in the window's render.\n" " - `select` — one-shot choice: a press calls `on_selected` (usually a\n" " `Goto`).\n" " - `radio` — single choice kept in the store, marked with\n" " `labels.checked`; read with `radio_value`.\n" " - `multiselect` — a set of choices with checkboxes and a `done` button\n" " shown only while the selection count is within `min`/`max`; read with\n" " `multiselect_values`.\n" " - `paged_select` — `select` and `pager` combined: items are sliced by the\n" " widget itself.\n" "\n" " Reading widget state from window handlers (`on_action`, `on_text`,\n" " `on_done`) and renders goes through `dialog.widget_store`:\n" "\n" " ```gleam\n" " let zone =\n" " dialog.widget_store(ctx, window_id: \"prefs\", widget_id: \"zone\")\n" " |> widget.radio_value\n" " |> option.unwrap(\"hall\")\n" " ```\n" "\n" " All user-facing symbols (`‹`, `›`, `●`, `☑`, \"done\") come from the\n" " dialog's `Labels` — override them with `dialog.with_labels` for i18n.\n" ). -type select_item() :: {select_item, binary(), binary()}. -file("src/telega/dialog/widget.gleam", 334). -spec action(binary(), binary()) -> binary(). action(Widget_id, Cmd) -> <<<<<<"w:"/utf8, Widget_id/binary>>/binary, ":"/utf8>>/binary, Cmd/binary>>. -file("src/telega/dialog/widget.gleam", 387). -spec clamp_page(integer(), integer()) -> integer(). clamp_page(Page, Pages) -> gleam@int:clamp(Page, 0, gleam@int:max(Pages - 1, 0)). -file("src/telega/dialog/widget.gleam", 100). ?DOC(" The current 0-based page kept by a `pager`/`paged_select` store.\n"). -spec current_page(telega@dialog@types:widget_store()) -> integer(). current_page(Store) -> _pipe = telega@dialog@types:store_get(Store, <<"page"/utf8>>), _pipe@1 = gleam@option:map(_pipe, fun gleam_stdlib:parse_int/1), _pipe@2 = gleam@option:map(_pipe@1, fun gleam@option:from_result/1), _pipe@3 = gleam@option:flatten(_pipe@2), gleam@option:unwrap(_pipe@3, 0). -file("src/telega/dialog/widget.gleam", 391). -spec turn_page(telega@dialog@types:widget_store(), binary(), integer()) -> telega@dialog@types:widget_store(). turn_page(Store, Cmd, Pages) -> Page = current_page(Store), Page@1 = case Cmd of <<"prev"/utf8>> -> Page - 1; <<"next"/utf8>> -> Page + 1; _ -> Page end, telega@dialog@types:store_set( Store, <<"page"/utf8>>, erlang:integer_to_binary(clamp_page(Page@1, Pages)) ). -file("src/telega/dialog/widget.gleam", 380). -spec page_count(integer(), integer()) -> integer(). page_count(Total, Page_size) -> case (Total =< 0) orelse (Page_size =< 0) of true -> 1; false -> case Page_size of 0 -> 0; Gleam@denominator -> ((Total + Page_size) - 1) div Gleam@denominator end end. -file("src/telega/dialog/widget.gleam", 401). -spec pager_row(binary(), integer(), integer(), telega@dialog@types:labels()) -> list(list(telega@dialog@types:dialog_button())). pager_row(Widget_id, Page, Pages, Labels) -> case Pages > 1 of false -> []; true -> Info = begin _pipe = erlang:element(4, Labels), _pipe@1 = gleam@string:replace( _pipe, <<"{current}"/utf8>>, erlang:integer_to_binary(Page + 1) ), gleam@string:replace( _pipe@1, <<"{total}"/utf8>>, erlang:integer_to_binary(Pages) ) end, [[{action_button, erlang:element(2, Labels), action(Widget_id, <<"prev"/utf8>>)}, {noop_button, Info}, {action_button, erlang:element(3, Labels), action(Widget_id, <<"next"/utf8>>)}]] end. -file("src/telega/dialog/widget.gleam", 74). ?DOC( " Page navigation: the store keeps the current page, the row renders as\n" " `‹ 2/5 ›` (the counter is a no-op button). `total` returns the number of\n" " items; the row disappears when everything fits on one page. Slice your\n" " content in the window's render with `current_page`.\n" ). -spec pager( binary(), integer(), fun((AWKD, telega@bot:context(AWKE, AWKF, AWKG)) -> integer()) ) -> telega@dialog@types:keyboard_widget(AWKD, AWKE, AWKF, AWKG). pager(Id, Page_size, Total) -> {keyboard_widget, Id, fun(Wctx) -> Pages = page_count( Total(erlang:element(2, Wctx), erlang:element(5, Wctx)), Page_size ), pager_row( Id, clamp_page(current_page(erlang:element(3, Wctx)), Pages), Pages, erlang:element(4, Wctx) ) end, fun(Wctx@1, Cmd, _) -> Pages@1 = page_count( Total(erlang:element(2, Wctx@1), erlang:element(5, Wctx@1)), Page_size ), {ok, {store_updated, turn_page(erlang:element(3, Wctx@1), Cmd, Pages@1)}} end, [], [action(Id, <<"prev"/utf8>>), action(Id, <<"next"/utf8>>)]}. -file("src/telega/dialog/widget.gleam", 372). -spec offers( fun((AWNY, telega@bot:context(AWNZ, AWOA, AWOB)) -> list(select_item())), telega@dialog@types:widget_ctx(AWNY, AWNZ, AWOA, AWOB), binary() ) -> boolean(). offers(Items, Wctx, Item_id) -> gleam@list:any( Items(erlang:element(2, Wctx), erlang:element(5, Wctx)), fun(Item) -> erlang:element(2, Item) =:= Item_id end ). -file("src/telega/dialog/widget.gleam", 353). ?DOC( " A picked id is only trusted if the widget currently offers it — callback\n" " data arrives from the client and can be forged or outdated, so an unknown\n" " id is ignored (re-render) instead of reaching `on_selected`.\n" ). -spec pick_or_ignore( telega@dialog@types:widget_ctx(AWNC, AWND, AWNE, AWNF), binary(), gleam@option:option(binary()), fun((AWNC, telega@bot:context(AWND, AWNE, AWNF)) -> list(select_item())), fun((AWNC, binary(), telega@bot:context(AWND, AWNE, AWNF)) -> {ok, telega@dialog@types:dialog_action(AWNC)} | {error, AWNE}) ) -> {ok, telega@dialog@types:widget_result(AWNC)} | {error, AWNE}. pick_or_ignore(Wctx, Cmd, Arg, Items, On_selected) -> case {Cmd, Arg} of {<<"pick"/utf8>>, {some, Item_id}} -> case offers(Items, Wctx, Item_id) of true -> _pipe = On_selected( erlang:element(2, Wctx), Item_id, erlang:element(5, Wctx) ), gleam@result:map(_pipe, fun(Field@0) -> {emit, Field@0} end); false -> {ok, {store_updated, erlang:element(3, Wctx)}} end; {_, _} -> {ok, {store_updated, erlang:element(3, Wctx)}} end. -file("src/telega/dialog/widget.gleam", 338). -spec item_grid(list(select_item()), binary(), integer()) -> list(list(telega@dialog@types:dialog_button())). item_grid(Items, Widget_id, Columns) -> _pipe = Items, _pipe@1 = gleam@list:map( _pipe, fun(Item) -> {action_arg_button, erlang:element(3, Item), action(Widget_id, <<"pick"/utf8>>), erlang:element(2, Item)} end ), gleam@list:sized_chunk(_pipe@1, gleam@int:max(Columns, 1)). -file("src/telega/dialog/widget.gleam", 114). ?DOC( " One-shot choice: each item is a button, a press calls `on_selected` with\n" " the item id and the resulting `DialogAction` is applied (usually a\n" " `Goto`). Nothing is stored. `columns` lays the buttons out in a grid\n" " (`1` = one item per row).\n" ). -spec select( binary(), fun((AWKO, telega@bot:context(AWKP, AWKQ, AWKR)) -> list(select_item())), integer(), fun((AWKO, binary(), telega@bot:context(AWKP, AWKQ, AWKR)) -> {ok, telega@dialog@types:dialog_action(AWKO)} | {error, AWKQ}) ) -> telega@dialog@types:keyboard_widget(AWKO, AWKP, AWKQ, AWKR). select(Id, Items, Columns, On_selected) -> {keyboard_widget, Id, fun(Wctx) -> _pipe = Items(erlang:element(2, Wctx), erlang:element(5, Wctx)), item_grid(_pipe, Id, Columns) end, fun(Wctx@1, Cmd, Arg) -> pick_or_ignore(Wctx@1, Cmd, Arg, Items, On_selected) end, [], [action(Id, <<"pick"/utf8>>)]}. -file("src/telega/dialog/widget.gleam", 189). ?DOC(" The item id picked in a `radio` store, `None` until the first press.\n"). -spec radio_value(telega@dialog@types:widget_store()) -> gleam@option:option(binary()). radio_value(Store) -> telega@dialog@types:store_get(Store, <<"value"/utf8>>). -file("src/telega/dialog/widget.gleam", 145). ?DOC( " Single choice kept in the widget store: the selected item is marked with\n" " `labels.checked`, the rest with `labels.unchecked`. `default` is only a\n" " visual pre-selection — `radio_value` stays `None` until the user actually\n" " picks, so apply the same default when reading.\n" ). -spec radio( binary(), fun((AWLG, telega@bot:context(AWLH, AWLI, AWLJ)) -> list(select_item())), gleam@option:option(binary()) ) -> telega@dialog@types:keyboard_widget(AWLG, AWLH, AWLI, AWLJ). radio(Id, Items, Default) -> {keyboard_widget, Id, fun(Wctx) -> Selected = begin _pipe = radio_value(erlang:element(3, Wctx)), gleam@option:'or'(_pipe, Default) end, _pipe@1 = Items(erlang:element(2, Wctx), erlang:element(5, Wctx)), gleam@list:map( _pipe@1, fun(Item) -> Mark = case Selected =:= {some, erlang:element(2, Item)} of true -> erlang:element(5, erlang:element(4, Wctx)); false -> erlang:element(6, erlang:element(4, Wctx)) end, [{action_arg_button, <>, action(Id, <<"pick"/utf8>>), erlang:element(2, Item)}] end ) end, fun(Wctx@1, Cmd, Arg) -> case {Cmd, Arg} of {<<"pick"/utf8>>, {some, Item_id}} -> case offers(Items, Wctx@1, Item_id) of true -> {ok, {store_updated, telega@dialog@types:store_set( erlang:element(3, Wctx@1), <<"value"/utf8>>, Item_id )}}; false -> {ok, {store_updated, erlang:element(3, Wctx@1)}} end; {_, _} -> {ok, {store_updated, erlang:element(3, Wctx@1)}} end end, [], [action(Id, <<"pick"/utf8>>)]}. -file("src/telega/dialog/widget.gleam", 257). ?DOC(" The item ids currently selected in a `multiselect` store, in pick order.\n"). -spec multiselect_values(telega@dialog@types:widget_store()) -> list(binary()). multiselect_values(Store) -> _pipe = telega@dialog@types:store_get(Store, <<"values"/utf8>>), _pipe@2 = gleam@option:map( _pipe, fun(Raw) -> _pipe@1 = gleam@json:parse( Raw, gleam@dynamic@decode:list( {decoder, fun gleam@dynamic@decode:decode_string/1} ) ), gleam@result:unwrap(_pipe@1, []) end ), gleam@option:unwrap(_pipe@2, []). -file("src/telega/dialog/widget.gleam", 283). -spec within(integer(), integer(), integer()) -> boolean(). within(Count, Min, Max) -> (Min =< Count) andalso (Count =< Max). -file("src/telega/dialog/widget.gleam", 266). -spec toggle(telega@dialog@types:widget_store(), binary(), integer()) -> telega@dialog@types:widget_store(). toggle(Store, Item_id, Max) -> Values = multiselect_values(Store), Values@1 = case gleam@list:contains(Values, Item_id) of true -> gleam@list:filter(Values, fun(Value) -> Value /= Item_id end); false -> case erlang:length(Values) >= Max of true -> Values; false -> lists:append(Values, [Item_id]) end end, telega@dialog@types:store_set( Store, <<"values"/utf8>>, gleam@json:to_string( gleam@json:array(Values@1, fun gleam@json:string/1) ) ). -file("src/telega/dialog/widget.gleam", 199). ?DOC( " A set of choices with checkbox marks. Toggling above `max` is ignored;\n" " the `labels.done` button renders only while the count is within\n" " `min`/`max` and emits `Goto(done, state)` when pressed. `done` must be an\n" " existing window id — validated by `dialog.build()`.\n" ). -spec multiselect( binary(), fun((AWLU, telega@bot:context(AWLV, AWLW, AWLX)) -> list(select_item())), integer(), integer(), binary() ) -> telega@dialog@types:keyboard_widget(AWLU, AWLV, AWLW, AWLX). multiselect(Id, Items, Min, Max, Done) -> {keyboard_widget, Id, fun(Wctx) -> Selected = multiselect_values(erlang:element(3, Wctx)), Rows = begin _pipe = Items(erlang:element(2, Wctx), erlang:element(5, Wctx)), gleam@list:map( _pipe, fun(Item) -> Mark = case gleam@list:contains( Selected, erlang:element(2, Item) ) of true -> erlang:element(7, erlang:element(4, Wctx)); false -> erlang:element(8, erlang:element(4, Wctx)) end, [{action_arg_button, <>, action(Id, <<"tgl"/utf8>>), erlang:element(2, Item)}] end ) end, case within(erlang:length(Selected), Min, Max) of true -> lists:append( Rows, [[{action_button, erlang:element(9, erlang:element(4, Wctx)), action(Id, <<"done"/utf8>>)}]] ); false -> Rows end end, fun(Wctx@1, Cmd, Arg) -> case {Cmd, Arg} of {<<"tgl"/utf8>>, {some, Item_id}} -> case offers(Items, Wctx@1, Item_id) of true -> {ok, {store_updated, toggle( erlang:element(3, Wctx@1), Item_id, Max )}}; false -> {ok, {store_updated, erlang:element(3, Wctx@1)}} end; {<<"done"/utf8>>, _} -> case within( erlang:length( multiselect_values(erlang:element(3, Wctx@1)) ), Min, Max ) of true -> {ok, {emit, {goto, Done, erlang:element(2, Wctx@1)}}}; false -> {ok, {store_updated, erlang:element(3, Wctx@1)}} end; {_, _} -> {ok, {store_updated, erlang:element(3, Wctx@1)}} end end, [Done], [action(Id, <<"tgl"/utf8>>), action(Id, <<"done"/utf8>>)]}. -file("src/telega/dialog/widget.gleam", 292). ?DOC( " `select` and `pager` in one widget: items are sliced to the current page\n" " by the widget itself (`page_size` counts items, not rows), the pager row\n" " appears only when there is more than one page.\n" ). -spec paged_select( binary(), fun((AWMH, telega@bot:context(AWMI, AWMJ, AWMK)) -> list(select_item())), integer(), integer(), fun((AWMH, binary(), telega@bot:context(AWMI, AWMJ, AWMK)) -> {ok, telega@dialog@types:dialog_action(AWMH)} | {error, AWMJ}) ) -> telega@dialog@types:keyboard_widget(AWMH, AWMI, AWMJ, AWMK). paged_select(Id, Items, Page_size, Columns, On_selected) -> {keyboard_widget, Id, fun(Wctx) -> All = Items(erlang:element(2, Wctx), erlang:element(5, Wctx)), Pages = page_count(erlang:length(All), Page_size), Page = clamp_page(current_page(erlang:element(3, Wctx)), Pages), Rows = begin _pipe = All, _pipe@1 = gleam@list:drop(_pipe, Page * Page_size), _pipe@2 = gleam@list:take(_pipe@1, Page_size), item_grid(_pipe@2, Id, Columns) end, lists:append( Rows, pager_row(Id, Page, Pages, erlang:element(4, Wctx)) ) end, fun(Wctx@1, Cmd, Arg) -> case Cmd of <<"pick"/utf8>> -> pick_or_ignore(Wctx@1, Cmd, Arg, Items, On_selected); _ -> Pages@1 = page_count( erlang:length( Items( erlang:element(2, Wctx@1), erlang:element(5, Wctx@1) ) ), Page_size ), {ok, {store_updated, turn_page(erlang:element(3, Wctx@1), Cmd, Pages@1)}} end end, [], [action(Id, <<"pick"/utf8>>), action(Id, <<"prev"/utf8>>), action(Id, <<"next"/utf8>>)]}. -file("src/telega/dialog/widget.gleam", 440). ?DOC(false). -spec store_data_key(binary(), binary()) -> binary(). store_data_key(Window_id, Widget_id) -> <<<<<<"__dialog_widget:"/utf8, Window_id/binary>>/binary, ":"/utf8>>/binary, Widget_id/binary>>. -file("src/telega/dialog/widget.gleam", 449). ?DOC(false). -spec stash_stores(gleam@dict:dict(binary(), binary())) -> nil. stash_stores(Data) -> _ = erlang:put(<<"__telega_dialog_widget_stores"/utf8>>, Data), nil. -file("src/telega/dialog/widget.gleam", 457). ?DOC(false). -spec clear_stash() -> nil. clear_stash() -> _ = erlang:erase(<<"__telega_dialog_widget_stores"/utf8>>), nil. -file("src/telega/dialog/widget.gleam", 493). -spec stashed_stores() -> gleam@dict:dict(binary(), binary()). stashed_stores() -> _pipe = erlang:get(<<"__telega_dialog_widget_stores"/utf8>>), _pipe@1 = gleam@dynamic@decode:run( _pipe, gleam@dynamic@decode:dict( {decoder, fun gleam@dynamic@decode:decode_string/1}, {decoder, fun gleam@dynamic@decode:decode_string/1} ) ), gleam@result:unwrap(_pipe@1, maps:new()). -file("src/telega/dialog/widget.gleam", 465). ?DOC( " Read a widget's store from inside a window render or handler. Returns an\n" " empty store when the widget has no state yet. Prefer the typed readers on\n" " top: `radio_value`, `multiselect_values`, `current_page`.\n" ). -spec widget_store(telega@bot:context(any(), any(), any()), binary(), binary()) -> telega@dialog@types:widget_store(). widget_store(_, Window_id, Widget_id) -> _pipe = stashed_stores(), _pipe@1 = gleam_stdlib:map_get(_pipe, store_data_key(Window_id, Widget_id)), _pipe@2 = gleam@result:'try'( _pipe@1, fun(Raw) -> telega@dialog@types:decode_store(Raw) end ), gleam@result:unwrap(_pipe@2, telega@dialog@types:new_store()). -file("src/telega/dialog/widget.gleam", 478). ?DOC( " Seed a widget store for pure render tests — the runtime equivalent is the\n" " engine's automatic stash before user code. Merges into the current stash.\n" ). -spec seed_store(binary(), binary(), telega@dialog@types:widget_store()) -> nil. seed_store(Window_id, Widget_id, Store) -> Stores = gleam@dict:insert( stashed_stores(), store_data_key(Window_id, Widget_id), telega@dialog@types:encode_store(Store) ), _ = erlang:put(<<"__telega_dialog_widget_stores"/utf8>>, Stores), nil.