turbo-vision 0.2.11

A Rust implementation of the classic Borland Turbo Vision text-mode UI framework
Documentation
use crate::core::geometry::Rect;
use crate::core::event::{Event, EventType, KB_F10, KB_ALT_X, KB_ESC_X};
use crate::core::command::{CommandId, CM_QUIT, CM_COMMAND_SET_CHANGED, CM_CANCEL};
use crate::core::command_set;
use crate::terminal::Terminal;
use crate::views::{View, menu_bar::MenuBar, status_line::StatusLine, desktop::Desktop};
use std::time::Duration;

pub struct Application {
    pub terminal: Terminal,
    pub menu_bar: Option<MenuBar>,
    pub status_line: Option<StatusLine>,
    pub desktop: Desktop,
    pub running: bool,
    needs_redraw: bool,  // Track if full redraw is needed
    // Note: Command set is now stored in thread-local static (command_set module)
    // This matches Borland's architecture where TView::curCommandSet is static
}

impl Application {
    pub fn new() -> std::io::Result<Self> {
        let terminal = Terminal::init()?;
        let (width, height) = terminal.size();

        let desktop = Desktop::new(Rect::new(0, 1, width as i16, height as i16 - 1));

        // Initialize global command set
        // Matches Borland's initCommands() (tview.cc:58-68)
        command_set::init_command_set();

        Ok(Self {
            terminal,
            menu_bar: None,
            status_line: None,
            desktop,
            running: false,
            needs_redraw: true,  // Initial draw needed
        })
    }

    pub fn set_menu_bar(&mut self, menu_bar: MenuBar) {
        self.menu_bar = Some(menu_bar);
    }

    pub fn set_status_line(&mut self, status_line: StatusLine) {
        self.status_line = Some(status_line);
    }

    /// Get an event (with drawing)
    /// Matches Borland: TProgram::getEvent() (tprogram.cc:105-174)
    /// This is called by modal views' execute() methods.
    /// It handles idle processing, draws the screen, then polls for an event.
    pub fn get_event(&mut self) -> Option<Event> {
        // Idle processing - broadcast command set changes
        self.idle();

        // Update active view bounds
        self.update_active_view_bounds();

        // Draw everything (this is the key: drawing happens BEFORE getting events)
        // Matches Borland's CLY_Redraw() in getEvent
        self.draw();
        let _ = self.terminal.flush();

        // Poll for event
        self.terminal.poll_event(Duration::from_millis(50)).ok().flatten()
    }

    /// Execute a view (modal or modeless)
    /// Matches Borland: TProgram::execView() (tprogram.cc:177-197)
    ///
    /// If the view has SF_MODAL flag set, runs a modal event loop.
    /// Otherwise, adds the view to the desktop and returns immediately.
    ///
    /// Returns the view's end_state (the command that closed the modal view)
    pub fn exec_view(&mut self, view: Box<dyn View>) -> CommandId {
        use crate::core::state::SF_MODAL;

        // Check if view is modal
        let is_modal = (view.state() & SF_MODAL) != 0;

        // Add view to desktop
        self.desktop.add(view);
        let view_index = self.desktop.child_count() - 1;

        if !is_modal {
            // Modeless view - just add to desktop and return
            return 0;
        }

        // Modal view - run event loop
        // Matches Borland: TProgram::execView() runs modal loop (tprogram.cc:184-194)
        loop {
            // Idle processing (broadcasts command changes, etc.)
            self.idle();

            // Update active view bounds
            self.update_active_view_bounds();

            // Draw everything
            self.draw();
            let _ = self.terminal.flush();

            // Poll for event
            if let Ok(Some(mut event)) = self.terminal.poll_event(Duration::from_millis(50)) {
                // Handle event through normal chain
                self.handle_event(&mut event);
            }

            // Check if the modal view wants to close
            // Matches Borland: TGroup::execute() checks endState (tgroup.cc:192)
            if view_index < self.desktop.child_count() {
                let end_state = self.desktop.child_at(view_index).get_end_state();
                if end_state != 0 {
                    // Modal view wants to close
                    // Remove it from desktop and return the end state
                    self.desktop.remove_child(view_index);
                    return end_state;
                }
            } else {
                // View was removed (closed externally)
                return CM_CANCEL;
            }
        }
    }

