uzor 1.3.0

Core UI engine — geometry, interaction, input state
//! Chrome persistent state.
//!
//! All fields are stored flat so the caller holds a single `ChromeState`
//! regardless of which `ChromeRenderKind` is active.

use crate::input::core::coordinator::InputCoordinator;
use crate::layout::docking::DockPanel;
use crate::layout::LayoutManager;
use crate::ui::widgets::atomic::tooltip::TooltipState;
use crate::ui::widgets::composite::context_menu::ContextMenuState;

use super::types::{ChromeColors, ChromeHit};

// ---------------------------------------------------------------------------
// TabState
// ---------------------------------------------------------------------------

/// Per-tab transient interaction state.
#[derive(Debug, Clone, Default)]
pub struct TabState {
    /// Stable string id — must match `ChromeTabConfig::id`.
    pub id: String,
    /// Pointer is currently over the tab body.
    pub hovered: bool,
    /// Tab body is currently pressed (pointer-down).
    pub pressed: bool,
    /// Pointer is over the close-X on this tab.
    pub close_hovered: bool,
}

impl TabState {
    /// Create a new `TabState` for the given tab id.
    pub fn new(id: impl Into<String>) -> Self {
        Self {
            id: id.into(),
            hovered: false,
            pressed: false,
            close_hovered: false,
        }
    }
}

// ---------------------------------------------------------------------------
// ChromeState
// ---------------------------------------------------------------------------

/// All per-chrome-instance persistent state.
#[derive(Debug, Clone, Default)]
pub struct ChromeState {
    // --- Per-frame hit result (set each frame, not persisted across renders) ---

    /// Which zone the pointer is currently over.
    pub hovered: ChromeHit,

    // --- Window state ---

    /// Whether the window is currently maximized.
    pub is_maximized: bool,
    /// Whether a window-drag gesture is in progress (set by parent).
    pub dragging_window: bool,

    // --- Tab state ---

    /// Per-tab hover / press state.  Caller must keep length in sync with
    /// the `tabs` slice passed to `ChromeView`.
    pub tabs_state: Vec<TabState>,
    /// Id of the currently active tab.
    pub active_tab_id: Option<String>,
    /// Pre-computed tab widths (pixels) for the current frame.
    ///
    /// Updated each frame by `update_tab_widths` before registration.
    pub tab_widths: Vec<f64>,

    // --- Overlays ---

    /// Tooltip state for button labels and tab names.
    pub tooltip: TooltipState,
    /// Context-menu state (right-click on tab or button area).
    pub context_menu: ContextMenuState,

    // --- Theme colours ---

    /// Live colour tokens.  Caller may swap these to reflect theme changes.
    pub colors: ChromeColors,

    // --- Legacy field (kept so existing callers compile) ---

    /// Which titlebar button the pointer is over (coarse — use `hovered` for
    /// fine-grained hit data).
    pub chrome_button: Option<super::types::ChromeButton>,

    // --- Accessibility / window manager ---

    /// Window title (not rendered in the strip; passed to the OS title bar).
    pub title: String,
}

impl ChromeState {
    /// Create a new `ChromeState` with default colours.
    pub fn new() -> Self {
        Self::default()
    }

    /// Sync `tabs_state` length to `tab_count`, inserting or removing entries
    /// as needed while preserving existing state for stable tab ids.
    pub fn sync_tabs(&mut self, ids: &[&str]) {
        // Remove entries whose ids no longer appear.
        self.tabs_state.retain(|ts| ids.contains(&ts.id.as_str()));

        // Append new entries for ids that didn't exist.
        for &id in ids {
            if !self.tabs_state.iter().any(|ts| ts.id == id) {
                self.tabs_state.push(TabState::new(id));
            }
        }

        // Reorder to match `ids` order.
        let ordered: Vec<TabState> = ids
            .iter()
            .filter_map(|&id| {
                self.tabs_state.iter().find(|ts| ts.id == id).cloned()
            })
            .collect();
        self.tabs_state = ordered;
    }

