turbo-vision 1.0.0

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

//! HelpWindow view - window container for displaying context-sensitive help.
// HelpWindow - Help display window
//
// Matches Borland: THelpWindow (help.h)
//
// A window containing a HelpViewer with navigation and topic selection.

use crate::core::geometry::Rect;
use crate::core::event::{Event, EventType, KB_ESC};
use crate::core::state::StateFlags;
use crate::core::command::{CM_CANCEL, CommandId};
use crate::terminal::Terminal;
use super::view::View;
use super::window::Window;
use super::help_viewer::HelpViewer;
use super::help_file::HelpFile;
use std::rc::Rc;
use std::cell::RefCell;

/// Wrapper that allows HelpViewer to be shared between window and HelpWindow
struct SharedHelpViewer(Rc<RefCell<HelpViewer>>);

impl View for SharedHelpViewer {
    fn bounds(&self) -> Rect {
        self.0.borrow().bounds()
    }

    fn set_bounds(&mut self, bounds: Rect) {
        self.0.borrow_mut().set_bounds(bounds);
    }

    fn draw(&mut self, terminal: &mut Terminal) {
        self.0.borrow_mut().draw(terminal);
    }

    fn handle_event(&mut self, event: &mut Event) {
        self.0.borrow_mut().handle_event(event);
    }

    fn can_focus(&self) -> bool {
        self.0.borrow().can_focus()
    }

    fn state(&self) -> StateFlags {
        self.0.borrow().state()
    }

    fn set_state(&mut self, state: StateFlags) {
        self.0.borrow_mut().set_state(state);
    }

    fn get_palette(&self) -> Option<crate::core::palette::Palette> {
        self.0.borrow().get_palette()
    }

    fn get_owner_type(&self) -> super::view::OwnerType {
        self.0.borrow().get_owner_type()
    }

    fn set_owner_type(&mut self, owner_type: super::view::OwnerType) {
        self.0.borrow_mut().set_owner_type(owner_type);
    }
}

/// HelpWindow - Window containing help viewer
///
/// Matches Borland: THelpWindow (parent-child hierarchy)
pub struct HelpWindow {
    window: Window,
    viewer: Rc<RefCell<HelpViewer>>,  // Shared reference for API access
    help_file: Rc<RefCell<HelpFile>>,
    /// Topic history for back/forward navigation
    history: Vec<String>,
    /// Current position in history
    history_pos: usize,
}

impl HelpWindow {
    /// Create a new help window
    ///
    /// Matches Borland: THelpWindow constructor creates TWindow and inserts THelp Viewer as child
    pub fn new(bounds: Rect, title: &str, help_file: Rc<RefCell<HelpFile>>) -> Self {
        let mut window = Window::new(bounds, title);

        // Viewer fills the window interior
        let viewer_bounds = Rect::new(1, 1, bounds.width() - 2, bounds.height() - 2);
        let viewer = Rc::new(RefCell::new(
            HelpViewer::new(viewer_bounds).with_scrollbar()
        ));

        // Insert viewer as a child of window (matches Borland's window->insert(viewer))
        window.add(Box::new(SharedHelpViewer(Rc::clone(&viewer))));

        Self {
            window,
            viewer,
            help_file,
            history: Vec::new(),
            history_pos: 0,
        }
    }

    /// Show a topic by ID
    /// Does not add to history (use switchToTopic for navigation with history)
    pub fn show_topic(&mut self, topic_id: &str) -> bool {
        let help = self.help_file.borrow();
        if let Some(topic) = help.get_topic(topic_id) {
            self.viewer.borrow_mut().set_topic(topic);
            true
        } else {
            false
        }
    }

    /// Show the default topic
    pub fn show_default_topic(&mut self) {
        let help = self.help_file.borrow();
        if let Some(topic) = help.get_default_topic() {
            self.viewer.borrow_mut().set_topic(topic);
        }
    }

    /// Get the current topic ID
    pub fn current_topic(&self) -> Option<String> {
        self.viewer.borrow().current_topic().map(|s| s.to_string())
    }

    /// Get a cloned Rc to the viewer for advanced access
    pub fn viewer_rc(&self) -> Rc<RefCell<HelpViewer>> {
        Rc::clone(&self.viewer)
    }

    /// Get reference to the help file
    pub fn help_file(&self) -> &Rc<RefCell<HelpFile>> {
        &self.help_file
    }

    /// Switch to a topic (with history tracking)
    /// Matches Borland: THelpViewer::switchToTopic()
    /// This is the method to use for hyperlink navigation
    pub fn switch_to_topic(&mut self, topic_id: &str) -> bool {
        // Only proceed if topic exists
        let help = self.help_file.borrow();
        if help.get_topic(topic_id).is_none() {
            return false;
        }
        drop(help);

        // If we're not at the end of history, truncate future history
        if self.history_pos < self.history.len() {
            self.history.truncate(self.history_pos);
        }

        // Add current topic to history before switching
        if let Some(current) = self.viewer.borrow().current_topic() {
            self.history.push(current.to_string());
        }

        // Show the new topic
        let success = self.show_topic(topic_id);
        if success {
            self.history_pos = self.history.len();
        }
        success
    }

    /// Navigate back in history
    /// Returns true if navigation occurred
    pub fn go_back(&mut self) -> bool {
        if self.history_pos > 0 {
            self.history_pos -= 1;
            let topic_id = self.history[self.history_pos].clone();
            self.show_topic(&topic_id);
            true
        } else {
            false
        }
    }