    pub fn run(&mut self) {
        self.running = true;

        // Initial draw
        self.update_active_view_bounds();
        self.draw();
        let _ = self.terminal.flush();

        while self.running {
            // Handle events first
            let had_event = if let Ok(Some(mut event)) = self.terminal.poll_event(Duration::from_millis(50)) {
                self.handle_event(&mut event);
                true
            } else {
                false
            };

            // Idle processing - broadcast command set changes
            // Matches Borland: TProgram::idle() called during event loop
            self.idle();

            // Remove closed windows (those with SF_CLOSED flag)
            // In Borland, views call CLY_destroy() to remove themselves
            // In Rust, views set SF_CLOSED and parent removes them
            let had_closed_windows = self.desktop.remove_closed_windows();
            if had_closed_windows {
                self.needs_redraw = true;  // Window removal requires full redraw
            }

            // Check for moved windows and redraw affected areas (Borland's drawUnderRect pattern)
            // Matches Borland: TView::locate() checks for movement and calls drawUnderRect
            // This optimized redraw only redraws the union of old + new position
            let had_moved_windows = self.desktop.handle_moved_windows(&mut self.terminal);

            // Update active view bounds for F11 dumps
            self.update_active_view_bounds();

            // Optimized drawing strategy (matches Borland's approach):
            // - For moved windows: only redraw union rect (already done in handle_moved_windows)
            // - For content changes: full redraw when events occur
            // - No redraw on idle frames (significant performance improvement)
            //
            // This prevents redrawing every frame (60 FPS) when nothing is happening
            // Borland only redraws when views explicitly request it via draw() or on events
            if self.needs_redraw {
                // Explicit redraw requested (window closed, resize, etc.)
                self.draw();
                self.needs_redraw = false;
                let _ = self.terminal.flush();
            } else if had_moved_windows {
                // Window movement: partial redraw already done via draw_under_rect
                // Just flush the terminal buffer
                let _ = self.terminal.flush();
            } else if had_event {
                // Event occurred: do full redraw for content changes
                // This could be optimized further by tracking which views changed
                self.draw();
                let _ = self.terminal.flush();
            }
            // If no event, no movement, no close: no redraw (idle frame)
        }
    }

    fn update_active_view_bounds(&mut self) {
        // The active view is the topmost window on the desktop (last child with shadow)
        // Get the focused child from the desktop
        let child_count = self.desktop.child_count();
        if child_count > 0 {
            let last_child = self.desktop.child_at(child_count - 1);
            self.terminal.set_active_view_bounds(last_child.shadow_bounds());
        } else {
            self.terminal.clear_active_view_bounds();
        }
    }

    pub fn draw(&mut self) {
        // Draw desktop first, then menu bar on top (so dropdown appears over desktop)
        self.desktop.draw(&mut self.terminal);

        if let Some(ref mut menu_bar) = self.menu_bar {
            menu_bar.draw(&mut self.terminal);
        }

        if let Some(ref mut status_line) = self.status_line {
            status_line.draw(&mut self.terminal);
        }

        // Update cursor after drawing all views
        // Desktop contains windows/dialogs with focused controls
        self.desktop.update_cursor(&mut self.terminal);
    }

    pub fn handle_event(&mut self, event: &mut Event) {
        // Menu bar gets first shot
        if let Some(ref mut menu_bar) = self.menu_bar {
            menu_bar.handle_event(event);
            if event.what == EventType::Nothing {
                return;
            }
        }

        // Desktop/windows
        self.desktop.handle_event(event);
        if event.what == EventType::Nothing {
            return;
        }

        // Status line
        if let Some(ref mut status_line) = self.status_line {
            status_line.handle_event(event);
            if event.what == EventType::Nothing {
                return;
            }
        }

        // Application-level command handling
        if event.what == EventType::Command && event.command == CM_QUIT {
            self.running = false;
            event.clear();
        }

        // Handle Ctrl+C, F10, Alt+X, and ESC+X at application level
        if event.what == EventType::Keyboard
            && (event.key_code == 0x0003
                || event.key_code == KB_F10
                || event.key_code == KB_ALT_X
                || event.key_code == KB_ESC_X)
        {
            // Treat these as quit command
            *event = Event::command(CM_QUIT);
            self.running = false;
        }
    }

    // Command Set Management
    // Delegates to global command set functions (command_set module)
    // Matches Borland's TView command set methods (tview.cc:161-389, 672-677)

    /// Check if a command is currently enabled
    /// Matches Borland: TView::commandEnabled(ushort command) (tview.cc:142-147)
    pub fn command_enabled(&self, command: CommandId) -> bool {
        command_set::command_enabled(command)
    }

    /// Enable a single command
    /// Matches Borland: TView::enableCommand(ushort command) (tview.cc:384-389)
    pub fn enable_command(&mut self, command: CommandId) {
        command_set::enable_command(command);
    }

    /// Disable a single command
    /// Matches Borland: TView::disableCommand(ushort command) (tview.cc:161-166)
    pub fn disable_command(&mut self, command: CommandId) {
        command_set::disable_command(command);
    }

    /// Idle processing - broadcasts command set changes
    /// Matches Borland: TProgram::idle() (tprogram.cc:248-257)
    pub fn idle(&mut self) {
        // Check if command set changed and broadcast to all views
        if command_set::command_set_changed() {
            let mut event = Event::broadcast(CM_COMMAND_SET_CHANGED);

            // Broadcast to desktop (which propagates to all children)
            self.desktop.handle_event(&mut event);

            // Also send to menu bar and status line
            if let Some(ref mut menu_bar) = self.menu_bar {
                menu_bar.handle_event(&mut event);
            }
            if let Some(ref mut status_line) = self.status_line {
                status_line.handle_event(&mut event);
            }

            command_set::clear_command_set_changed();
        }
    }
}

impl Drop for Application {
    fn drop(&mut self) {
        let _ = self.terminal.shutdown();
    }
}