# `Drafter.Widget.SplitPaneDivider`
[🔗](https://github.com/jaman/drafter/blob/main/lib/drafter/widget/split_pane_divider.ex#L1)

Renders a resizable divider between two panes in a split layout.

The divider position starts as a ratio (0.0–1.0) for the initial draw and after
screen resize. Once the user drags the divider, it switches to an absolute pixel
position so that resizing an outer split pane does not affect this divider.

Once `:fixed_pos` is set it takes precedence over `:ratio`; a parent that wants
to reposition the divider from the outside must clear it.

## Component tag

This module has no `component_tag/0` and no `Drafter.App` helper of its own.
The renderer creates one divider per gap in a `{:split_pane, children, opts}`
element, built by `Drafter.App.split_pane/2`.

## Options

  * `:id` - identifier for the divider. Default `nil`; set by the renderer.
    Mount-only.
  * `:ratio` - `t:float/0` in `0.0..1.0` giving the initial split point. Default
    `0.5`. Mount-only — `update/2` never changes it.
  * `:orientation` - `:horizontal` (side-by-side panes) or `:vertical` (stacked
    panes). Default `:horizontal`. Live-updatable. Any other value raises a
    `CaseClauseError` from `render/2`.
  * `:total_size` - `t:pos_integer/0` size in cells of the axis being split.
    Default `100`. Live-updatable; changing it re-clamps a `:fixed_pos` that has
    already been set.
  * `:show_handle` - `t:boolean/0`, draw the grip marker in the middle of the
    divider. Default `true`. Live-updatable. The marker is only drawn while the
    divider is focused.
  * `:resize_mode` - `:quick | :live`. Default `:quick`, which emits
    `{:divider_move, :quick}` on each drag step and `{:widget_layout_needed,
    :all}` on mouse up. `:live` emits `{:widget_layout_needed, :all}` on every
    drag step and nothing on mouse up. Mount-only.

## Key bindings

Arrow keys move the divider by one cell, and only with `alt` or `shift` held:
left/right for a `:horizontal` divider, up/down for a `:vertical` one. Every
other key bubbles.

## Position clamping

Both dragging and nudging clamp the position to
`max(1, round(total_size * 0.1))..min(total_size - 2, round(total_size * 0.9))`,
so with the default `:total_size` of `100` the divider stays within columns
10 through 90.

## State fields (read via `WidgetHierarchy.get_widget_state/2`)

  * `:ratio` - float 0.0–1.0; used for initial layout and screen resize fallback
  * `:fixed_pos` - integer column/row offset from start of parent rect; `nil`
    until the first drag or nudge, after which it overrides `:ratio`
  * `:orientation` - `:horizontal` (side-by-side) or `:vertical` (top-bottom)
  * `:dragging` - `true` between a press and the matching mouse up
  * `:drag_start_pos` - the position at the moment of the press; `nil` otherwise

# `t`

```elixir
@type t() :: %Drafter.Widget.SplitPaneDivider{
  drag_start_pos: integer() | nil,
  dragging: boolean(),
  fixed_pos: integer() | nil,
  focused: boolean(),
  id: term(),
  orientation: :horizontal | :vertical,
  ratio: float(),
  resize_mode: :quick | :live,
  show_handle: boolean(),
  total_size: pos_integer()
}
```

# `effective_pos`

```elixir
@spec effective_pos(t()) :: integer()
```

The divider's current offset in cells from the start of the parent rect.

Returns `:fixed_pos` when it is an integer, and `round(ratio * (total_size - 1))`
otherwise. Not clamped — an out-of-range `:fixed_pos` is returned as it stands.

    iex> Drafter.Widget.SplitPaneDivider.effective_pos(Drafter.Widget.SplitPaneDivider.mount(%{}))
    50

    iex> state = Drafter.Widget.SplitPaneDivider.mount(%{ratio: 0.25, total_size: 41})
    iex> Drafter.Widget.SplitPaneDivider.effective_pos(state)
    10

    iex> state = %{Drafter.Widget.SplitPaneDivider.mount(%{}) | fixed_pos: 7}
    iex> Drafter.Widget.SplitPaneDivider.effective_pos(state)
    7

# `focused`

# `handle_drag`

```elixir
@spec handle_drag(integer(), integer(), t()) :: {:ok, t(), list()}
```

Moves the divider by the drag delta and clamps it.

`x` is used for a `:horizontal` divider and `y` for a `:vertical` one; the other
coordinate is ignored. Both are deltas, not absolute positions. Returns
`{:ok, state, [{:widget_layout_needed, :all}]}` in `:live` resize mode and
`{:ok, state, [{:divider_move, :quick}]}` otherwise.

    iex> state = Drafter.Widget.SplitPaneDivider.mount(%{})
    iex> {:ok, dragged, actions} = Drafter.Widget.SplitPaneDivider.handle_drag(5, 0, state)
    iex> {dragged.fixed_pos, actions}
    {55, [{:divider_move, :quick}]}

    iex> state = Drafter.Widget.SplitPaneDivider.mount(%{resize_mode: :live})
    iex> {:ok, _dragged, actions} = Drafter.Widget.SplitPaneDivider.handle_drag(5, 0, state)
    iex> actions
    [{:widget_layout_needed, :all}]

# `handle_event`

# `handle_key`

```elixir
@spec handle_key(term(), t()) :: {:bubble, t()}
```

Bubbles every unmodified key press. Resizing needs `alt` or `shift`; see
`handle_key/3`.

# `handle_key`

```elixir
@spec handle_key(term(), Drafter.Widget.modifiers(), t()) ::
  {:ok, t()} | {:bubble, t()}
```

Nudges the divider one cell when `alt` or `shift` is held and the arrow key
matches the orientation.

Returns `{:ok, state}` with `:fixed_pos` moved and clamped, or `{:bubble, state}`
when no modifier is held or the key does not match the axis.

    iex> state = Drafter.Widget.SplitPaneDivider.mount(%{})
    iex> {:ok, moved} = Drafter.Widget.SplitPaneDivider.handle_key(:left, [:alt], state)
    iex> moved.fixed_pos
    49

    iex> state = Drafter.Widget.SplitPaneDivider.mount(%{})
    iex> Drafter.Widget.SplitPaneDivider.handle_key(:left, [], state) == {:bubble, state}
    true

    iex> state = Drafter.Widget.SplitPaneDivider.mount(%{})
    iex> Drafter.Widget.SplitPaneDivider.handle_key(:up, [:alt], state) == {:bubble, state}
    true

# `handle_mouse_up`

```elixir
@spec handle_mouse_up(integer(), integer(), t()) :: {:ok, t()} | {:ok, t(), list()}
```

Ends a drag by clearing `:dragging`. The coordinates are ignored.

In `:live` resize mode returns `{:ok, state}` and keeps `:drag_start_pos`,
because the layout was already refreshed on every drag step. In `:quick` mode it
also clears `:drag_start_pos` and returns
`{:ok, state, [{:widget_layout_needed, :all}]}`.

    iex> state = Drafter.Widget.SplitPaneDivider.mount(%{})
    iex> {:ok, released, actions} = Drafter.Widget.SplitPaneDivider.handle_mouse_up(0, 0, state)
    iex> {released.dragging, actions}
    {false, [{:widget_layout_needed, :all}]}

    iex> state = Drafter.Widget.SplitPaneDivider.mount(%{resize_mode: :live})
    iex> {:ok, released} = Drafter.Widget.SplitPaneDivider.handle_mouse_up(0, 0, state)
    iex> released.dragging
    false

# `handle_press`

```elixir
@spec handle_press(integer(), integer(), t()) :: {:ok, t()}
```

Starts a drag: sets `:dragging` and records `:drag_start_pos` as the current
effective position. The press coordinates are ignored.

    iex> state = Drafter.Widget.SplitPaneDivider.mount(%{})
    iex> {:ok, pressed} = Drafter.Widget.SplitPaneDivider.handle_press(0, 0, state)
    iex> {pressed.dragging, pressed.drag_start_pos}
    {true, 50}

# `mount`

```elixir
@spec mount(map()) :: t()
```

Builds the widget state from `props`.

Reads `:id` (default `nil`), `:ratio` (default `0.5`), `:orientation` (default
`:horizontal`), `:total_size` (default `100`), `:show_handle` (default `true`)
and `:resize_mode` (default `:quick`). `:fixed_pos` always starts as `nil` and
`:focused` and `:dragging` as `false`, whatever `props` says.

    iex> state = Drafter.Widget.SplitPaneDivider.mount(%{})
    iex> {state.ratio, state.orientation, state.total_size, state.resize_mode}
    {0.5, :horizontal, 100, :quick}

    iex> Drafter.Widget.SplitPaneDivider.mount(%{fixed_pos: 40}).fixed_pos
    nil

# `preferred_height`

# `render`

```elixir
@spec render(t(), Drafter.Widget.rect()) :: [Drafter.Draw.Strip.t()]
```

Draws the divider into `rect`, using the current theme's `:primary` colour while
focused and `:text_muted` otherwise.

A `:horizontal` divider is a one-column vertical line of `rect.height` strips; a
`:vertical` divider is one strip `rect.width` wide. The grip marker replaces the
middle character only while the divider is focused and `:show_handle` is set.

# `unmount`

# `update`

```elixir
@spec update(map(), t()) :: t()
```

Folds `:orientation`, `:total_size` and `:show_handle` into `state`.

`:id`, `:ratio` and `:resize_mode` are ignored, so they are mount-only. A
`:fixed_pos` that has already been set is re-clamped when `:total_size` changes
and otherwise kept, so a parent that wants to reposition the divider from the
outside has to clear `:fixed_pos` itself.

    iex> state = Drafter.Widget.SplitPaneDivider.mount(%{})
    iex> Drafter.Widget.SplitPaneDivider.update(%{ratio: 0.9, show_handle: false}, state)
    ...> |> then(&{&1.ratio, &1.show_handle})
    {0.5, false}

    iex> state = %{Drafter.Widget.SplitPaneDivider.mount(%{}) | fixed_pos: 90}
    iex> Drafter.Widget.SplitPaneDivider.update(%{total_size: 50}, state).fixed_pos
    45

# `update_props_from_mount`

---

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