telega/dialog/widget
Built-in managed keyboard widgets for dialogs.
A widget renders extra button rows for a window and handles their events
itself (see KeyboardWidget in telega/dialog/types): the dialog engine
appends widget rows after the window’s own buttons and routes widget
button presses (w:<widget_id>:<cmd> action ids) to the widget, bypassing
the window’s on_action. Widget state lives in a per-widget WidgetStore
persisted with the dialog instance, so selections and page positions
survive restarts.
Built-ins:
pager— page navigation row (‹ 2/5 ›); read the page withcurrent_pageto slice content in the window’s render.select— one-shot choice: a press callson_selected(usually aGoto).radio— single choice kept in the store, marked withlabels.checked; read withradio_value.multiselect— a set of choices with checkboxes and adonebutton shown only while the selection count is withinmin/max; read withmultiselect_values.paged_select—selectandpagercombined: items are sliced by the widget itself.counter— a number nudged with −/+ withinmin…max; read withcounter_value.calendar— a month grid bounded by a date range, with month paging.list_group— a row of buttons per item (“Edit / Delete” per row).
Reading widget state from window handlers (on_action, on_text,
on_done) and renders goes through dialog.widget_store:
let zone =
dialog.widget_store(ctx, window_id: "prefs", widget_id: "zone")
|> widget.radio_value
|> option.unwrap("hall")
All user-facing symbols (‹, ›, ●, ☑, “done”) come from the
dialog’s Labels — override them with dialog.with_labels for i18n.
Types
One button repeated for every item of a list_group. label builds its
text from the item, so it can carry the item’s own name.
pub type ItemAction {
ItemAction(id: String, label: fn(SelectItem) -> String)
}
Constructors
-
ItemAction(id: String, label: fn(SelectItem) -> String)
An item offered by select/radio/multiselect/paged_select. The id
travels in callback data — keep it short (the 64-byte limit is validated
on every render) and without : at the edges of your scheme (an id may
contain :, it is re-joined on parse).
pub type SelectItem {
SelectItem(id: String, label: String)
}
Constructors
-
SelectItem(id: String, label: String)
Values
pub fn calendar(
id id: String,
from from: calendar.Date,
to to: calendar.Date,
on_picked on_picked: fn(
state,
calendar.Date,
bot.Context(session, error, dependencies),
) -> Result(types.DialogAction(state), error),
) -> types.KeyboardWidget(state, session, error, dependencies)
A month grid of days with ‹/› month navigation, bounded by from…to.
Pressing a day calls on_picked with that date and emits whatever action
it returns — usually a Goto or a Stay that stores the date in your own
state. The displayed month lives in the widget store, so paging around does
not touch the dialog state.
widget.calendar(
id: "date",
from: calendar.Date(2026, calendar.September, 1),
to: calendar.Date(2026, calendar.December, 31),
on_picked: fn(state, date, _ctx) { Ok(types.Goto("time", with_date(state, date))) },
)
pub fn counter(
id id: String,
min min: Int,
max max: Int,
step step: Int,
initial initial: Int,
) -> types.KeyboardWidget(state, session, error, dependencies)
A number the user nudges with labels.decrement / labels.increment,
clamped to min…max. The value lives in the widget store; read it with
counter_value, passing the same initial.
|> dialog.window_with_widgets(id: "guests", render:, on_action:, widgets: [
widget.counter(id: "n", min: 1, max: 12, step: 1, initial: 2),
])
// in the render or a handler:
let guests =
dialog.widget_store(ctx, window_id: "guests", widget_id: "n")
|> widget.counter_value(default: 2)
pub fn counter_value(
store store: types.WidgetStore,
default default: Int,
) -> Int
The number in a counter store, or default before the first press.
pub fn current_page(store store: types.WidgetStore) -> Int
The current 0-based page kept by a pager/paged_select store.
pub fn list_group(
id id: String,
items items: fn(
state,
bot.Context(session, error, dependencies),
) -> List(SelectItem),
actions actions: List(ItemAction),
on_action on_action: fn(
state,
String,
String,
bot.Context(session, error, dependencies),
) -> Result(types.DialogAction(state), error),
) -> types.KeyboardWidget(state, session, error, dependencies)
A row of buttons per item — the shape select cannot express, where
every row acts on one thing: “Edit / Delete”, “▲ / ▼”, “Book / Details”.
A press calls on_action with the pressed action id and the item id.
Callback data is w:<widget_id>:<action_id> carrying the item id as its
argument, so keep both short (the 64-byte limit is checked on every
render). Item ids the widget does not currently offer are ignored —
callback data can be forged or come from an outdated message.
widget.list_group(
id: "bk",
items: fn(state: State, _ctx) {
list.map(state.bookings, fn(b) { SelectItem(id: b.id, label: b.date) })
},
actions: [
widget.ItemAction("open", fn(item: SelectItem) { item.label }),
widget.ItemAction("drop", fn(_item) { "🗑" }),
],
on_action: fn(state, action, item_id, _ctx) {
case action {
"open" -> Ok(types.Goto("details", select_booking(state, item_id)))
_ -> Ok(types.Stay(remove_booking(state, item_id)))
}
},
)
pub fn multiselect(
id id: String,
items items: fn(
state,
bot.Context(session, error, dependencies),
) -> List(SelectItem),
min min: Int,
max max: Int,
done done: String,
) -> types.KeyboardWidget(state, session, error, dependencies)
A set of choices with checkbox marks. Toggling above max is ignored;
the labels.done button renders only while the count is within
min/max and emits Goto(done, state) when pressed. done must be an
existing window id — validated by dialog.build().
pub fn multiselect_values(
store store: types.WidgetStore,
) -> List(String)
The item ids currently selected in a multiselect store, in pick order.
pub fn paged_select(
id id: String,
items items: fn(
state,
bot.Context(session, error, dependencies),
) -> List(SelectItem),
page_size page_size: Int,
columns columns: Int,
on_selected on_selected: fn(
state,
String,
bot.Context(session, error, dependencies),
) -> Result(types.DialogAction(state), error),
) -> types.KeyboardWidget(state, session, error, dependencies)
select and pager in one widget: items are sliced to the current page
by the widget itself (page_size counts items, not rows), the pager row
appears only when there is more than one page.
pub fn pager(
id id: String,
page_size page_size: Int,
total total: fn(
state,
bot.Context(session, error, dependencies),
) -> Int,
) -> types.KeyboardWidget(state, session, error, dependencies)
Page navigation: the store keeps the current page, the row renders as
‹ 2/5 › (the counter is a no-op button). total returns the number of
items; the row disappears when everything fits on one page. Slice your
content in the window’s render with current_page.
pub fn radio(
id id: String,
items items: fn(
state,
bot.Context(session, error, dependencies),
) -> List(SelectItem),
default default: option.Option(String),
) -> types.KeyboardWidget(state, session, error, dependencies)
Single choice kept in the widget store: the selected item is marked with
labels.checked, the rest with labels.unchecked. default is only a
visual pre-selection — radio_value stays None until the user actually
picks, so apply the same default when reading.
pub fn radio_value(
store store: types.WidgetStore,
) -> option.Option(String)
The item id picked in a radio store, None until the first press.
pub fn reset_stores(
ctx: bot.Context(session, error, dependencies),
) -> Nil
Drop everything seed_store put in this context’s scope.
pub fn seed_store(
ctx: bot.Context(session, error, dependencies),
window_id window_id: String,
widget_id widget_id: String,
store store: types.WidgetStore,
) -> Nil
Seed a widget store for pure render tests — the runtime equivalent is the
engine’s automatic stash before user code. Seeds live in the context’s
scope, so pass the very ctx the render is about to receive.
Merges into that scope’s stash, so several widgets can be seeded for
one render. A context built afresh starts empty; reset_stores clears one
you keep reusing.
pub fn select(
id id: String,
items items: fn(
state,
bot.Context(session, error, dependencies),
) -> List(SelectItem),
columns columns: Int,
on_selected on_selected: fn(
state,
String,
bot.Context(session, error, dependencies),
) -> Result(types.DialogAction(state), error),
) -> types.KeyboardWidget(state, session, error, dependencies)
One-shot choice: each item is a button, a press calls on_selected with
the item id and the resulting DialogAction is applied (usually a
Goto). Nothing is stored. columns lays the buttons out in a grid
(1 = one item per row).
pub fn widget_store(
ctx: bot.Context(session, error, dependencies),
window_id window_id: String,
widget_id widget_id: String,
) -> types.WidgetStore
Read a widget’s store from inside a window render or handler. Returns an
empty store when the widget has no state yet. Prefer the typed readers on
top: radio_value, multiselect_values, current_page.