# Widgets Cheatsheet

Quick lookup for every built-in widget and helper. One example per struct, key fields inline. For concepts see [Building UIs](../building_uis.md); for full option shapes, the module docs.

## Styles & rich text
{: .col-2}

### Style

```elixir
%Style{
  fg: :green,                         # or {r,g,b}, or 0..255 index
  bg: :reset,
  underline_color: {:rgb, 255, 0, 0}, # colored underline (kitty/wezterm)
  modifiers: [:bold, :italic, :underlined, :reversed, :dim]
}
```

Colors accept `:reset`, named atoms, `{r, g, b}`, or 0–255. Modifiers are additive.

### Rich text spans/lines

```elixir
alias ExRatatui.Text.{Line, Span}

Line.new([
  Span.new(" ok ", style: %Style{fg: :green, modifiers: [:bold]}),
  Span.new(" build passed", style: %Style{fg: :gray})
])
```

Accepted by `Paragraph.text`, `List.items`, `Table.rows`/`header`, `Tabs.titles`, `Block.title`.

## Events
{: .col-2}

### `poll_event/1` returns

```elixir
case ExRatatui.poll_event(timeout) do
  %Event.Key{code: "q"} -> :quit
  %Event.Mouse{kind: "down", button: "left", x: x, y: y} ->
    {:click, x, y}
  %Event.Resize{width: w, height: h} -> {:resize, w, h}
  %Event.Paste{content: text} -> {:paste, text}
  %Event.FocusGained{} -> :resume_animations
  %Event.FocusLost{} -> :pause_animations
  nil -> :no_event
end
```

`Event.Paste` arrives only on the local terminal today (byte-stream transports' VTE parser doesn't decode `CSI 200~/201~` yet). Focus events are **opt-in**: `ExRatatui.run(fun, focus_events: true)`.

### Consuming Paste in widgets

```elixir
case event do
  %Event.Paste{content: text} ->
    ExRatatui.text_input_insert_str(state.input, text)
  # ...
end
```

`text_input_insert_str/2` strips every control character (single-line). `textarea_insert_str/2` preserves `\n` and `\r\n` as line breaks; lone `\r` is dropped. Both batch the insert as one NIF call.

## Layout
{: .col-3}

### `Layout.split/4`

```elixir
alias ExRatatui.Layout

[left, right] =
  Layout.split(frame_rect, :horizontal, [
    {:percentage, 30},
    {:percentage, 70}
  ])
```

Direction: `:horizontal` or `:vertical`. Constraints: `:length`, `:percentage`, `:min`, `:max`, `:ratio`, `{:fill, weight}`.

### Flex + spacing + margin

```elixir
# Centered fixed-width popup
[popup] = Layout.split(area, :horizontal,
  [{:length, 40}], flex: :center)

# Two growable panels with a 2-cell gutter
[a, b] = Layout.split(area, :horizontal,
  [{:fill, 1}, {:fill, 1}], spacing: 2)

# Split but leave a 1-cell border around the edges
[body] = Layout.split(area, :vertical, [{:min, 0}], margin: 1)
```

`:flex` is one of `:legacy`, `:start`, `:end`, `:center`, `:space_between`, `:space_around`. `:spacing` is cells *between* segments; `:margin` (or `:horizontal_margin` / `:vertical_margin`) insets the whole area *before* splitting.

### `%Rect{}`

```elixir
%ExRatatui.Layout.Rect{x: 0, y: 0, width: 80, height: 24}
```

All widgets render into a `Rect`. Returned from `Layout.split/4`, derived from `%Frame{}`, or built manually for absolute placement.

### Padding helpers

```elixir
alias ExRatatui.Layout.Padding

%Block{padding: Padding.uniform(1)}        # {1, 1, 1, 1}
%Block{padding: Padding.symmetric(2, 1)}   # {2, 2, 1, 1}  (horiz, vert)
%Block{padding: Padding.horizontal(2)}     # {2, 2, 0, 0}
%Block{padding: Padding.vertical(1)}       # {0, 0, 1, 1}
%Block{padding: Padding.new(1, 2, 3, 4)}   # {1, 2, 3, 4}
```

