TermUI.Widgets.ContextMenu.Behavior (TermUI v1.0.0)
View SourceShared behavior for context menu variants.
This module provides common functionality for both positioned (ContextMenu)
and inline (ContextMenu.Inline) menu implementations. It extracts shared
logic for item selection, cursor management, and menu actions to eliminate
code duplication and ensure consistent behavior across menu types.
This is not a formal Elixir @behaviour but rather a collection of utility
functions used by multiple menu implementations.
Shared Functionality
- Item Selection: Determining which items can be selected
- Cursor Management: Moving cursor between selectable items
- Menu Actions: Selecting items and closing menus
Usage
Menu implementations should alias this module and delegate to its functions:
alias TermUI.Widgets.ContextMenu.Behavior
def init(props) do
state = %{
items: props.items,
cursor: Behavior.find_first_selectable(props.items),
# ...
}
{:ok, state}
end
def handle_event(%Event.Key{key: :down}, state) do
state = Behavior.move_cursor(state, 1)
{:ok, state}
end
Summary
Functions
Closes the menu and invokes the on_close callback.
Finds the ID of the first selectable item in a list.
Moves the cursor in the specified direction among selectable items.
Selects the item at the current cursor position.
Returns whether an item can be selected.
Functions
Finds the ID of the first selectable item in a list.
Returns nil if no selectable items exist. This is useful for initializing
the cursor position to the first available action.
Examples
iex> items = [
...> %{type: :separator},
...> %{type: :action, id: :copy, disabled: false},
...> %{type: :action, id: :paste, disabled: false}
...> ]
iex> Behavior.find_first_selectable(items)
:copy
iex> Behavior.find_first_selectable([%{type: :separator}])
nil
Moves the cursor in the specified direction among selectable items.
Direction is +1 for next item, -1 for previous item. Movement is clamped
at boundaries (does not wrap around). Non-selectable items (separators, disabled
actions) are automatically skipped.
Parameters
state- State map containing:itemsand:cursorkeysdirection- Integer offset:-1for previous,+1for next
Returns
Updated state with new cursor position. If cursor is at a boundary and movement would go beyond it, cursor remains unchanged.
Examples
state = %{
items: [
%{type: :action, id: :copy},
%{type: :action, id: :paste}
],
cursor: :copy
}
# Move to next item
new_state = Behavior.move_cursor(state, 1)
# new_state.cursor == :paste
# At boundary, cursor stays in place
new_state = Behavior.move_cursor(new_state, 1)
# new_state.cursor == :paste (unchanged)
Selects the item at the current cursor position.
Invokes the on_select callback if the item is selectable (action type and
not disabled), then closes the menu. If the cursor is on a non-selectable
item, no action is taken.
Parameters
state- State map containing::items- List of menu items:cursor- ID of currently focused item:on_select- Callback functionfn id -> ... end(optional)
Returns
Updated state with menu closed (:visible set to false).
Callback Execution
The on_select callback is executed synchronously. Callback exceptions are
rescued, logged, and returned internally as errors without crashing the
caller.
Examples
state = %{
items: [%{type: :action, id: :copy, disabled: false}],
cursor: :copy,
on_select: fn id -> IO.puts("Selected: #{id}") end,
visible: true
}
new_state = Behavior.select_at_cursor(state)
# Prints: "Selected: copy"
# new_state.visible == false
Returns whether an item can be selected.
An item is selectable if it's an action type and not disabled. Separators and disabled action items are not selectable.
Examples
iex> Behavior.selectable?(%{type: :action, disabled: false})
true
iex> Behavior.selectable?(%{type: :action, disabled: true})
false
iex> Behavior.selectable?(%{type: :separator})
false