Compare commits
2 Commits
e578aadd61
...
e500914fa0
| Author | SHA1 | Date |
|---|---|---|
|
|
e500914fa0 | |
|
|
a36fae7f8a |
|
|
@ -757,6 +757,46 @@ impl MenuSystem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Measure the minimum width needed for a menu's contents.
|
||||||
|
/// Accounts for label text + gap + shortcut text + padding.
|
||||||
|
fn measure_menu_width(ui: &egui::Ui, children: &[MenuDef], keymap: Option<&crate::keymap::KeymapManager>) -> f32 {
|
||||||
|
let label_font = egui::FontId::proportional(14.0);
|
||||||
|
let shortcut_font = egui::FontId::proportional(12.0);
|
||||||
|
let gap = 24.0; // space between label and shortcut
|
||||||
|
let padding = 16.0; // left + right padding
|
||||||
|
|
||||||
|
let mut max_width: f32 = 0.0;
|
||||||
|
for child in children {
|
||||||
|
match child {
|
||||||
|
MenuDef::Item(item_def) => {
|
||||||
|
let label_width = ui.fonts_mut(|f| f.layout_no_wrap(item_def.label.to_string(), label_font.clone(), egui::Color32::WHITE).size().x);
|
||||||
|
let effective_shortcut = if let Some(km) = keymap {
|
||||||
|
if let Ok(app_action) = crate::keymap::AppAction::try_from(item_def.action) {
|
||||||
|
km.get(app_action)
|
||||||
|
} else {
|
||||||
|
item_def.shortcut
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
item_def.shortcut
|
||||||
|
};
|
||||||
|
let shortcut_width = if let Some(shortcut) = &effective_shortcut {
|
||||||
|
let text = Self::format_shortcut(shortcut);
|
||||||
|
ui.fonts_mut(|f| f.layout_no_wrap(text, shortcut_font.clone(), egui::Color32::WHITE).size().x) + gap
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
max_width = max_width.max(label_width + shortcut_width);
|
||||||
|
}
|
||||||
|
MenuDef::Submenu { label, .. } => {
|
||||||
|
let label_width = ui.fonts_mut(|f| f.layout_no_wrap(label.to_string(), label_font.clone(), egui::Color32::WHITE).size().x);
|
||||||
|
max_width = max_width.max(label_width + 20.0); // extra space for submenu arrow
|
||||||
|
}
|
||||||
|
MenuDef::Separator => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
max_width + padding
|
||||||
|
}
|
||||||
|
|
||||||
/// Render egui menu bar from the same menu structure (for Linux/Windows)
|
/// Render egui menu bar from the same menu structure (for Linux/Windows)
|
||||||
pub fn render_egui_menu_bar(
|
pub fn render_egui_menu_bar(
|
||||||
&self,
|
&self,
|
||||||
|
|
@ -767,13 +807,60 @@ impl MenuSystem {
|
||||||
current_layout_index: usize,
|
current_layout_index: usize,
|
||||||
) -> Option<MenuAction> {
|
) -> Option<MenuAction> {
|
||||||
let mut action = None;
|
let mut action = None;
|
||||||
|
let ctx = ui.ctx().clone();
|
||||||
|
let menus = MenuItemDef::menu_structure();
|
||||||
|
|
||||||
egui::MenuBar::new().ui(ui, |ui| {
|
egui::MenuBar::new().ui(ui, |ui| {
|
||||||
for menu_def in MenuItemDef::menu_structure() {
|
// Phase 1: render all top-level buttons and collect responses.
|
||||||
if let Some(a) = self.render_menu_def(ui, menu_def, recent_files, keymap, layout_names, current_layout_index) {
|
// For non-submenu items (separators, bare actions), render them inline.
|
||||||
|
let mut button_entries: Vec<(egui::Response, egui::Id, &MenuDef)> = Vec::new();
|
||||||
|
for menu_def in menus {
|
||||||
|
if let MenuDef::Submenu { label, .. } = menu_def {
|
||||||
|
let response = ui.button(*label);
|
||||||
|
let popup_id = egui::Popup::default_response_id(&response);
|
||||||
|
button_entries.push((response, popup_id, menu_def));
|
||||||
|
} else if let Some(a) = self.render_menu_def(ui, menu_def, recent_files, keymap, layout_names, current_layout_index) {
|
||||||
action = Some(a);
|
action = Some(a);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 2: hover-to-switch between top-level menus.
|
||||||
|
// If one of our menu popups is open and the user hovers a different button, switch.
|
||||||
|
let any_ours_open = button_entries.iter().any(|(_, pid, _)| egui::Popup::is_id_open(&ctx, *pid));
|
||||||
|
if any_ours_open {
|
||||||
|
for (response, popup_id, _) in &button_entries {
|
||||||
|
if response.hovered() && !egui::Popup::is_id_open(&ctx, *popup_id) {
|
||||||
|
// open_id closes all other popups and opens this one
|
||||||
|
egui::Popup::open_id(&ctx, *popup_id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 3: show popups via standard Popup::menu.
|
||||||
|
// Popup::menu sets UiKind::Menu, Frame::popup, menu_style, and MenuState::mark_shown,
|
||||||
|
// so SubMenuButton works correctly for nested submenus.
|
||||||
|
for (response, _, menu_def) in button_entries {
|
||||||
|
if let MenuDef::Submenu { children, .. } = menu_def {
|
||||||
|
let popup_result = egui::Popup::menu(&response).show(|ui| {
|
||||||
|
let min_width = Self::measure_menu_width(ui, children, keymap);
|
||||||
|
ui.set_width(min_width);
|
||||||
|
let mut a = None;
|
||||||
|
for child in *children {
|
||||||
|
if let Some(result) = self.render_menu_def(ui, child, recent_files, keymap, layout_names, current_layout_index) {
|
||||||
|
a = Some(result);
|
||||||
|
ui.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a
|
||||||
|
});
|
||||||
|
if let Some(r) = popup_result {
|
||||||
|
if let Some(a) = r.inner {
|
||||||
|
action = Some(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
action
|
action
|
||||||
|
|
@ -802,65 +889,63 @@ impl MenuSystem {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
MenuDef::Submenu { label, children } => {
|
MenuDef::Submenu { label, children } => {
|
||||||
let mut action = None;
|
let (_, popup) = egui::containers::menu::SubMenuButton::new(*label)
|
||||||
ui.menu_button(*label, |ui| {
|
.ui(ui, |ui| {
|
||||||
if *label == "Open Recent" {
|
if *label == "Open Recent" {
|
||||||
// Special handling for "Open Recent" submenu
|
let mut action = None;
|
||||||
for (index, path) in recent_files.iter().enumerate() {
|
for (index, path) in recent_files.iter().enumerate() {
|
||||||
let display_name = path
|
let display_name = path
|
||||||
.file_name()
|
.file_name()
|
||||||
.and_then(|s| s.to_str())
|
.and_then(|s| s.to_str())
|
||||||
.unwrap_or("Unknown");
|
.unwrap_or("Unknown");
|
||||||
|
if ui.button(display_name).clicked() {
|
||||||
if ui.button(display_name).clicked() {
|
action = Some(MenuAction::OpenRecent(index));
|
||||||
action = Some(MenuAction::OpenRecent(index));
|
|
||||||
ui.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !recent_files.is_empty() {
|
|
||||||
ui.separator();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ui.button("Clear Recent Files").clicked() {
|
|
||||||
action = Some(MenuAction::ClearRecentFiles);
|
|
||||||
ui.close();
|
|
||||||
}
|
|
||||||
} else if *label == "Layout" {
|
|
||||||
// Render static items first (Next/Previous Layout)
|
|
||||||
for child in *children {
|
|
||||||
if let Some(a) = self.render_menu_def(ui, child, recent_files, keymap, layout_names, current_layout_index) {
|
|
||||||
action = Some(a);
|
|
||||||
ui.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dynamic layout list
|
|
||||||
if !layout_names.is_empty() {
|
|
||||||
ui.separator();
|
|
||||||
for (index, name) in layout_names.iter().enumerate() {
|
|
||||||
let label = if index == current_layout_index {
|
|
||||||
format!("* {}", name)
|
|
||||||
} else {
|
|
||||||
name.clone()
|
|
||||||
};
|
|
||||||
if ui.button(label).clicked() {
|
|
||||||
action = Some(MenuAction::SwitchLayout(index));
|
|
||||||
ui.close();
|
ui.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if !recent_files.is_empty() {
|
||||||
} else {
|
ui.separator();
|
||||||
// Normal submenu rendering
|
}
|
||||||
for child in *children {
|
if ui.button("Clear Recent Files").clicked() {
|
||||||
if let Some(a) = self.render_menu_def(ui, child, recent_files, keymap, layout_names, current_layout_index) {
|
action = Some(MenuAction::ClearRecentFiles);
|
||||||
action = Some(a);
|
|
||||||
ui.close();
|
ui.close();
|
||||||
}
|
}
|
||||||
|
action
|
||||||
|
} else if *label == "Layout" {
|
||||||
|
let mut action = None;
|
||||||
|
for child in *children {
|
||||||
|
if let Some(a) = self.render_menu_def(ui, child, recent_files, keymap, layout_names, current_layout_index) {
|
||||||
|
action = Some(a);
|
||||||
|
ui.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !layout_names.is_empty() {
|
||||||
|
ui.separator();
|
||||||
|
for (index, name) in layout_names.iter().enumerate() {
|
||||||
|
let entry = if index == current_layout_index {
|
||||||
|
format!("* {}", name)
|
||||||
|
} else {
|
||||||
|
name.clone()
|
||||||
|
};
|
||||||
|
if ui.button(entry).clicked() {
|
||||||
|
action = Some(MenuAction::SwitchLayout(index));
|
||||||
|
ui.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
action
|
||||||
|
} else {
|
||||||
|
let mut action = None;
|
||||||
|
for child in *children {
|
||||||
|
if let Some(a) = self.render_menu_def(ui, child, recent_files, keymap, layout_names, current_layout_index) {
|
||||||
|
action = Some(a);
|
||||||
|
ui.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
action
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
popup.and_then(|r| r.inner)
|
||||||
action
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -883,9 +968,6 @@ impl MenuSystem {
|
||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set minimum width for menu items to prevent cramping
|
|
||||||
ui.set_min_width(180.0);
|
|
||||||
|
|
||||||
let desired_width = ui.available_width();
|
let desired_width = ui.available_width();
|
||||||
let (rect, response) = ui.allocate_exact_size(
|
let (rect, response) = ui.allocate_exact_size(
|
||||||
egui::vec2(desired_width, ui.spacing().interact_size.y),
|
egui::vec2(desired_width, ui.spacing().interact_size.y),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue