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}