    /// Navigate forward in history
    /// Returns true if navigation occurred
    pub fn go_forward(&mut self) -> bool {
        if self.history_pos < self.history.len() {
            let topic_id = self.history[self.history_pos].clone();
            self.history_pos += 1;
            self.show_topic(&topic_id);
            true
        } else {
            false
        }
    }

    /// Check if we can go back
    pub fn can_go_back(&self) -> bool {
        self.history_pos > 0
    }

    /// Check if we can go forward
    pub fn can_go_forward(&self) -> bool {
        self.history_pos < self.history.len()
    }

    /// Create and show a topic selection dialog
    /// Matches Borland: THelpViewer::makeSelectTopic()
    /// Returns the selected topic ID, or None if cancelled
    pub fn make_select_topic(&self) -> Option<String> {
        // Get all available topics from help file
        let help = self.help_file.borrow();
        let topics = help.get_topic_ids();

        if topics.is_empty() {
            return None;
        }

        // For now, return the first topic as a placeholder
        // TODO: Show a proper selection dialog (ListBox in a Dialog)
        // This would require access to Application to show modal dialog
        Some(topics[0].clone())
    }

    /// Execute the help window modally
    pub fn execute(&mut self, app: &mut crate::app::Application) -> CommandId {
        self.window.execute(app)
    }

    /// End the modal event loop
    pub fn end_modal(&mut self, command: CommandId) {
        self.window.end_modal(command);
    }
}

impl View for HelpWindow {
    fn bounds(&self) -> Rect {
        self.window.bounds()
    }

    fn set_bounds(&mut self, bounds: Rect) {
        self.window.set_bounds(bounds);
        // Update viewer bounds to match window interior
        let viewer_bounds = Rect::new(1, 1, bounds.width() - 2, bounds.height() - 2);
        self.viewer.borrow_mut().set_bounds(viewer_bounds);
    }

    fn draw(&mut self, terminal: &mut Terminal) {
        // Window draws itself and all children (including viewer)
        self.window.draw(terminal);
    }

    fn handle_event(&mut self, event: &mut Event) {
        // ESC closes the help window
        if event.what == EventType::Keyboard && event.key_code == KB_ESC {
            self.window.end_modal(CM_CANCEL);
            event.clear();
            return;
        }

        // Window handles events and dispatches to children (including viewer)
        self.window.handle_event(event);
    }

    fn can_focus(&self) -> bool {
        true
    }

    fn state(&self) -> StateFlags {
        self.window.state()
    }

    fn set_state(&mut self, state: StateFlags) {
        self.window.set_state(state);
        self.viewer.borrow_mut().set_state(state);
    }

    fn get_palette(&self) -> Option<crate::core::palette::Palette> {
        self.window.get_palette()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Write;
    use tempfile::NamedTempFile;

    fn create_test_help_file() -> (NamedTempFile, Rc<RefCell<HelpFile>>) {
        let mut file = NamedTempFile::new().unwrap();
        writeln!(file, "# Test Topic {{#test}}").unwrap();
        writeln!(file, "").unwrap();
        writeln!(file, "This is test content.").unwrap();
        file.flush().unwrap();

        let help = HelpFile::new(file.path().to_str().unwrap()).unwrap();
        (file, Rc::new(RefCell::new(help)))
    }

    #[test]
    fn test_help_window_creation() {
        let (_file, help) = create_test_help_file();
        let bounds = Rect::new(10, 5, 70, 20);
        let window = HelpWindow::new(bounds, "Help", help);

        assert_eq!(window.bounds(), bounds);
    }

    #[test]
    fn test_show_topic() {
        let (_file, help) = create_test_help_file();
        let bounds = Rect::new(10, 5, 70, 20);
        let mut window = HelpWindow::new(bounds, "Help", help);

        assert!(window.show_topic("test"));
        assert_eq!(window.current_topic(), Some("test".to_string()));
    }

    #[test]
    fn test_show_default_topic() {
        let (_file, help) = create_test_help_file();
        let bounds = Rect::new(10, 5, 70, 20);
        let mut window = HelpWindow::new(bounds, "Help", help);

        window.show_default_topic();
        assert_eq!(window.current_topic(), Some("test".to_string()));
    }

    #[test]
    fn test_show_nonexistent_topic() {
        let (_file, help) = create_test_help_file();
        let bounds = Rect::new(10, 5, 70, 20);
        let mut window = HelpWindow::new(bounds, "Help", help);

        assert!(!window.show_topic("nonexistent"));
    }
}

/// Builder for creating help windows with a fluent API.
pub struct HelpWindowBuilder {
    bounds: Option<Rect>,
    title: Option<String>,
    help_file: Option<Rc<RefCell<HelpFile>>>,
}

impl HelpWindowBuilder {
    pub fn new() -> Self {
        Self { bounds: None, title: None, help_file: None }
    }

    #[must_use]
    pub fn bounds(mut self, bounds: Rect) -> Self {
        self.bounds = Some(bounds);
        self
    }

    #[must_use]
    pub fn title(mut self, title: impl Into<String>) -> Self {
        self.title = Some(title.into());
        self
    }

    #[must_use]
    pub fn help_file(mut self, help_file: Rc<RefCell<HelpFile>>) -> Self {
        self.help_file = Some(help_file);
        self
    }

    pub fn build(self) -> HelpWindow {
        let bounds = self.bounds.expect("HelpWindow bounds must be set");
        let title = self.title.expect("HelpWindow title must be set");
        let help_file = self.help_file.expect("HelpWindow help_file must be set");
        HelpWindow::new(bounds, &title, help_file)
    }

    pub fn build_boxed(self) -> Box<HelpWindow> {
        Box::new(self.build())
    }
}

impl Default for HelpWindowBuilder {
    fn default() -> Self {
        Self::new()
    }
}