rioterm 0.3.8

Rio terminal is a hardware-accelerated GPU terminal emulator, focusing to run in desktops and browsers.
use rio_backend::ansi::graphics::{KittyPlacement, StoredImage, VirtualPlacement};
use rio_backend::config::colors::term::TermColors;
use rio_backend::config::CursorConfig;
use rio_backend::crosswords::grid::row::Row;
use rio_backend::crosswords::pos::CursorState;
use rio_backend::crosswords::square::Square;
use rio_backend::event::TerminalDamage;
use rio_backend::selection::SelectionRange;
use rustc_hash::FxHashMap;
use std::time::Instant;

/// UI-level damage tracking for non-terminal elements
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct UIDamage {
    /// Island (tab bar with progress) needs redraw
    pub island: bool,
    /// Search bar needs redraw
    pub search: bool,
}

impl UIDamage {
    /// Merge two UI damages
    pub fn merge(self, other: Self) -> Self {
        Self {
            island: self.island || other.island,
            search: self.search || other.search,
        }
    }
}

#[derive(Clone, Copy, Debug)]
pub enum BackgroundState {
    Set(wgpu::Color),
    Reset,
}

#[derive(Clone, Copy, Debug)]
pub enum WindowUpdate {
    Background(BackgroundState),
}

#[derive(Default, Clone, Debug)]
pub struct Cursor {
    pub state: CursorState,
    pub content: char,
    pub content_ref: char,
    pub is_ime_enabled: bool,
}

/// Hint label information for rendering
#[derive(Clone, Debug)]
#[allow(dead_code)]
pub struct HintLabel {
    pub position: rio_backend::crosswords::pos::Pos,
    pub label: Vec<char>,
    pub is_first: bool,
}

#[derive(Default)]
pub struct RenderableContent {
    // TODO: Should not use default
    pub cursor: Cursor,
    pub has_blinking_enabled: bool,
    pub is_blinking_cursor_visible: bool,
    pub selection_range: Option<SelectionRange>,
    pub hyperlink_range: Option<SelectionRange>,
    pub hint_labels: Vec<HintLabel>,
    pub highlighted_hint: Option<crate::hints::HintMatch>,
    pub hint_matches: Option<Vec<rio_backend::crosswords::search::Match>>,
    pub last_typing: Option<Instant>,
    pub last_blink_toggle: Option<Instant>,
    pub pending_update: PendingUpdate,
    pub background: Option<BackgroundState>,
}

impl RenderableContent {
    pub fn new(cursor: Cursor) -> Self {
        RenderableContent {
            cursor,
            has_blinking_enabled: false,
            selection_range: None,
            hint_labels: Vec::new(),
            highlighted_hint: None,
            hint_matches: None,
            last_typing: None,
            last_blink_toggle: None,
            hyperlink_range: None,
            pending_update: PendingUpdate::default(),
            is_blinking_cursor_visible: false,
            background: None,
        }
    }

    pub fn from_cursor_config(config_cursor: &CursorConfig) -> Self {
        let cursor = Cursor {
            content: config_cursor.shape.into(),
            content_ref: config_cursor.shape.into(),
            state: CursorState::new(config_cursor.shape.into()),
            is_ime_enabled: false,
        };
        Self::new(cursor)
    }
}

#[derive(Debug, Clone, PartialEq)]
pub struct TerminalSnapshot {
    pub colors: TermColors,
    pub display_offset: usize,
    pub blinking_cursor: bool,
    pub visible_rows: Vec<Row<Square>>,
    /// Snapshot of the grid's per-cell style intern table. Cloned cheaply
    /// (the table is just a `Vec<Style>` plus an `FxHashMap`); the renderer
    /// dereferences cell `style_id`s through this clone instead of reaching
    /// into the live grid.
    pub style_set: rio_backend::crosswords::style::StyleSet,
    /// Snapshot of the grid's extras table (zero-width chars, hyperlinks,
    /// sixel/iterm graphics). The renderer reads per-cell graphic data
    /// through this clone.
    pub extras_table: rio_backend::crosswords::grid::ExtrasTable,
    pub cursor: CursorState,
    pub damage: TerminalDamage,
    // Cache terminal dimensions to avoid repeated calls
    pub columns: usize,
    pub screen_lines: usize,
    pub history_size: usize,
    // Kitty graphics virtual placements
    pub kitty_virtual_placements: FxHashMap<(u32, u32), VirtualPlacement>,
    // Kitty graphics stored images
    pub kitty_images: FxHashMap<u32, StoredImage>,
    // Kitty graphics overlay placements (sorted by z_index for layered rendering)
    pub kitty_placements: Vec<KittyPlacement>,
    // Whether kitty graphics state changed since last frame
    pub kitty_graphics_dirty: bool,
}

#[derive(Debug, Default)]
pub struct PendingUpdate {
    /// Whether there's any pending update that needs rendering
    dirty: bool,
    /// Terminal content damage (lines, text)
    terminal_damage: Option<TerminalDamage>,
    /// UI element damage (island, search bar, etc.)
    ui_damage: UIDamage,
}

impl PendingUpdate {
    /// Check if there's a pending update
    #[inline]
    pub fn is_dirty(&self) -> bool {
        self.dirty
    }

    /// Mark as needing to check for damage on next render
    pub fn set_dirty(&mut self) {
        self.dirty = true;
    }

    /// Mark terminal content as damaged
    pub fn set_terminal_damage(&mut self, damage: TerminalDamage) {
        self.dirty = true;
        self.terminal_damage = Some(match self.terminal_damage.take() {
            None => damage,
            Some(existing) => Self::merge_terminal_damages(existing, damage),
        });
    }

    /// Mark UI elements as damaged
    pub fn set_ui_damage(&mut self, damage: UIDamage) {
        self.dirty = true;
        self.ui_damage = self.ui_damage.merge(damage);
    }

    /// Get and clear terminal damage
    pub fn take_terminal_damage(&mut self) -> Option<TerminalDamage> {
        self.terminal_damage.take()
    }

    /// Get and clear UI damage
    pub fn take_ui_damage(&mut self) -> UIDamage {
        std::mem::take(&mut self.ui_damage)
    }

    /// Reset the dirty flag after rendering
    pub fn reset(&mut self) {
        self.dirty = false;
        // Note: damages are cleared by take_*_damage during render
    }

    /// Merge two terminal damages into one
    pub fn merge_terminal_damages(
        existing: TerminalDamage,
        new: TerminalDamage,
    ) -> TerminalDamage {
        match (existing, new) {
            // Any damage + Full = Full
            (_, TerminalDamage::Full) | (TerminalDamage::Full, _) => TerminalDamage::Full,
            // Partial damages: merge the line lists
            (TerminalDamage::Partial(mut lines1), TerminalDamage::Partial(lines2)) => {
                lines1.extend(lines2);
                TerminalDamage::Partial(lines1)
            }
            // CursorOnly damages need special handling
            (TerminalDamage::CursorOnly, TerminalDamage::Partial(lines))
            | (TerminalDamage::Partial(lines), TerminalDamage::CursorOnly) => {
                TerminalDamage::Partial(lines)
            }
            (TerminalDamage::CursorOnly, TerminalDamage::CursorOnly) => {
                TerminalDamage::CursorOnly
            }
            // Noop + anything = the other thing
            (TerminalDamage::Noop, other) | (other, TerminalDamage::Noop) => other,
        }
    }
}