///|
/// ActionMenu - Reusable action menu with key bindings
///|
/// A single action item in the menu
pub(all) struct ActionItem {
key : Char
label : String
handler : () -> Unit
}
///|
/// Create an action item
pub fn ActionItem::new(
key : Char,
label : String,
handler : () -> Unit,
) -> ActionItem {
{ key, label, handler }
}
///|
/// Action menu state and configuration
pub(all) struct ActionMenu {
/// Modal state for visibility
modal : ModalState
/// Title of the menu
title : String
/// List of actions
actions : Array[ActionItem]
/// Key to toggle the menu (default: None, must be handled externally)
toggle_key : Char?
/// Callback when menu closes
on_close : () -> Unit
}
///|
/// Create a new action menu
pub fn ActionMenu::new(
title : String,
actions : Array[ActionItem],
toggle_key? : Char? = None,
on_close? : () -> Unit = fn() { },
) -> ActionMenu {
{ modal: ModalState::new(), title, actions, toggle_key, on_close }
}
///|
/// Open the menu
pub fn ActionMenu::open(self : ActionMenu) -> Unit {
self.modal.open()
}
///|
/// Close the menu
pub fn ActionMenu::close(self : ActionMenu) -> Unit {
self.modal.close()
(self.on_close)()
}
///|
/// Check if menu is open
pub fn ActionMenu::is_open(self : ActionMenu) -> Bool {
self.modal.get_is_open()
}
///|
/// Handle a key event when menu is open
/// Returns true if the key was handled, false otherwise
pub fn ActionMenu::handle_key(self : ActionMenu, key : Char) -> Bool {
// Check toggle key
match self.toggle_key {
Some(toggle) =>
if key == toggle || key == toggle_case(toggle) {
self.close()
return true
}
None => ()
}
// Check action keys
for action in self.actions {
if key == action.key || key == toggle_case(action.key) {
(action.handler)()
self.close()
return true
}
}
false
}
///|
/// Handle escape key (closes menu)
pub fn ActionMenu::handle_escape(self : ActionMenu) -> Bool {
if self.is_open() {
self.close()
true
} else {
false
}
}
///|
/// Build the menu component
pub fn ActionMenu::build(
self : ActionMenu,
container_width : Int,
container_height : Int,
) -> @core.Component {
let items : Array[@core.Component] = []
// Title
items.push(text(self.title, fg=@core.Color::cyan(), bold=true))
items.push(@core.spacer(flex_grow=0.5))
// Action items
for action in self.actions {
let label = "[" + action.key.to_string() + "] " + action.label
items.push(text(label))
}
items.push(@core.spacer(flex_grow=1.0))
// Close hint
let close_hint = match self.toggle_key {
Some(k) => "[Esc/" + k.to_string() + "] Close"
None => "[Esc] Close"
}
items.push(text(close_hint, fg=@core.Color::rgb(100, 100, 100)))
let content = column(items, gap=0.0, min_width=@types.Dimension::Length(25.0))
modal_with_backdrop(
content,
container_width=@types.Dimension::Length(container_width.to_double()),
container_height=@types.Dimension::Length(container_height.to_double()),
)
}
///|
fn toggle_case(c : Char) -> Char {
let code = c.to_int()
if code >= 65 && code <= 90 {
// A-Z -> a-z
(code + 32).unsafe_to_char()
} else if code >= 97 && code <= 122 {
// a-z -> A-Z
(code - 32).unsafe_to_char()
} else {
c
}
}
///|
/// Result of handling a key event with action menu
pub(all) enum MenuKeyResult {
/// Key was handled by the menu
Handled
/// Key was not handled, pass to normal handler
NotHandled
/// Menu was closed
Closed
}
///|
/// Handle key event for action menu with automatic App buffer management
/// Call this at the start of your key handler
/// Returns MenuKeyResult indicating how the event was processed
pub fn ActionMenu::handle_event(
self : ActionMenu,
app : @render.App,
event : @events.InputEvent,
) -> MenuKeyResult {
if not(self.is_open()) {
return MenuKeyResult::NotHandled
}
match event {
@events.InputEvent::Key(key_event) =>
match key_event {
@events.KeyEvent::Special(@events.SpecialKey::Escape, _) => {
self.close()
app.clear_prev_buffer()
MenuKeyResult::Closed
}
@events.KeyEvent::Char(ch, _) => {
let handled = self.handle_key(ch)
if handled {
app.clear_prev_buffer()
MenuKeyResult::Closed
} else {
MenuKeyResult::Handled // Still consume key when menu is open
}
}
_ => MenuKeyResult::Handled // Consume other keys when menu is open
}
_ => MenuKeyResult::NotHandled // Let mouse events through
}
}
///|
/// Open the menu and clear app buffer for clean render
pub fn ActionMenu::open_with_app(self : ActionMenu, app : @render.App) -> Unit {
self.open()
app.clear_prev_buffer()
}