`Block.padding` is a `{left, right, top, bottom}` tuple; these just build it.

## Focus & theming
{: .col-2}

### Focus ring

```elixir
alias ExRatatui.{Focus, Event}

# Build the ring once.
focus = Focus.new([:search, :results, :details])

# Register hit-test regions after layout (typically on resize).
focus =
  focus
  |> Focus.set_region(:search, search_rect)
  |> Focus.set_regions(%{results: results_rect, details: details_rect})

# Route keys: Tab / Shift+Tab move focus, everything else passes through.
{focus, key} = Focus.handle_key(focus, key_event)

# Route mouse: left-click in a region focuses that ID + passes event through.
{focus, mouse} = Focus.handle_mouse(focus, mouse_event)

# Style off the focused ID — Focus never touches widget structs.
border = if Focus.focused?(focus, :search),
  do: %Style{fg: :yellow},
  else: %Style{fg: :gray}
```

`Focus.at(focus, x, y)` returns the ID under a point (smallest region wins on overlap). Scroll routing is intentionally not built in — pick `Focus.current/1` for "scroll the focused widget" or `Focus.at/3` for "scroll the widget under the cursor".

### Theme palette

```elixir
alias ExRatatui.Theme

theme = Theme.default()                 # dark-friendly; surface nil
# or  Theme.light()                     # dark text on white surface
# or  %Theme{primary: :magenta, accent: {:rgb, 245, 158, 11}, ...}

%Block{
  borders: [:all],
  border_style: Theme.border_style(theme, focused: focused?)
}

%List{
  items: results,
  highlight_style: Theme.selection_style(theme)
}

# Body text + dim hint
Theme.text_style(theme)                 # %Style{fg: theme.text, bg: theme.surface}
Theme.text_style(theme, dim: true)      # %Style{fg: theme.text_dim, ...}
```

Eleven semantic slots: `:primary`, `:accent`, `:border`, `:border_focused`, `:surface`, `:surface_alt`, `:text`, `:text_dim`, `:success`, `:warning`, `:danger`. Each accepts any `t:ExRatatui.Style.color/0`. Apps thread the theme through render code by hand; no global config, no automatic widget injection.

## Text & content
{: .col-2}

### Paragraph

```elixir
%Paragraph{
  text: "Hello!",                      # String | Span | Line | [Span]
  style: %Style{fg: :green},
  alignment: :center,                  # :left | :center | :right
  wrap: true,
  scroll: {0, 0},                      # {vertical, horizontal}
  block: %Block{title: " Info ", borders: [:all]}
}
```

### Markdown

```elixir
%Markdown{
  content: "# Title\n\n**bold** *italic* `code`\n\n- a\n- b",
  wrap: true,
  scroll: {0, 0},
  block: %Block{borders: [:all]}
}
```

Supports headings, bold/italic, inline code, lists.

### BigText

```elixir
ExRatatui.BigText.new("HELLO",
  pixel_size: :quadrant,               # :full | :half_height | :half_width
                                       # :quadrant | :third_height | :sextant
                                       # :quarter_height | :octant
  alignment: :center,                  # :left | :center | :right
  style: %Style{fg: :magenta},
  block: %Block{borders: [:all]}
)
```

