Skip to main content

par_term/tab_bar_ui/
state.rs

1//! `TabBarUI` struct definition and constructor.
2
3use crate::tab::TabId;
4use crate::ui_constants::TAB_SPACING;
5use winit::event::MouseScrollDelta;
6
7/// Tab bar UI state
8pub struct TabBarUI {
9    /// Currently hovered tab ID
10    pub hovered_tab: Option<TabId>,
11    /// Tab where close button is hovered
12    pub close_hovered: Option<TabId>,
13    /// Whether a drag is in progress
14    pub(super) drag_in_progress: bool,
15    /// Tab being dragged
16    pub(super) dragging_tab: Option<TabId>,
17    /// Cached title of the tab being dragged (for ghost rendering)
18    pub(super) dragging_title: String,
19    /// Cached color of the tab being dragged
20    pub(super) dragging_color: Option<[u8; 3]>,
21    /// Width of the tab being dragged (for ghost rendering)
22    pub(super) dragging_tab_width: f32,
23    /// Visual indicator for where the dragged tab would be inserted
24    pub(super) drop_target_index: Option<usize>,
25    /// Per-frame cache of tab rects for drop target calculation
26    pub(super) tab_rects: Vec<(TabId, egui::Rect)>,
27    /// Tab ID for which context menu is open
28    pub(super) context_menu_tab: Option<TabId>,
29    /// Position where context menu was opened
30    pub(super) context_menu_pos: egui::Pos2,
31    /// Frame when context menu was opened (to avoid closing on same frame)
32    pub(super) context_menu_opened_frame: u64,
33    /// Color being edited in the color picker (for the context menu)
34    pub(super) editing_color: [u8; 3],
35    /// Whether the rename text field is active in the context menu
36    pub(super) renaming_tab: bool,
37    /// Frame when rename mode was activated (to ignore the activating click)
38    pub(super) rename_activated_frame: u64,
39    /// Buffer for the rename text field
40    pub(super) rename_buffer: String,
41    /// Title of the tab in the context menu (for rename pre-fill)
42    pub(super) context_menu_title: String,
43    /// Whether the icon picker is active in the context menu
44    pub(super) picking_icon: bool,
45    /// Frame when icon picker mode was activated (to ignore the activating click)
46    pub(super) icon_activated_frame: u64,
47    /// Buffer for the icon text field in the context menu
48    pub(super) icon_buffer: String,
49    /// Current custom icon of the tab in the context menu (for "Clear Icon" visibility)
50    pub(super) context_menu_icon: Option<String>,
51    /// Horizontal scroll offset for tabs (in pixels)
52    pub(super) scroll_offset: f32,
53    /// Whether the horizontal tab bar needs scroll (more tabs than fit).
54    /// Set each frame by `render_horizontal`.
55    pub(super) needs_horizontal_scroll: bool,
56    /// Whether the new-tab profile popup is open
57    pub show_new_tab_profile_menu: bool,
58    /// Set per-frame: candidate destination windows for the "Move Tab to Window →" submenu.
59    /// Each entry is `(WindowId, display_label)` (e.g., `"Window 2 — vim"`).
60    pub(crate) move_candidates: Vec<(winit::window::WindowId, String)>,
61    /// Set per-frame: true if the current window has an active tmux gateway.
62    /// When true, the Move Tab menu entries are disabled for every tab.
63    pub(crate) move_gateway_active: bool,
64    /// Set per-frame: number of tabs in the source window. Used to disable
65    /// "Move Tab to New Window" when `== 1` (solo-tab guard).
66    pub(crate) move_source_tab_count: usize,
67    /// Set per-frame: true when the context-menu tab has multiple panes.
68    pub(crate) tab_has_multiple_panes: bool,
69    /// The in-app menu, drawn in the tab bar strip on platforms that cannot
70    /// attach a native menu bar. Inert elsewhere — see [`crate::menu::AppMenuUi`].
71    pub(super) app_menu: crate::menu::AppMenuUi,
72}
73
74impl TabBarUI {
75    /// Create a new tab bar UI
76    pub fn new() -> Self {
77        Self {
78            hovered_tab: None,
79            close_hovered: None,
80            drag_in_progress: false,
81            dragging_tab: None,
82            dragging_title: String::new(),
83            dragging_color: None,
84            dragging_tab_width: 0.0,
85            drop_target_index: None,
86            tab_rects: Vec::new(),
87            context_menu_tab: None,
88            context_menu_pos: egui::Pos2::ZERO,
89            context_menu_opened_frame: 0,
90            editing_color: [100, 100, 100],
91            renaming_tab: false,
92            rename_activated_frame: 0,
93            rename_buffer: String::new(),
94            context_menu_title: String::new(),
95            picking_icon: false,
96            icon_activated_frame: 0,
97            icon_buffer: String::new(),
98            context_menu_icon: None,
99            scroll_offset: 0.0,
100            needs_horizontal_scroll: false,
101            show_new_tab_profile_menu: false,
102            move_candidates: Vec::new(),
103            move_gateway_active: false,
104            move_source_tab_count: 0,
105            tab_has_multiple_panes: false,
106            app_menu: crate::menu::AppMenuUi::new(),
107        }
108    }
109}
110
111impl Default for TabBarUI {
112    fn default() -> Self {
113        Self::new()
114    }
115}
116
117impl TabBarUI {
118    /// Handle mouse wheel when hovering over the horizontal tab bar.
119    /// Converts vertical scroll delta to horizontal tab scrolling.
120    /// Returns `true` if the event was consumed.
121    pub fn handle_mouse_wheel(
122        &mut self,
123        delta: &MouseScrollDelta,
124        tab_min_width: f32,
125        tab_count: usize,
126    ) -> bool {
127        if !self.needs_horizontal_scroll || tab_count == 0 {
128            return false;
129        }
130
131        // Convert vertical wheel delta to horizontal scroll.
132        // Positive y = scroll up = reveal tabs to the left (decrease offset).
133        let scroll_amount = match delta {
134            MouseScrollDelta::LineDelta(_x, y) => *y * (tab_min_width + TAB_SPACING),
135            MouseScrollDelta::PixelDelta(pos) => pos.y as f32,
136        };
137
138        if scroll_amount.abs() < 0.5 {
139            return false;
140        }
141
142        // Invert: scroll-up (positive y) reveals tabs to the left (decrease offset).
143        self.scroll_offset = (self.scroll_offset - scroll_amount).max(0.0);
144        true
145    }
146}