egui-desktop 0.2.4

Cross-platform GUI for egui applications
Documentation
use egui::{Color32, FontId, Id, ImageSource, Painter, Rect, Ui};

use crate::{
    KeyboardShortcut, ThemeProvider, TitleBarOptions,
    menu::{
        core::states::{nav_state::KeyboardState, render_state::RenderState},
        items::MenuItem,
    },
    theme::{ThemeMode, TitleBarTheme, detect_system_dark_mode},
    titlebar::options::HamburgerStyle,
};

/// Custom icon for the title bar
pub enum CustomIcon {
    /// SVG/PNG/JPEG image icon
    Image(ImageSource<'static>),
    /// Custom drawing function
    Drawn(Box<dyn Fn(&Painter, Rect, Color32) + Send + Sync>),
    /// Animated icon with framework-managed animation state and context
    Animated(
        Box<dyn Fn(&Painter, Rect, Color32, &mut IconAnimationState, AnimationCtx) + Send + Sync>,
    ),
    /// Animated icon that renders using Ui primitives instead of Painter
    AnimatedUi(
        Box<dyn Fn(&mut Ui, Rect, Color32, &mut IconAnimationState, AnimationCtx) + Send + Sync>,
    ),
}

/// Configuration for a custom icon button (internal use only).
pub struct CustomIconButton {
    /// Icon kind to render.
    pub icon: CustomIcon,
    /// Optional tooltip displayed on hover.
    pub tooltip: Option<String>,
    /// Override hover background color.
    pub hover_color: Option<Color32>,
    /// Override icon color.
    pub icon_color: Option<Color32>,
    /// Optional click callback.
    pub callback: Option<Box<dyn Fn() + Send + Sync>>,
    /// Optional keyboard shortcut for this icon.
    pub shortcut: Option<KeyboardShortcut>,
}

/// Title bar state and configuration.
pub struct TitleBar {
    /// Optional title text.
    pub title: Option<String>,
    /// Unique egui id for interactions.
    pub id: Id,
    /// Background color of the bar.
    pub background_color: Color32,
    /// Hover color for window controls.
    pub hover_color: Color32,
    /// Hover color for the close button.
    pub close_hover_color: Color32,
    /// Close icon color.
    pub close_icon_color: Color32,
    /// Maximize icon color.
    pub maximize_icon_color: Color32,
    /// Restore icon color.
    pub restore_icon_color: Color32,
    /// Minimize icon color.
    pub minimize_icon_color: Color32,
    /// Simple menu items (label, optional callback).
    pub menu_items: Vec<(String, Option<Box<dyn Fn() + Send + Sync>>)>,
    /// Menus with submenus.
    pub menu_items_with_submenus: Vec<MenuItem>,
    /// Menu order tracking - preserves chronological addition order
    /// Stores (is_submenu, index) where is_submenu = true for submenu items, false for simple items
    pub menu_order: Vec<(bool, usize)>,
    /// Index of currently open submenu.
    pub open_submenu: Option<usize>,
    /// Time when submenu was opened.
    pub submenu_open_time: Option<f64>,
    /// Guard to prevent immediate close after open in same frame.
    pub submenu_just_opened_frame: bool,
    /// Flag to track if submenu was opened from hamburger menu
    pub submenu_from_hamburger: bool,
    /// Time when hamburger menu was opened (to prevent immediate closure)
    pub hamburger_open_time: Option<f64>,
    /// Last click time used for overlay logic.
    pub last_click_time: f64,
    /// Monotonic id of last click used to open submenu.
    pub last_click_id: usize,
    /// Cached x positions for submenu alignment.
    pub menu_positions: Vec<f32>,
    /// Custom icon buttons shown on the right.
    pub custom_icons: Vec<CustomIconButton>,
    /// Optional app icon displayed next to the title (Windows/Linux).
    pub app_icon: Option<ImageSource<'static>>,
    /// Title text color.
    pub title_color: Color32,
    /// Title font size in points.
    pub title_font_size: f32,
    /// Selected theme mode for rendering.
    pub theme_mode: ThemeMode,
    /// Whether to display title on macOS.
    pub show_title_on_macos: bool,
    /// Whether to display title on Windows.
    pub show_title_on_windows: bool,
    /// Whether to display title on Linux.
    pub show_title_on_linux: bool,
    // Keyboard navigation state
    /// Whether keyboard navigation is active.
    pub keyboard_navigation_active: bool,
    /// Currently selected top-level menu index.
    pub selected_menu_index: Option<usize>,
    /// Time of last keyboard navigation.
    pub last_keyboard_nav_time: f64,
    /// Menu text color.
    pub menu_text_color: Color32,
    /// Menu text size in points.
    pub menu_text_size: f32,
    /// Menu hover background color.
    pub menu_hover_color: Color32,
    /// Keyboard selection highlight color.
    pub keyboard_selection_color: Color32,
    // Submenu colors
    /// Submenu background color.
    pub submenu_background_color: Color32,
    /// Submenu text color.
    pub submenu_text_color: Color32,
    /// Submenu text size in points.
    pub submenu_text_size: f32,
    /// Submenu hover background color.
    pub submenu_hover_color: Color32,
    /// Submenu disabled item color.
    pub submenu_disabled_color: Color32,
    /// Submenu shortcut text color.
    pub submenu_shortcut_color: Color32,
    /// Submenu border color.
    pub submenu_border_color: Color32,
    /// Submenu keyboard selection highlight color.
    pub submenu_keyboard_selection_color: Color32,
    // Optional external theme provider
    /// Optional external theme provider.
    pub theme_provider: Option<Box<dyn ThemeProvider + Send + Sync>>,
    /// Current theme id, if any.
    pub current_theme_id: Option<String>,
    // Control button visibility
    /// Whether to show the close button.
    pub show_close_button: bool,
    /// Whether to show the maximize button.
    pub show_maximize_button: bool,
    /// Whether to show the minimize button.
    pub show_minimize_button: bool,
    // Per custom icon animation states (kept aligned with custom_icons)
    /// Per-icon animation state list aligned with `custom_icons`.
    pub icon_animation_states: Vec<IconAnimationState>,
    /// Spacing between custom icons in pixels.
    pub icon_spacing: f32,
    // Responsive menu behavior
    /// Whether hamburger menu is currently open.
    pub hamburger_menu_open: bool,
    /// Hamburger menu animation style.
    pub hamburger_style: HamburgerStyle,
    /// Hamburger menu animation state.
    pub hamburger_animation_state: IconAnimationState,
    /// Actual X position of hamburger overlay after adjustment (for submenu positioning).
    pub hamburger_overlay_x: Option<f32>,
    /// Track which items are currently visible (fitted in available space).
    pub items_fitted: Vec<usize>,
    /// Track if dots are selected by keyboard navigation.
    pub dots_selected: bool,
    /// Track selected item index in hamburger/dots overlay.
    pub overlay_selected_index: Option<usize>,
    /// Force close overlay on next render frame.
    pub force_close_overlay: bool,
    /// Recursive navigation state for unlimited submenu levels
    pub recursive_state: KeyboardState,
    /// Recursive render state for unlimited submenu levels (replaces legacy variables)
    pub render_state: RenderState,
    /// Deferred menu leaf action (menu_index, path) so callback runs after releasing menu borrow
    pub pending_menu_leaf_action: Option<(usize, Vec<usize>)>,
}

impl TitleBar {
    /// Create a new title bar with options
    ///
    /// # Examples
    ///
    /// ```rust
    /// // Simple title bar with title
    /// TitleBar::new(TitleBarOptions::new().with_title("My App"))
    ///
    /// // Icon-only title bar
    /// TitleBar::new(TitleBarOptions::new())
    ///
    /// // Custom themed title bar
    /// TitleBar::new(
    ///     TitleBarOptions::new()
    ///         .with_title("My App")
    ///         .with_theme_mode(ThemeMode::Dark)
    ///         .with_background_color(Color32::from_rgb(30, 30, 30))
    /// )
    /// ```
    pub fn new(options: TitleBarOptions) -> Self {
        let theme = match options.theme_mode {
            ThemeMode::Light => TitleBarTheme::light(),
            ThemeMode::Dark => TitleBarTheme::dark(),
            ThemeMode::System => {
                if detect_system_dark_mode() {
                    TitleBarTheme::dark()
                } else {
                    TitleBarTheme::light()
                }
            }
        };

        let title_bar = Self {
            title: options.title,
            id: Id::new("title_bar"),
            background_color: options.background_color.unwrap_or(theme.background_color),
            hover_color: options.hover_color.unwrap_or(theme.hover_color),
            close_hover_color: options.close_hover_color.unwrap_or(theme.close_hover_color),
            close_icon_color: options.close_icon_color.unwrap_or(theme.close_icon_color),
            maximize_icon_color: options
                .maximize_icon_color
                .unwrap_or(theme.maximize_icon_color),
            restore_icon_color: options
                .restore_icon_color
                .unwrap_or(theme.restore_icon_color),
            minimize_icon_color: options
                .minimize_icon_color
                .unwrap_or(theme.minimize_icon_color),
            menu_items: Vec::new(),
            menu_items_with_submenus: Vec::new(),
            menu_order: Vec::new(),
            open_submenu: None,
            submenu_open_time: None,
            submenu_just_opened_frame: false,
            submenu_from_hamburger: false,
            hamburger_open_time: None,
            last_click_time: 0.0,
            last_click_id: 0,
            menu_positions: Vec::new(),
            custom_icons: Vec::new(),
            app_icon: options.app_icon,
            // Initialize keyboard navigation state
            keyboard_navigation_active: false,
            selected_menu_index: None,
            last_keyboard_nav_time: 0.0,
            // Track which items are currently visible (fitted in available space)
            items_fitted: Vec::new(),
            // Track if dots are selected by keyboard navigation
            dots_selected: false,
            // Track selected item index in hamburger/dots overlay
            overlay_selected_index: None,
            title_color: options.title_color.unwrap_or(theme.title_color),
            title_font_size: options.title_font_size.unwrap_or(12.0),
            theme_mode: options.theme_mode,
            show_title_on_macos: options.show_title_on_macos,
            show_title_on_windows: options.show_title_on_windows,
            show_title_on_linux: options.show_title_on_linux,
            menu_text_color: options.menu_text_color.unwrap_or(theme.menu_text_color),
            menu_text_size: options.menu_text_size.unwrap_or(theme.menu_text_size),
            menu_hover_color: options.menu_hover_color.unwrap_or(theme.menu_hover_color),
            keyboard_selection_color: options
                .keyboard_selection_color
                .unwrap_or(theme.keyboard_selection_color),
            // Submenu colors
            submenu_background_color: theme.submenu_background_color,
            submenu_text_color: theme.submenu_text_color,
            submenu_text_size: theme.submenu_text_size,
            submenu_hover_color: theme.submenu_hover_color,
            submenu_disabled_color: theme.submenu_disabled_color,
            submenu_shortcut_color: theme.submenu_shortcut_color,
            submenu_border_color: theme.submenu_border_color,
            submenu_keyboard_selection_color: theme.submenu_keyboard_selection_color,
            // Theme provider
            theme_provider: None,
            current_theme_id: None,
            // Control button visibility (default to true if not specified)
            show_close_button: options.show_close_button.unwrap_or(true),
            show_maximize_button: options.show_maximize_button.unwrap_or(true),
            show_minimize_button: options.show_minimize_button.unwrap_or(true),
            icon_animation_states: Vec::new(),
            icon_spacing: options.icon_spacing.unwrap_or(4.0),
            // Responsive menu state (automatic behavior)
            hamburger_menu_open: false,
            hamburger_style: options.hamburger_style,
            hamburger_animation_state: IconAnimationState::default(),
            hamburger_overlay_x: None,
            force_close_overlay: false,
            // Initialize recursive navigation state
            recursive_state: KeyboardState::new(),
            render_state: RenderState::new(),
            pending_menu_leaf_action: None,
        };

        title_bar
    }

