1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
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>)>,
/// Whether to show the separator line below the title bar (default: true).
pub show_bottom_border: bool,
}
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,
show_bottom_border: options.show_bottom_border.unwrap_or(true),
};
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,
}