Oversized 8×8-pixel text for slide titles / banners. Smaller `pixel_size` packs more characters into the same area; `:octant` is the densest. Backed by [tui-big-text](https://github.com/ratatui/tui-widgets/tree/main/tui-big-text).

### CodeBlock

```elixir
%ExRatatui.Widgets.CodeBlock{
  content: source_code,
  language: "rust",                    # nil = plain text fallback
  theme: :base16_ocean_dark,           # 7 curated atoms or any raw string
  line_numbers: true,
  starting_line: 1,
  highlight_lines: [3, 7..9]           # ints + ranges, normalised
}
```

Syntax-highlighted source code, display-only. Themes (curated atoms):
`:base16_ocean_dark`, `:base16_ocean_light`, `:base16_eighties_dark`,
`:base16_mocha_dark`, `:inspired_github`, `:solarized_dark`, `:solarized_light`.
Raw strings pass through for custom theme sets. Languages: syntect's bundled
SyntaxSet (Rust, Python, JS, Ruby, Go, Java, JSON, YAML, Erlang, etc.) **plus
Elixir** which we ship as an additional bundled syntax.

Raw highlighted lines for composite widgets:

```elixir
ExRatatui.CodeBlock.highlight("fn main() {}", "rust", :solarized_dark)
# => [%ExRatatui.Text.Line{spans: [...]}, ...]
```

## Lists, tables & scrolling
{: .col-2}

### List

```elixir
%List{
  items: ["Item 1", "Item 2", "Item 3"],
  selected: 0,
  highlight_style: %Style{fg: :yellow, modifiers: [:bold]},
  highlight_symbol: ">> ",
  direction: :bottom_to_top,            # default :top_to_bottom (chat logs)
  scroll_padding: 2,                     # default 0; vim's scrolloff
  repeat_highlight_symbol: true,         # default false; multi-row items
  block: %Block{title: " Menu ", borders: [:all]}
}
```

### Table

```elixir
%Table{
  header: ["Name", "Score"],
  footer: ["Total", "182"],              # optional, renders at bottom
  rows: [["Alice", "95"], ["Bob", "87"]],
  widths: [{:percentage, 70}, {:percentage, 30}],
  selected: 0,                           # row index
  selected_column: 1,                    # required to fire column/cell styles below
  highlight_style: %Style{fg: :yellow},          # row highlight
  header_style: %Style{modifiers: [:bold]},
  footer_style: %Style{fg: :gray},
  column_highlight_style: %Style{bg: :dark_gray},
  cell_highlight_style: %Style{fg: :black, bg: :yellow},
  highlight_spacing: :always,            # :always | :when_selected (default) | :never
  column_spacing: 1,
  block: %Block{borders: [:all]}
}
```

Without `:selected_column` set, `:column_highlight_style` and `:cell_highlight_style` are no-ops (the activation gate is ratatui's `TableState::select_column/1`).

### WidgetList

```elixir
%WidgetList{
  items: [
    {%Paragraph{text: "Row 1"}, 3},    # {widget, height}
    {%Paragraph{text: "Row 2"}, 5}
  ],
  selected: 0,
  scroll_offset: 0,
  highlight_style: %Style{bg: {50, 50, 50}}
}
```

Row-clipped scroll. Good for 1k+ items; only visible rows cross the NIF boundary.

### Scrollbar

```elixir
%Scrollbar{
  orientation: :vertical_right,        # or :vertical_left, :horizontal_bottom, :horizontal_top
  content_length: 100,
  position: 25,
  viewport_content_length: 20,
  thumb_style: %Style{fg: :yellow}
}
```

## Progress & activity
{: .col-2}

### Gauge

```elixir
%Gauge{
  ratio: 0.65,                         # 0.0..1.0
  label: "65%",
  gauge_style: %Style{fg: :green},
  block: %Block{borders: [:all]}
}
```

### LineGauge

```elixir
%LineGauge{
  ratio: 0.4,
  label: "Loading...",
  filled_style: %Style{fg: :blue},
  unfilled_style: %Style{fg: :dark_gray}
}
```

Single-cell-tall bar — great for compact status rows.

### Sparkline

```elixir
%Sparkline{
  data: [0, 1, 3, 5, 8, nil, 2, 4],    # nil = absent
  max: 8,                              # nil auto-scales
  direction: :left_to_right,
  bar_set: :nine_levels,
  style: %Style{fg: :cyan}
}
```

### Throbber

```elixir
%Throbber{
  label: "Loading",
  throbber_set: :braille,              # :braille | :dots | :line | :ascii
  step: state.tick,                    # caller increments
  throbber_style: %Style{fg: :cyan}
}
```

Drive with `Subscription.interval/3` (reducer) or `Process.send_after/3` (callback).

### BarChart

```elixir
alias ExRatatui.Widgets.Bar

%BarChart{
  data: [
    %Bar{label: "Mon", value: 42},
    %Bar{label: "Tue", value: 67, style: %Style{fg: :yellow}}
  ],
  bar_width: 5,
  bar_gap: 1,
  direction: :vertical,                # or :horizontal
  max: 100                             # nil auto-scales
}
```

Use `:groups` with `%BarGroup{}` for side-by-side clusters (mutually exclusive with `:data`).

## Input
{: .col-2}

### TextInput

```elixir
state = ExRatatui.text_input_new()     # mount once, keep in app state

%TextInput{
  state: state,
  placeholder: "Type here...",
  style: %Style{fg: :white},
  cursor_style: %Style{bg: :white, fg: :black},
  block: %Block{borders: [:all]}
}

ExRatatui.text_input_handle_key(state, key_event)
value = ExRatatui.text_input_get_value(state)
```

### Textarea

```elixir
state = ExRatatui.textarea_new()

%Textarea{
  state: state,
  placeholder: "Enter message...",
  cursor_line_style: %Style{bg: {40, 40, 40}},
  line_number_style: %Style{fg: :dark_gray},  # nil hides line numbers
  block: %Block{borders: [:all]}
}

ExRatatui.textarea_handle_key(state, key_event)
value = ExRatatui.textarea_get_value(state)
```

Enter = newline, Ctrl+Enter = submit.

### Checkbox

```elixir
%Checkbox{
  label: "Enable notifications",
  checked: true,
  checked_style: %Style{fg: :green},
  checked_symbol: "[x]",
  unchecked_symbol: "[ ]"
}
```

### SlashCommands

```elixir
alias ExRatatui.Widgets.SlashCommands
alias ExRatatui.Widgets.SlashCommands.Command

commands = [
  %Command{name: "help", description: "Show help", aliases: ["h", "?"]},
  %Command{name: "quit", description: "Exit", aliases: ["q"]}
]

case SlashCommands.parse(input_text) do
  {:slash, query} -> SlashCommands.match_commands(commands, query)
  :no_slash -> []
end
```

Pair the matches with a `%Popup{}` in `render/2` to show autocomplete.

## Charts & canvas
{: .col-2}

### Chart

```elixir
alias ExRatatui.Widgets.Chart.{Axis, Dataset}

%Chart{
  datasets: [
    %Dataset{
      name: "CPU",
      data: [{0.0, 12.0}, {1.0, 25.0}, {2.0, 48.0}],
      marker: :braille,
      graph_type: :line,                # :line | :scatter | :bar
      style: %Style{fg: :cyan}
    }
  ],
  x_axis: %Axis{title: "Time", bounds: {0.0, 3.0}, labels: ["0", "1", "2", "3"]},
  y_axis: %Axis{title: "Usage %", bounds: {0.0, 100.0}, labels: ["0", "50", "100"]},
  legend_position: :top_right            # nil hides
}
```

### Canvas

```elixir
alias ExRatatui.Widgets.Canvas.{Circle, Label, Line, Points, Rectangle}

%Canvas{
  x_bounds: {0.0, 100.0},
  y_bounds: {0.0, 50.0},
  marker: :braille,                     # :braille | :dot | :block | :bar | :half_block
  background_color: :black,
  shapes: [
    %Line{x1: 0.0, y1: 0.0, x2: 100.0, y2: 50.0, color: :cyan},
    %Rectangle{x: 10.0, y: 10.0, width: 30.0, height: 20.0, color: :yellow},
    %Circle{x: 70.0, y: 25.0, radius: 10.0, color: :magenta},
    %Points{coords: [{20.0, 40.0}, {50.0, 30.0}], color: :green},
    %Label{x: 70.0, y: 25.0, text: "★", color: :white}
  ]
}
```

### World map

```elixir
alias ExRatatui.Widgets.Canvas.Map, as: CanvasMap

%Canvas{
  x_bounds: {-180.0, 180.0},
  y_bounds: {-90.0, 90.0},
  marker: :dot,
  shapes: [
    %CanvasMap{resolution: :high, color: :green},
    %Label{x: -74.0, y: 40.7, text: "NYC", color: :yellow}
  ]
}
```

## Containers & overlays
{: .col-2}

### Block

```elixir
%Block{
  title: " Panel ",                     # String | Span | Line | [Span]
  borders: [:all],                      # or [:top, :left, ...]
  border_type: :rounded,                # :plain :rounded :double :thick
                                        # :light/heavy_{double,triple,quadruple}_dashed
                                        # :quadrant_inside :quadrant_outside
  border_style: %Style{fg: :cyan},
  padding: {0, 0, 0, 0}                 # {left, right, top, bottom}
}
```

Wraps another widget via that widget's `:block` field.

### Block multi-title

```elixir
alias ExRatatui.Widgets.Block.Title

%Block{
  title: "src/lib.rs",                  # top-left, single-title shortcut
  titles: [
    %Title{content: "[3/12]", alignment: :right},
    %Title{content: "modified", position: :bottom, alignment: :left},
    %Title{content: "L42:7",   position: :bottom, alignment: :right}
  ],
  title_position: :top,                 # default for titles without one
  title_alignment: :left,               # default alignment
  title_style: %Style{fg: :gray},       # default style
  borders: [:all]
}
```

Each `%Block.Title{}` carries optional `:position`, `:alignment`, `:style` overrides over the block defaults. Raw line-like entries in `:titles` (e.g. `titles: ["status"]`) inherit the defaults.

### Popup

```elixir
%Popup{
  content: %Paragraph{text: "Are you sure?"},
  percent_width: 50,                    # or fixed_width: 40
  percent_height: 30,                   # or fixed_height: 10
  block: %Block{title: " Confirm ", borders: [:all], border_type: :double}
}
```

Renders centered on top of whatever's already drawn.

### Tabs

```elixir
%Tabs{
  titles: ["Overview", "Details", "Settings"],
  selected: 0,
  highlight_style: %Style{fg: :yellow, modifiers: [:bold]},
  divider: " | ",
  padding: {1, 1},
  block: %Block{borders: [:bottom]}
}
```

### Clear

```elixir
%Clear{}
```

Fills the rect with blank cells. Use before a popup to dim underlying content.

## Calendar
{: .width-50}

```elixir
%Calendar{
  display_date: ~D[2026-03-15],
  events: [
    {~D[2026-03-10], %Style{fg: :red, modifiers: [:bold]}},
    {~D[2026-03-20], %Style{fg: :green}}
  ],
  default_style: %Style{fg: :white},
  show_month_header: true,
  header_style: %Style{fg: :yellow, modifiers: [:bold]},
  show_weekdays_header: true,
  weekday_style: %Style{fg: :cyan},
  show_surrounding: %Style{fg: :dark_gray},   # nil hides surrounding days
  block: %Block{title: " March ", borders: [:all]}
}
```

Needs ~22×8 cells (24×10 with a block). `events` accepts a keyword-like list of `{%Date{}, %Style{}}` tuples or a `%{Date => Style}` map.

## Image
{: .width-50}

```elixir
{:ok, picture} = ExRatatui.Image.new(File.read!("priv/cover.png"),
  protocol: :auto,        # or :halfblocks / :kitty / :sixel / :iterm2
  resize: :fit,           # or :crop / :scale
  background: {0, 0, 0}   # nil for transparent
)

# Drop into a render tree like any other widget:
[{picture, area}]
```

Hold the handle in your model (decode is non-trivial — don't rebuild per frame). `:auto` adapts to the transport: Kitty/Ghostty get native graphics, Livebook gets halfblocks. Errors return `{:error, {:decode_failed, msg}}`.

Probe the local terminal for best protocol detection:

```elixir
ExRatatui.Image.auto_local_protocol(terminal_ref)
```

Over SSH or Distributed, declare both the protocol and the client's cell pixel size (needed for accurate Kitty/Sixel/iTerm2 scaling):

```elixir
ExRatatui.SSH.Daemon.start_link(
  mod: MyApp.TUI,
  image_protocol: :kitty,
  image_font_size: {10, 20}
)

ExRatatui.Distributed.attach(:"app@host", MyApp.TUI,
  image_protocol: :kitty,
  image_font_size: {10, 20}
)
```

See [Images](../images.md) for the full transport / protocol resolution table.