    /// Calculate the total width taken by control buttons and custom icons
    pub fn calculate_control_buttons_width(&self) -> f32 {
        let mut total_width = 0.0;

        // Native control buttons (platform-specific)
        #[cfg(target_os = "macos")]
        {
            // macOS has different control buttons layout (left side)
            total_width += 0.0; // No buttons on macOS
        }

        #[cfg(not(target_os = "macos"))]
        {
            // Windows/Linux: close, minimize, maximize buttons
            // Each button has 46px hover zone (from desired_size = Vec2::new(46.0, 32.0))
            // Total: 46 + 46 + 46 = 138px for 3 buttons
            if self.show_close_button {
                total_width += 46.0; // button hover zone
            }
            if self.show_minimize_button {
                total_width += 46.0; // button hover zone
            }
            if self.show_maximize_button {
                total_width += 46.0; // button hover zone
            }
        }

        // Custom icons (from render_custom_icons in api.rs)
        if !self.custom_icons.is_empty() {
            let icon_size = 16.0; // From: let icon_size = 16.0;
            let spacing = self.icon_spacing; // From: let spacing = self.icon_spacing;
            let extra_spacing = 16.0; // From: let extra_spacing = 16.0;

            // Formula from: total_width = self.custom_icons.len() as f32 * (icon_size + spacing) - spacing + extra_spacing;
            total_width +=
                self.custom_icons.len() as f32 * (icon_size + spacing) - spacing + extra_spacing;
        }

        total_width
    }

