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:

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

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

Search Document