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. Defaultnil; set by the renderer. Mount-only.:ratio-float/0in0.0..1.0giving the initial split point. Default0.5. Mount-only —update/2never changes it.:orientation-:horizontal(side-by-side panes) or:vertical(stacked panes). Default:horizontal. Live-updatable. Any other value raises aCaseClauseErrorfromrender/2.:total_size-pos_integer/0size in cells of the axis being split. Default100. Live-updatable; changing it re-clamps a:fixed_posthat has already been set.:show_handle-boolean/0, draw the grip marker in the middle of the divider. Defaulttrue. 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.:liveemits{: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;niluntil the first drag or nudge, after which it overrides:ratio:orientation-:horizontal(side-by-side) or:vertical(top-bottom):dragging-truebetween a press and the matching mouse up:drag_start_pos- the position at the moment of the press;nilotherwise
Summary
Functions
The divider's current offset in cells from the start of the parent rect.
Moves the divider by the drag delta and clamps it.
Callback implementation for Drafter.Widget.handle_event/2.
Bubbles every unmodified key press. Resizing needs alt or shift; see
handle_key/3.
Nudges the divider one cell when alt or shift is held and the arrow key
matches the orientation.
Ends a drag by clearing :dragging. The coordinates are ignored.
Starts a drag: sets :dragging and records :drag_start_pos as the current
effective position. The press coordinates are ignored.
Builds the widget state from props.
Draws the divider into rect, using the current theme's :primary colour while
focused and :text_muted otherwise.
Callback implementation for Drafter.Widget.unmount/1.
Folds :orientation, :total_size and :show_handle into state.
Types
Functions
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
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}]
Callback implementation for Drafter.Widget.handle_event/2.
Bubbles every unmodified key press. Resizing needs alt or shift; see
handle_key/3.
@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
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
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}
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
@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.
Callback implementation for Drafter.Widget.unmount/1.
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