turbo-vision 0.9.0

A Rust implementation of the classic Borland Turbo Vision text-mode UI framework
Documentation
// (C) 2025 - Enzo Lombardi

//! ListBox view - scrollable list with single selection support.

use crate::core::geometry::Rect;
use crate::core::event::{Event, EventType, KB_ENTER, MB_LEFT_BUTTON};
use crate::core::palette::colors;
use crate::core::draw::DrawBuffer;
use crate::core::state::StateFlags;
use crate::terminal::Terminal;
use crate::core::command::CommandId;
use super::view::{View, write_line_to_terminal};
use super::list_viewer::{ListViewer, ListViewerState};

/// ListBox - A scrollable list of selectable items
///
/// Now implements ListViewer trait for standard navigation behavior.
/// Matches Borland: TListBox (extends TListViewer)
pub struct ListBox {
    bounds: Rect,
    items: Vec<String>,
    list_state: ListViewerState,  // Embedded state from ListViewer
    state: StateFlags,
    on_select_command: CommandId,
}

impl ListBox {
    /// Create a new list box
    pub fn new(bounds: Rect, on_select_command: CommandId) -> Self {
        Self {
            bounds,
            items: Vec::new(),
            list_state: ListViewerState::new(),
            state: 0,
            on_select_command,
        }
    }

    /// Set the items in the list
    pub fn set_items(&mut self, items: Vec<String>) {
        self.items = items;
        self.list_state.set_range(self.items.len());
    }

    /// Add an item to the list
    pub fn add_item(&mut self, item: String) {
        self.items.push(item);
        self.list_state.set_range(self.items.len());
    }

    /// Clear all items
    pub fn clear(&mut self) {
        self.items.clear();
        self.list_state.set_range(0);
    }

    /// Get the currently selected item index
    pub fn get_selection(&self) -> Option<usize> {
        self.list_state.focused
    }

    /// Get the currently selected item text
    pub fn get_selected_item(&self) -> Option<&str> {
        self.list_state.focused.and_then(|idx| self.items.get(idx).map(|s| s.as_str()))
    }

    /// Set the selected item by index
    pub fn set_selection(&mut self, index: usize) {
        if index < self.items.len() {
            let visible_rows = self.bounds.height() as usize;
            self.list_state.focus_item(index, visible_rows);
        }
    }

    /// Get the number of items
    pub fn item_count(&self) -> usize {
        self.items.len()
    }

    // Convenience methods for compatibility with existing code
    // These delegate to ListViewerState methods

    /// Move selection up (convenience method)
    pub fn select_prev(&mut self) {
        let visible_rows = self.bounds.height() as usize;
        self.list_state.focus_prev(visible_rows);
    }

    /// Move selection down (convenience method)
    pub fn select_next(&mut self) {
        let visible_rows = self.bounds.height() as usize;
        self.list_state.focus_next(visible_rows);
    }

    /// Select first item (convenience method)
    pub fn select_first(&mut self) {
        let visible_rows = self.bounds.height() as usize;
        self.list_state.focus_first(visible_rows);
    }

    /// Select last item (convenience method)
    pub fn select_last(&mut self) {
        let visible_rows = self.bounds.height() as usize;
        self.list_state.focus_last(visible_rows);
    }

    /// Page up (convenience method)
    pub fn page_up(&mut self) {
        let visible_rows = self.bounds.height() as usize;
        self.list_state.focus_page_up(visible_rows);
    }

    /// Page down (convenience method)
    pub fn page_down(&mut self) {
        let visible_rows = self.bounds.height() 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() as usize;
        let height = self.bounds.height() as usize;

        let color_normal = if self.is_focused() {
            colors::LISTBOX_FOCUSED
        } else {
            colors::LISTBOX_NORMAL
        };
        let color_selected = if self.is_focused() {
            colors::LISTBOX_SELECTED_FOCUSED
        } else {
            colors::LISTBOX_SELECTED
        };

        // Draw visible items
        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);

                // Fill rest of line with spaces
                let text_len = text.len();
                if text_len < width {
                    buf.move_char(text_len, ' ', color, width - text_len);
                }
            } else {
                // Empty line
                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) {
        // First try standard list navigation (from ListViewer trait)
        if self.handle_list_event(event) {
            return;
        }

        // Handle ListBox-specific events
        match event.what {
            EventType::Keyboard => {
                if event.key_code == KB_ENTER {
                    // Enter on selected item generates command
                    *event = Event::command(self.on_select_command);
                }
            }
            EventType::MouseDown => {
                let mouse_pos = event.mouse.pos;

                // Check if click is within the listbox bounds
                if self.bounds.contains(mouse_pos) && event.mouse.buttons & MB_LEFT_BUTTON != 0 {
                    // Double-click triggers selection command (matching Borland's TListViewer)
                    if event.mouse.double_click {
                        *event = Event::command(self.on_select_command);
                    }
                    // Single click is handled by handle_list_event above
                }
            }
            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)
    }
}

// Implement ListViewer trait
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()
    }
}

#[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(); // Should stay at 2 (last item)
        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); // Out of bounds, should be ignored
        assert_eq!(listbox.get_selection(), Some(2)); // Should not change
    }

    #[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);
    }
}