use super::list_viewer::{ListViewer, ListViewerState};
use super::view::{write_line_to_terminal, View};
use crate::core::command::CommandId;
use crate::core::draw::DrawBuffer;
use crate::core::event::{Event, EventType, KB_ENTER, MB_LEFT_BUTTON};
use crate::core::geometry::Rect;
use crate::core::palette::{LISTBOX_FOCUSED, LISTBOX_NORMAL, LISTBOX_SELECTED};
use crate::core::state::StateFlags;
use crate::terminal::Terminal;
pub struct ListBox {
bounds: Rect,
items: Vec<String>,
list_state: ListViewerState, state: StateFlags,
on_select_command: CommandId,
palette_chain: Option<crate::core::palette_chain::PaletteChainNode>,
}
impl ListBox {
pub fn new(bounds: Rect, on_select_command: CommandId) -> Self {
Self {
bounds,
items: Vec::new(),
list_state: ListViewerState::new(),
state: 0,
on_select_command,
palette_chain: None,
}
}
pub fn set_items(&mut self, items: Vec<String>) {
self.items = items;
self.list_state.set_range(self.items.len());
}
pub fn add_item(&mut self, item: String) {
self.items.push(item);
self.list_state.set_range(self.items.len());
}
pub fn clear(&mut self) {
self.items.clear();
self.list_state.set_range(0);
}
pub fn get_selection(&self) -> Option<usize> {
self.list_state.focused
}
pub fn get_selected_item(&self) -> Option<&str> {
self.list_state
.focused
.and_then(|idx| self.items.get(idx).map(|s| s.as_str()))
}
pub fn set_selection(&mut self, index: usize) {
if index < self.items.len() {
let visible_rows = self.bounds.height_clamped() as usize;
self.list_state.focus_item(index, visible_rows);
}
}
pub fn item_count(&self) -> usize {
self.items.len()
}
pub fn select_prev(&mut self) {
let visible_rows = self.bounds.height_clamped() as usize;
self.list_state.focus_prev(visible_rows);
}
pub fn select_next(&mut self) {
let visible_rows = self.bounds.height_clamped() as usize;
self.list_state.focus_next(visible_rows);
}
pub fn select_first(&mut self) {
let visible_rows = self.bounds.height_clamped() as usize;
self.list_state.focus_first(visible_rows);
}
pub fn select_last(&mut self) {
let visible_rows = self.bounds.height_clamped() as usize;
self.list_state.focus_last(visible_rows);
}
pub fn page_up(&mut self) {
let visible_rows = self.bounds.height_clamped() as usize;
self.list_state.focus_page_up(visible_rows);
}
pub fn page_down(&mut self) {
let visible_rows = self.bounds.height_clamped() as usize;
self.list_state.focus_page_down(visible_rows);
}
}
impl View for ListBox {
fn bounds(&self) -> Rect {
self.bounds
}
fn set_bounds(&mut self, bounds: Rect) {
self.bounds = bounds;
}
fn draw(&mut self, terminal: &mut Terminal) {
let width = self.bounds.width_clamped() as usize;
let height = self.bounds.height_clamped() as usize;
let color_normal = if self.is_focused() {
self.map_color(LISTBOX_FOCUSED) } else {
self.map_color(LISTBOX_NORMAL) };
let color_selected = self.map_color(LISTBOX_SELECTED);
for i in 0..height {
let mut buf = DrawBuffer::new(width);
let item_idx = self.list_state.top_item + i;
if item_idx < self.items.len() {
let is_selected = Some(item_idx) == self.list_state.focused;
let color = if is_selected {
color_selected
} else {
color_normal
};
let text = &self.items[item_idx];
buf.move_str(0, text, color);
let text_len = text.len();
if text_len < width {
buf.move_char(text_len, ' ', color, width - text_len);
}
} else {
buf.move_char(0, ' ', color_normal, width);
}
write_line_to_terminal(terminal, self.bounds.a.x, self.bounds.a.y + i as i16, &buf);
}
}
fn handle_event(&mut self, event: &mut Event) {
if event.what == EventType::MouseDown {
let mouse_pos = event.mouse.pos;
if self.bounds.contains(mouse_pos) && event.mouse.buttons & MB_LEFT_BUTTON != 0 {
if event.mouse.double_click {
let relative_y = (mouse_pos.y - self.bounds.a.y) as usize;
let clicked_item = self.list_state.top_item + relative_y;
if clicked_item < self.items.len() {
let visible_rows = self.bounds.height_clamped() as usize;
self.list_state.focus_item(clicked_item, visible_rows);
}
*event = Event::command(self.on_select_command);
return;
}
}
}
if self.handle_list_event(event) {
return;
}
match event.what {
EventType::Keyboard => {
if event.key_code == KB_ENTER {
*event = Event::command(self.on_select_command);
}
}
EventType::MouseDown => {
}
EventType::MouseWheelUp => {
let mouse_pos = event.mouse.pos;
if self.bounds.contains(mouse_pos) {
self.select_prev();
event.clear();
}
}
EventType::MouseWheelDown => {
let mouse_pos = event.mouse.pos;
if self.bounds.contains(mouse_pos) {
self.select_next();
event.clear();
}
}
_ => {}
}
}
fn can_focus(&self) -> bool {
true
}
fn state(&self) -> StateFlags {
self.state
}
fn set_state(&mut self, state: StateFlags) {
self.state = state;
}
fn set_list_selection(&mut self, index: usize) {
self.set_selection(index);
}
fn get_list_selection(&self) -> usize {
self.list_state.focused.unwrap_or(0)
}
fn set_palette_chain(&mut self, node: Option<crate::core::palette_chain::PaletteChainNode>) {
self.palette_chain = node;
}
fn get_palette_chain(&self) -> Option<&crate::core::palette_chain::PaletteChainNode> {
self.palette_chain.as_ref()
}
fn get_palette(&self) -> Option<crate::core::palette::Palette> {
use crate::core::palette::{palettes, Palette};
Some(Palette::from_slice(palettes::CP_LISTBOX))
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}
impl ListViewer for ListBox {
fn list_state(&self) -> &ListViewerState {
&self.list_state
}
fn list_state_mut(&mut self) -> &mut ListViewerState {
&mut self.list_state
}
fn get_text(&self, item: usize, _max_len: usize) -> String {
self.items.get(item).cloned().unwrap_or_default()
}
}
pub struct ListBoxBuilder {
bounds: Option<Rect>,
on_select_command: CommandId,
}
impl ListBoxBuilder {
pub fn new() -> Self {
Self { bounds: None, on_select_command: 0 }
}
#[must_use]
pub fn bounds(mut self, bounds: Rect) -> Self {
self.bounds = Some(bounds);
self
}
#[must_use]
pub fn on_select_command(mut self, command: CommandId) -> Self {
self.on_select_command = command;
self
}
pub fn build(self) -> ListBox {
let bounds = self.bounds.expect("ListBox bounds must be set");
ListBox::new(bounds, self.on_select_command)
}
pub fn build_boxed(self) -> Box<ListBox> {
Box::new(self.build())
}
}
impl Default for ListBoxBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_listbox_creation() {
let listbox = ListBox::new(Rect::new(0, 0, 20, 10), 1000);
assert_eq!(listbox.item_count(), 0);
assert_eq!(listbox.get_selection(), None);
}
#[test]
fn test_listbox_add_items() {
let mut listbox = ListBox::new(Rect::new(0, 0, 20, 10), 1000);
listbox.add_item("Item 1".to_string());
listbox.add_item("Item 2".to_string());
listbox.add_item("Item 3".to_string());
assert_eq!(listbox.item_count(), 3);
assert_eq!(listbox.get_selection(), Some(0));
assert_eq!(listbox.get_selected_item(), Some("Item 1"));
}
#[test]
fn test_listbox_set_items() {
let mut listbox = ListBox::new(Rect::new(0, 0, 20, 10), 1000);
let items = vec!["Alpha".to_string(), "Beta".to_string(), "Gamma".to_string()];
listbox.set_items(items);
assert_eq!(listbox.item_count(), 3);
assert_eq!(listbox.get_selection(), Some(0));
}
#[test]
fn test_listbox_navigation() {
let mut listbox = ListBox::new(Rect::new(0, 0, 20, 10), 1000);
listbox.set_items(vec![
"Item 1".to_string(),
"Item 2".to_string(),
"Item 3".to_string(),
]);
assert_eq!(listbox.get_selection(), Some(0));
listbox.select_next();
assert_eq!(listbox.get_selection(), Some(1));
listbox.select_next();
assert_eq!(listbox.get_selection(), Some(2));
listbox.select_next(); assert_eq!(listbox.get_selection(), Some(2));
listbox.select_prev();
assert_eq!(listbox.get_selection(), Some(1));
listbox.select_first();
assert_eq!(listbox.get_selection(), Some(0));
listbox.select_last();
assert_eq!(listbox.get_selection(), Some(2));
}
#[test]
fn test_listbox_set_selection() {
let mut listbox = ListBox::new(Rect::new(0, 0, 20, 10), 1000);
listbox.set_items(vec![
"A".to_string(),
"B".to_string(),
"C".to_string(),
"D".to_string(),
]);
listbox.set_selection(2);
assert_eq!(listbox.get_selection(), Some(2));
assert_eq!(listbox.get_selected_item(), Some("C"));
listbox.set_selection(10); assert_eq!(listbox.get_selection(), Some(2)); }
#[test]
fn test_listbox_clear() {
let mut listbox = ListBox::new(Rect::new(0, 0, 20, 10), 1000);
listbox.set_items(vec!["Item 1".to_string(), "Item 2".to_string()]);
assert_eq!(listbox.item_count(), 2);
listbox.clear();
assert_eq!(listbox.item_count(), 0);
assert_eq!(listbox.get_selection(), None);
}
}