    /// Calculate the total width needed for all menu items
    pub fn calculate_menu_width(&self, ui: &mut Ui) -> f32 {
        let mut total_width = 0.0;

        // Calculate width for simple menu items
        for (label, _) in &self.menu_items {
            let label_width = ui.fonts_mut(|f| {
                f.layout_no_wrap(
                    label.clone(),
                    FontId::proportional(self.menu_text_size),
                    self.menu_text_color,
                )
                .size()
                .x
            }) + 16.0; // Add padding
            total_width += label_width;
        }

        // Calculate width for submenu items
        for menu_item in &self.menu_items_with_submenus {
            let label_width = ui.fonts_mut(|f| {
                f.layout_no_wrap(
                    menu_item.label.clone(),
                    FontId::proportional(self.menu_text_size),
                    self.menu_text_color,
                )
                .size()
                .x
            }) + 16.0; // Add padding
            total_width += label_width;
        }

        total_width
    }
}

/// Public animation context passed to animated icon callbacks.
#[derive(Clone, Copy)]
pub struct AnimationCtx {
    /// Absolute time in seconds.
    pub time: f64,
    /// Delta time since last frame in seconds.
    pub delta_seconds: f32,
    /// Whether the icon is hovered this frame.
    pub hovered: bool,
    /// Whether the icon is pressed this frame.
    pub pressed: bool,
}

/// Per-icon animation state managed by the framework
#[derive(Clone, Copy, Default)]
pub struct IconAnimationState {
    /// 0..1 smooth hover progress
    pub hover_t: f32,
    /// 0..1 smooth press progress
    pub press_t: f32,
    /// Last absolute time seen by this icon
    pub last_time: f64,
    /// Generic 0..1 progress you can drive from the callback
    pub progress: f32,
}