///|
/// Listbox - Selection list component (single/multi select)
///|
/// Listbox option
pub(all) struct ListboxOption {
id : String
label : String
disabled : Bool
} derive(Eq)
///|
/// Create a listbox option
pub fn listbox_option(
id : String,
label : String,
disabled? : Bool = false,
) -> ListboxOption {
{ id, label, disabled }
}
///|
/// Listbox orientation
pub(all) enum ListboxOrientation {
Vertical
Horizontal
}
///|
/// Listbox state
pub(all) struct ListboxState {
focused_id : String
selected_ids : Array[String]
anchor_id : String // for shift+click range selection
}
///|
/// Create initial listbox state
pub fn ListboxState::new(
initial_selected? : Array[String] = [],
) -> ListboxState {
{ focused_id: "", selected_ids: initial_selected, anchor_id: "" }
}
///|
/// Create a single-select listbox with theme support
pub fn listbox(
options : Array[ListboxOption],
selected_id : String,
id? : String = "",
focused_id? : String = "",
orientation? : ListboxOrientation = ListboxOrientation::Vertical,
theme? : Theme = Theme::default(),
// Size
min_width? : Double = 20.0,
max_height? : Int = 10,
padding_x? : Double = 1.0,
) -> @core.Component {
let state : ListboxState = {
focused_id,
selected_ids: [selected_id],
anchor_id: selected_id,
}
build_listbox(
options, state, false, orientation, id, theme, min_width, max_height, padding_x,
)
}
///|
/// Create a multi-select listbox with theme support
pub fn listbox_multi(
options : Array[ListboxOption],
state : ListboxState,
id? : String = "",
orientation? : ListboxOrientation = ListboxOrientation::Vertical,
theme? : Theme = Theme::default(),
// Size
min_width? : Double = 20.0,
max_height? : Int = 10,
padding_x? : Double = 1.0,
) -> @core.Component {
build_listbox(
options, state, true, orientation, id, theme, min_width, max_height, padding_x,
)
}
///|
fn build_listbox(
options : Array[ListboxOption],
state : ListboxState,
multiselectable : Bool,
orientation : ListboxOrientation,
id : String,
theme : Theme,
min_width : Double,
max_height : Int,
padding_x : Double,
) -> @core.Component {
let items : Array[@core.Component] = []
let visible_count = if options.length() > max_height {
max_height
} else {
options.length()
}
for i = 0; i < visible_count; i = i + 1 {
let opt = options[i]
let is_selected = state.selected_ids.contains(opt.id)
let is_focused = state.focused_id == opt.id
// Determine colors - no background, use text color
let item_fg = if opt.disabled {
theme.disabled
} else if is_focused {
theme.focus
} else if is_selected {
theme.primary
} else {
theme.fg
}
// Build option item
let label_text = if multiselectable {
// Show checkbox for multi-select
let check_icon = if is_selected { "[✓]" } else { "[ ]" }
check_icon + " " + opt.label
} else {
opt.label
}
// Selection and focus indicators
let prefix = if is_focused {
"▸ "
} else if is_selected {
"● "
} else {
" "
}
// Text with padding
let padded_text = text(
" ".repeat(padding_x.to_int()) + prefix + label_text,
fg=item_fg,
)
let item = row(
[padded_text],
id=opt.id,
height=@types.Dimension::Length(1.0),
role=Some(@core.Role::Option),
)
items.push(item)
}
// Add scroll indicator if needed
if options.length() > max_height {
let remaining = options.length() - max_height
let scroll_text = text(
" ".repeat(padding_x.to_int()) +
" ... " +
remaining.to_string() +
" more",
fg=theme.disabled,
)
items.push(row([scroll_text], height=@types.Dimension::Length(1.0)))
}
// Build container based on orientation
match orientation {
ListboxOrientation::Vertical =>
column(
items,
id~,
min_width=@types.Dimension::Length(min_width),
// Auto-size to content
border=Some(@core.BorderChars::single()),
border_color=theme.border,
role=Some(@core.Role::Listbox),
)
ListboxOrientation::Horizontal =>
row(
items,
id~,
min_width=@types.Dimension::Length(min_width),
border=Some(@core.BorderChars::single()),
border_color=theme.border,
role=Some(@core.Role::Listbox),
)
}
}
///|
/// Simple option list without border (for embedding)
pub fn option_list(
options : Array[ListboxOption],
selected_ids : Array[String],
focused_id? : String = "",
multiselectable? : Bool = false,
theme? : Theme = Theme::default(),
) -> @core.Component {
let items : Array[@core.Component] = []
for opt in options {
let is_selected = selected_ids.contains(opt.id)
let is_focused = focused_id == opt.id
let item_fg = if opt.disabled {
theme.disabled
} else if is_focused {
theme.focus
} else if is_selected {
theme.primary
} else {
theme.fg
}
let label_text = if multiselectable {
let check = if is_selected { "[✓]" } else { "[ ]" }
check + " " + opt.label
} else {
opt.label
}
let focus_prefix = if is_focused { "▸ " } else { " " }
items.push(
row(
[text(focus_prefix + label_text, fg=item_fg)],
id=opt.id,
role=Some(@core.Role::Option),
),
)
}
column(items, role=Some(@core.Role::Listbox))
}
///|
/// Inline selectable list (no box styling)
pub fn selectable_list(
items_data : Array[(String, String)], // (id, label)
selected_id : String,
focused_id? : String = "",
theme? : Theme = Theme::default(),
) -> @core.Component {
let options = items_data.map(fn(item) {
{ id: item.0, label: item.1, disabled: false }
})
option_list(options, [selected_id], focused_id~, theme~)
}