    /// Pre-compute tab pixel widths.
    ///
    /// `text_widths` — caller-measured label widths (one per tab).
    /// `tab_padding_h` — horizontal padding per side.
    /// `tab_close_size` — width of the close-X zone.
    pub fn update_tab_widths(
        &mut self,
        text_widths: &[f64],
        tab_padding_h: f64,
        tab_close_size: f64,
    ) {
        self.tab_widths = text_widths
            .iter()
            .map(|&tw| tab_padding_h + tw + tab_close_size + tab_padding_h)
            .collect();
    }

    /// Clear all transient hover/press state (call at end of frame if needed).
    pub fn clear_hover(&mut self) {
        self.hovered = ChromeHit::None;
        self.chrome_button = None;
        for ts in &mut self.tabs_state {
            ts.hovered = false;
            ts.pressed = false;
            ts.close_hovered = false;
        }
    }

    /// Sync per-tab hover state from the input coordinator.
    ///
    /// **Deprecated** — use `sync_hover_from_layout` instead when a
    /// `LayoutManager` is available.  Kept for back-compat with L3 callers that
    /// hold a raw `InputCoordinator` reference.
    ///
    /// The coordinator tracks which registered child widget is hovered; this
    /// method translates coordinator widget-ids of the form
    /// `{chrome_id}:tab:{i}` and `{chrome_id}:tab_close:{i}` into the
    /// corresponding `TabState` hover flags.
    ///
    /// `chrome_id` — the stable id passed to `register_layout_manager_chrome`
    ///               (e.g. `"chrome-widget"`).
    pub fn sync_hover_from_coordinator(&mut self, coord: &InputCoordinator, chrome_id: &str) {
        let hovered = coord.hovered_widget().map(|w| w.0.clone());
        self.apply_hover_from_id(hovered, chrome_id);
    }

    /// Sync per-tab hover state from the `LayoutManager` (L3 authoritative hover).
    ///
    /// Preferred over `sync_hover_from_coordinator` — reads from
    /// `LayoutManager::hovered_widget()` which is kept current by
    /// `on_pointer_move` and is not reset by `begin_frame`.
    ///
    /// `chrome_id` — the stable id passed to `register_layout_manager_chrome`.
    pub fn sync_hover_from_layout<P: DockPanel>(&mut self, layout: &LayoutManager<P>, chrome_id: &str) {
        let hovered = layout.hovered_widget().map(|w| w.0.clone());
        self.apply_hover_from_id(hovered, chrome_id);
    }

    fn apply_hover_from_id(&mut self, hovered: Option<String>, chrome_id: &str) {
        use super::types::ChromeHit;
        let tab_prefix   = format!("{chrome_id}:tab:");
        let close_prefix = format!("{chrome_id}:tab_close:");

        for (i, ts) in self.tabs_state.iter_mut().enumerate() {
            let idx_str = i.to_string();
            ts.hovered       = hovered.as_deref().map(|h| h == format!("{}{}", tab_prefix, idx_str)).unwrap_or(false);
            ts.close_hovered = hovered.as_deref().map(|h| h == format!("{}{}", close_prefix, idx_str)).unwrap_or(false);
        }

        // Right-side action buttons: update the coarse `hovered: ChromeHit`
        // field so render can highlight the focused button.
        self.hovered = match hovered.as_deref() {
            Some(h) if h == format!("{chrome_id}:new_win")    => ChromeHit::NewWindowBtn,
            Some(h) if h == format!("{chrome_id}:menu")       => ChromeHit::Menu,
            Some(h) if h == format!("{chrome_id}:close_win")  => ChromeHit::CloseWindowBtn,
            Some(h) if h == format!("{chrome_id}:min")        => ChromeHit::MinBtn,
            Some(h) if h == format!("{chrome_id}:max")        => ChromeHit::MaxBtn,
            Some(h) if h == format!("{chrome_id}:close")      => ChromeHit::CloseBtn,
            _ => ChromeHit::None,
        };
    }
}