# `ExRatatui.Focus`
[🔗](https://github.com/mcass19/ex_ratatui/blob/v0.10.2/lib/ex_ratatui/focus.ex#L1)

Focus management for multi-panel apps.

`Focus` is a tiny state machine over an ordered ring of focusable
IDs. Declare the IDs up front, feed every key event through
`handle_key/2`, and pattern-match on `current/1` to decide which
widget receives the keystroke. `handle_key/2` consumes
Tab / Shift+Tab (or custom overrides) and passes everything else
through unchanged.

There is no process, no macro, no protocol — just a struct kept in
the reducer state or `ExRatatui.App` model.

## Caller pattern

    def handle_event(%Event.Key{} = key, state) do
      {focus, key} = Focus.handle_key(state.focus, key)
      state = %{state | focus: focus}

      case key do
        nil ->
          # consumed by Focus (Tab / Shift+Tab); nothing more to do
          {:noreply, state}

        key ->
          case Focus.current(focus) do
            :search  -> {:noreply, update_search(state, key)}
            :results -> {:noreply, update_results(state, key)}
            :details -> {:noreply, update_details(state, key)}
          end
      end
    end

## Styling the focused widget

`Focus` never touches widget structs. Use `focused?/2` to decide the
style:

    border_style =
      if Focus.focused?(focus, :search),
        do: %Style{fg: :yellow},
        else: %Style{fg: :gray}

    %TextInput{
      state: search_state,
      block: %Block{borders: :all, border_style: border_style}
    }

## Custom keys

Pass `:next_keys` / `:prev_keys` to `new/2` as lists of
`%ExRatatui.Event.Key{}` structs. Only `:code` and `:modifiers`
matter — `:kind` is ignored, and `:modifiers` is compared as a set
(order-independent).

    Focus.new([:a, :b, :c],
      next_keys: [%Event.Key{code: "tab"}, %Event.Key{code: "right", modifiers: ["ctrl"]}],
      prev_keys: [%Event.Key{code: "left", modifiers: ["ctrl"]}]
    )

## Mouse routing

Associate each focusable ID with a hit-test `%ExRatatui.Layout.Rect{}`
after computing layout (typically inside a `%Event.Resize{}` handler
or any state change that affects geometry). `handle_mouse/2` then
focuses the widget under a left-click, passing the event through so
the underlying widget can also react.

    def handle_event(%Event.Resize{width: w, height: h}, state) do
      [search_rect, body_rect] =
        Layout.split(%Rect{x: 0, y: 0, width: w, height: h}, :vertical,
          [{:length, 3}, {:min, 0}])

      focus =
        state.focus
        |> Focus.set_region(:search, search_rect)
        |> Focus.set_region(:body, body_rect)

      %{state | focus: focus}
    end

    def handle_event(%Event.Mouse{} = mouse, state) do
      {focus, mouse} = Focus.handle_mouse(state.focus, mouse)
      # mouse is always returned for downstream handling — left-click
      # focuses the region's ID; scroll/drag/right-click are pass-through.
      ...
    end

Scroll-wheel routing is intentionally not built in: the conventional
contract is "scroll goes to the focused widget", which the app can
implement by inspecting `Focus.current/1` after `handle_mouse/2`
returns. Apps that prefer "scroll goes to the widget under the
cursor" can call `Focus.at/3` directly.

# `id`

```elixir
@type id() :: atom()
```

# `t`

```elixir
@type t() :: %ExRatatui.Focus{
  ids: [id(), ...],
  index: non_neg_integer(),
  next_keys: [ExRatatui.Event.Key.t()],
  prev_keys: [ExRatatui.Event.Key.t()],
  regions: %{required(id()) =&gt; ExRatatui.Layout.Rect.t()}
}
```

# `at`

```elixir
@spec at(t(), non_neg_integer(), non_neg_integer()) :: id() | nil
```

Returns the focusable ID whose region contains the point `(x, y)`, or
`nil` if no registered region contains the point.

When regions overlap, the smallest one (by area) wins — overlap usually
means a focusable widget sits inside a larger focusable container, and
the leaf should claim the click.

## Examples

    iex> alias ExRatatui.{Focus, Layout.Rect}
    iex> focus =
    ...>   Focus.new([:a, :b])
    ...>   |> Focus.set_region(:a, %Rect{x: 0, y: 0, width: 10, height: 10})
    ...>   |> Focus.set_region(:b, %Rect{x: 2, y: 2, width: 2, height: 2})
    iex> Focus.at(focus, 3, 3)
    :b
    iex> Focus.at(focus, 8, 8)
    :a
    iex> Focus.at(focus, 50, 50)
    nil

# `current`

```elixir
@spec current(t()) :: id()
```

Returns the currently focused ID.

## Examples

    iex> ExRatatui.Focus.new([:a, :b, :c]) |> ExRatatui.Focus.current()
    :a

    iex> ExRatatui.Focus.new([:a, :b, :c], initial: :b) |> ExRatatui.Focus.current()
    :b

# `focus`

```elixir
@spec focus(t(), id()) :: t()
```

Jumps focus to a specific ID.

Raises `ArgumentError` if `id` is not in the ring.

# `focused?`

```elixir
@spec focused?(t(), id()) :: boolean()
```

Returns `true` when `id` is the currently focused ID.

## Examples

    iex> focus = ExRatatui.Focus.new([:a, :b, :c])
    iex> ExRatatui.Focus.focused?(focus, :a)
    true
    iex> ExRatatui.Focus.focused?(focus, :b)
    false

# `handle_key`

```elixir
@spec handle_key(t(), ExRatatui.Event.Key.t()) :: {t(), ExRatatui.Event.Key.t() | nil}
```

Routes a key event through the focus ring.

Returns `{focus, nil}` when the event matched a `:next_keys` or
`:prev_keys` entry (focus moved, event consumed). Returns
`{focus, event}` unchanged otherwise so the caller can forward it to
the currently focused widget.

Matching compares `:code` and `:modifiers` (as a set). `:kind` is
ignored.

# `handle_mouse`

```elixir
@spec handle_mouse(t(), ExRatatui.Event.Mouse.t()) :: {t(), ExRatatui.Event.Mouse.t()}
```

Routes a mouse event through the focus ring.

On a left-button **down** event inside a registered region, focus
moves to that region's ID and the event is **passed through** so the
underlying widget can also react (toggle a checkbox, place a cursor,
start a drag). Every other mouse event — clicks outside any
registered region, right/middle clicks, scroll, drag, move, up — is
returned unchanged with focus untouched.

Returns `{focus, event}` regardless. Mirrors `handle_key/2` shape so
the same caller pattern (`{focus, event} = Focus.handle_*(focus, event)`)
works for both event types.

## Examples

    iex> alias ExRatatui.{Focus, Event, Layout.Rect}
    iex> focus =
    ...>   Focus.new([:a, :b])
    ...>   |> Focus.set_region(:a, %Rect{x: 0, y: 0, width: 10, height: 3})
    ...>   |> Focus.set_region(:b, %Rect{x: 0, y: 3, width: 10, height: 3})
    iex> click = %Event.Mouse{kind: "down", button: "left", x: 5, y: 4}
    iex> {focus, _event} = Focus.handle_mouse(focus, click)
    iex> Focus.current(focus)
    :b

# `new`

```elixir
@spec new(
  [id(), ...],
  keyword()
) :: t()
```

Builds a focus ring from an ordered list of IDs.

## Options

  * `:initial` — ID to start focused on (defaults to the first entry).
  * `:next_keys` — list of `%ExRatatui.Event.Key{}` that advance focus
    (defaults to Tab).
  * `:prev_keys` — list of `%ExRatatui.Event.Key{}` that retreat focus
    (defaults to Shift+Tab and `back_tab`).

Raises `ArgumentError` for an empty list, duplicate IDs, non-atom
entries, or an `:initial` that is not in `ids`.

# `next`

```elixir
@spec next(t()) :: t()
```

Advances focus to the next ID, wrapping from the last back to the first.

# `prev`

```elixir
@spec prev(t()) :: t()
```

Retreats focus to the previous ID, wrapping from the first back to the last.

# `region`

```elixir
@spec region(t(), id()) :: ExRatatui.Layout.Rect.t() | nil
```

Returns the region registered for `id`, or `nil` if none is registered.

# `set_region`

```elixir
@spec set_region(t(), id(), ExRatatui.Layout.Rect.t()) :: t()
```

Associates a hit-test region with a focusable ID.

Apps call this after computing layout (typically inside a `%Event.Resize{}`
handler, or any state change that affects the on-screen geometry of the
focusable widgets). `handle_mouse/2` uses the registered regions to focus
the widget under a click.

Raises `ArgumentError` if `id` is not in the ring.

## Examples

    iex> focus = ExRatatui.Focus.new([:search, :results])
    iex> rect = %ExRatatui.Layout.Rect{x: 0, y: 0, width: 40, height: 3}
    iex> focus |> ExRatatui.Focus.set_region(:search, rect) |> ExRatatui.Focus.region(:search)
    %ExRatatui.Layout.Rect{x: 0, y: 0, width: 40, height: 3}

# `set_regions`

```elixir
@spec set_regions(t(), %{required(id()) =&gt; ExRatatui.Layout.Rect.t()}) :: t()
```

Batch-registers multiple regions in one call.

Equivalent to calling `set_region/3` for each entry. Raises if any ID
is missing from the ring.

## Examples

    iex> focus = ExRatatui.Focus.new([:a, :b])
    iex> rects = %{
    ...>   a: %ExRatatui.Layout.Rect{x: 0, y: 0, width: 10, height: 1},
    ...>   b: %ExRatatui.Layout.Rect{x: 0, y: 1, width: 10, height: 1}
    ...> }
    iex> focus |> ExRatatui.Focus.set_regions(rects) |> ExRatatui.Focus.region(:b)
    %ExRatatui.Layout.Rect{x: 0, y: 1, width: 10, height: 1}

---

*Consult [api-reference.md](api-reference.md) for complete listing*
