Add app menu
This commit is contained in:
parent
48da21e062
commit
652b9e6cbb
|
|
@ -20,6 +20,15 @@ fn main() -> eframe::Result {
|
|||
println!(" - {}: {}", layout.name, layout.description);
|
||||
}
|
||||
|
||||
// Initialize native menus for macOS (app-wide, doesn't need window)
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
if let Ok(menu_system) = MenuSystem::new() {
|
||||
menu_system.init_for_macos();
|
||||
println!("✅ Native macOS menus initialized");
|
||||
}
|
||||
}
|
||||
|
||||
let options = eframe::NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default()
|
||||
.with_inner_size([1920.0, 1080.0])
|
||||
|
|
@ -180,8 +189,7 @@ struct EditorApp {
|
|||
fill_color: egui::Color32, // Fill color for drawing
|
||||
stroke_color: egui::Color32, // Stroke color for drawing
|
||||
pane_instances: HashMap<NodePath, PaneInstance>, // Pane instances per path
|
||||
menu_system: Option<MenuSystem>, // Native menu system
|
||||
menu_initialized: bool, // Track if menu has been initialized with window
|
||||
menu_system: Option<MenuSystem>, // Native menu system for event checking
|
||||
}
|
||||
|
||||
impl EditorApp {
|
||||
|
|
@ -206,7 +214,6 @@ impl EditorApp {
|
|||
stroke_color: egui::Color32::from_rgb(0, 0, 0), // Default black stroke
|
||||
pane_instances: HashMap::new(), // Initialize empty, panes created on-demand
|
||||
menu_system,
|
||||
menu_initialized: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -462,40 +469,26 @@ impl EditorApp {
|
|||
|
||||
impl eframe::App for EditorApp {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
// Check for native menu events
|
||||
// Check for native menu events (macOS)
|
||||
if let Some(menu_system) = &self.menu_system {
|
||||
if let Some(action) = menu_system.check_events() {
|
||||
self.handle_menu_action(action);
|
||||
}
|
||||
}
|
||||
|
||||
// Top menu bar
|
||||
let mut layout_to_switch: Option<usize> = None;
|
||||
let current_name = self.current_layout_def().name.clone();
|
||||
|
||||
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
|
||||
egui::menu::bar(ui, |ui| {
|
||||
ui.menu_button("Layout", |ui| {
|
||||
for (i, layout) in self.layouts.iter().enumerate() {
|
||||
if ui
|
||||
.selectable_label(i == self.current_layout_index, &layout.name)
|
||||
.clicked()
|
||||
{
|
||||
layout_to_switch = Some(i);
|
||||
ui.close_menu();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
ui.label(format!("Current: {}", current_name));
|
||||
});
|
||||
// Check keyboard shortcuts (works on all platforms)
|
||||
ctx.input(|i| {
|
||||
if let Some(action) = MenuSystem::check_shortcuts(i) {
|
||||
self.handle_menu_action(action);
|
||||
}
|
||||
});
|
||||
|
||||
// Switch layout after menu closes to avoid borrow issues
|
||||
if let Some(index) = layout_to_switch {
|
||||
self.switch_layout(index);
|
||||
}
|
||||
// Top menu bar (egui-rendered on all platforms)
|
||||
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
|
||||
if let Some(action) = MenuSystem::render_egui_menu_bar(ui) {
|
||||
self.handle_menu_action(action);
|
||||
}
|
||||
});
|
||||
|
||||
// Main pane area
|
||||
let mut layout_action: Option<LayoutAction> = None;
|
||||
|
|
|
|||
|
|
@ -2,12 +2,133 @@
|
|||
///
|
||||
/// This module creates the native menu bar with all menu items matching
|
||||
/// the JavaScript version's menu structure.
|
||||
///
|
||||
/// Menu definitions are centralized to allow generating both native menus
|
||||
/// and keyboard shortcut handlers from a single source.
|
||||
|
||||
use eframe::egui;
|
||||
use muda::{
|
||||
accelerator::{Accelerator, Code, Modifiers},
|
||||
Menu, MenuItem, PredefinedMenuItem, Submenu,
|
||||
};
|
||||
|
||||
/// Keyboard shortcut definition
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Shortcut {
|
||||
pub key: ShortcutKey,
|
||||
pub ctrl: bool,
|
||||
pub shift: bool,
|
||||
pub alt: bool,
|
||||
}
|
||||
|
||||
/// Keys that can be used in shortcuts
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ShortcutKey {
|
||||
// Letters
|
||||
A, C, E, G, I, K, L, N, O, Q, S, V, W, X, Z,
|
||||
// Numbers
|
||||
Num0,
|
||||
// Symbols
|
||||
Comma, Minus, Equals, Plus,
|
||||
BracketLeft, BracketRight,
|
||||
// Special
|
||||
Delete,
|
||||
}
|
||||
|
||||
impl Shortcut {
|
||||
pub const fn new(key: ShortcutKey, ctrl: bool, shift: bool, alt: bool) -> Self {
|
||||
Self { key, ctrl, shift, alt }
|
||||
}
|
||||
|
||||
/// Convert to muda Accelerator
|
||||
pub fn to_muda_accelerator(&self) -> Accelerator {
|
||||
let mut modifiers = Modifiers::empty();
|
||||
if self.ctrl {
|
||||
#[cfg(target_os = "macos")]
|
||||
{ modifiers |= Modifiers::META; }
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{ modifiers |= Modifiers::CONTROL; }
|
||||
}
|
||||
if self.shift {
|
||||
modifiers |= Modifiers::SHIFT;
|
||||
}
|
||||
if self.alt {
|
||||
modifiers |= Modifiers::ALT;
|
||||
}
|
||||
|
||||
let code = match self.key {
|
||||
ShortcutKey::A => Code::KeyA,
|
||||
ShortcutKey::C => Code::KeyC,
|
||||
ShortcutKey::E => Code::KeyE,
|
||||
ShortcutKey::G => Code::KeyG,
|
||||
ShortcutKey::I => Code::KeyI,
|
||||
ShortcutKey::K => Code::KeyK,
|
||||
ShortcutKey::L => Code::KeyL,
|
||||
ShortcutKey::N => Code::KeyN,
|
||||
ShortcutKey::O => Code::KeyO,
|
||||
ShortcutKey::Q => Code::KeyQ,
|
||||
ShortcutKey::S => Code::KeyS,
|
||||
ShortcutKey::V => Code::KeyV,
|
||||
ShortcutKey::W => Code::KeyW,
|
||||
ShortcutKey::X => Code::KeyX,
|
||||
ShortcutKey::Z => Code::KeyZ,
|
||||
ShortcutKey::Num0 => Code::Digit0,
|
||||
ShortcutKey::Comma => Code::Comma,
|
||||
ShortcutKey::Minus => Code::Minus,
|
||||
ShortcutKey::Equals => Code::Equal,
|
||||
ShortcutKey::Plus => Code::Equal, // Same key as equals
|
||||
ShortcutKey::BracketLeft => Code::BracketLeft,
|
||||
ShortcutKey::BracketRight => Code::BracketRight,
|
||||
ShortcutKey::Delete => Code::Delete,
|
||||
};
|
||||
|
||||
Accelerator::new(if modifiers.is_empty() { None } else { Some(modifiers) }, code)
|
||||
}
|
||||
|
||||
/// Check if this shortcut matches the current egui input state
|
||||
pub fn matches_egui_input(&self, input: &egui::InputState) -> bool {
|
||||
// Check modifiers first
|
||||
if self.ctrl != input.modifiers.ctrl {
|
||||
return false;
|
||||
}
|
||||
if self.shift != input.modifiers.shift {
|
||||
return false;
|
||||
}
|
||||
if self.alt != input.modifiers.alt {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check key
|
||||
let key = match self.key {
|
||||
ShortcutKey::A => egui::Key::A,
|
||||
ShortcutKey::C => egui::Key::C,
|
||||
ShortcutKey::E => egui::Key::E,
|
||||
ShortcutKey::G => egui::Key::G,
|
||||
ShortcutKey::I => egui::Key::I,
|
||||
ShortcutKey::K => egui::Key::K,
|
||||
ShortcutKey::L => egui::Key::L,
|
||||
ShortcutKey::N => egui::Key::N,
|
||||
ShortcutKey::O => egui::Key::O,
|
||||
ShortcutKey::Q => egui::Key::Q,
|
||||
ShortcutKey::S => egui::Key::S,
|
||||
ShortcutKey::V => egui::Key::V,
|
||||
ShortcutKey::W => egui::Key::W,
|
||||
ShortcutKey::X => egui::Key::X,
|
||||
ShortcutKey::Z => egui::Key::Z,
|
||||
ShortcutKey::Num0 => egui::Key::Num0,
|
||||
ShortcutKey::Comma => egui::Key::Comma,
|
||||
ShortcutKey::Minus => egui::Key::Minus,
|
||||
ShortcutKey::Equals => egui::Key::Equals,
|
||||
ShortcutKey::Plus => egui::Key::Plus,
|
||||
ShortcutKey::BracketLeft => egui::Key::OpenBracket,
|
||||
ShortcutKey::BracketRight => egui::Key::CloseBracket,
|
||||
ShortcutKey::Delete => egui::Key::Delete,
|
||||
};
|
||||
|
||||
input.key_pressed(key)
|
||||
}
|
||||
}
|
||||
|
||||
/// All possible menu actions that can be triggered
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum MenuAction {
|
||||
|
|
@ -74,6 +195,242 @@ pub enum MenuAction {
|
|||
CloseWindow,
|
||||
}
|
||||
|
||||
/// Menu item definition
|
||||
pub struct MenuItemDef {
|
||||
pub label: &'static str,
|
||||
pub action: MenuAction,
|
||||
pub shortcut: Option<Shortcut>,
|
||||
}
|
||||
|
||||
/// Menu structure definition - can be an item, separator, or submenu
|
||||
pub enum MenuDef {
|
||||
Item(&'static MenuItemDef),
|
||||
Separator,
|
||||
Submenu {
|
||||
label: &'static str,
|
||||
children: &'static [MenuDef],
|
||||
},
|
||||
}
|
||||
|
||||
// Shortcut constants for clarity
|
||||
const CTRL: bool = true;
|
||||
const SHIFT: bool = true;
|
||||
const ALT: bool = true;
|
||||
const NO_CTRL: bool = false;
|
||||
const NO_SHIFT: bool = false;
|
||||
const NO_ALT: bool = false;
|
||||
|
||||
// Central menu definitions - single source of truth
|
||||
impl MenuItemDef {
|
||||
// File menu items
|
||||
const NEW_FILE: Self = Self { label: "New file...", action: MenuAction::NewFile, shortcut: Some(Shortcut::new(ShortcutKey::N, CTRL, NO_SHIFT, NO_ALT)) };
|
||||
const NEW_WINDOW: Self = Self { label: "New Window", action: MenuAction::NewWindow, shortcut: Some(Shortcut::new(ShortcutKey::N, CTRL, SHIFT, NO_ALT)) };
|
||||
const SAVE: Self = Self { label: "Save", action: MenuAction::Save, shortcut: Some(Shortcut::new(ShortcutKey::S, CTRL, NO_SHIFT, NO_ALT)) };
|
||||
const SAVE_AS: Self = Self { label: "Save As...", action: MenuAction::SaveAs, shortcut: Some(Shortcut::new(ShortcutKey::S, CTRL, SHIFT, NO_ALT)) };
|
||||
const OPEN_FILE: Self = Self { label: "Open File...", action: MenuAction::OpenFile, shortcut: Some(Shortcut::new(ShortcutKey::O, CTRL, NO_SHIFT, NO_ALT)) };
|
||||
const REVERT: Self = Self { label: "Revert", action: MenuAction::Revert, shortcut: None };
|
||||
const IMPORT: Self = Self { label: "Import...", action: MenuAction::Import, shortcut: Some(Shortcut::new(ShortcutKey::I, CTRL, SHIFT, NO_ALT)) };
|
||||
const EXPORT: Self = Self { label: "Export...", action: MenuAction::Export, shortcut: Some(Shortcut::new(ShortcutKey::E, CTRL, SHIFT, NO_ALT)) };
|
||||
const QUIT: Self = Self { label: "Quit", action: MenuAction::Quit, shortcut: Some(Shortcut::new(ShortcutKey::Q, CTRL, NO_SHIFT, NO_ALT)) };
|
||||
|
||||
// Edit menu items
|
||||
const UNDO: Self = Self { label: "Undo", action: MenuAction::Undo, shortcut: Some(Shortcut::new(ShortcutKey::Z, CTRL, NO_SHIFT, NO_ALT)) };
|
||||
const REDO: Self = Self { label: "Redo", action: MenuAction::Redo, shortcut: Some(Shortcut::new(ShortcutKey::Z, CTRL, SHIFT, NO_ALT)) };
|
||||
const CUT: Self = Self { label: "Cut", action: MenuAction::Cut, shortcut: Some(Shortcut::new(ShortcutKey::X, CTRL, NO_SHIFT, NO_ALT)) };
|
||||
const COPY: Self = Self { label: "Copy", action: MenuAction::Copy, shortcut: Some(Shortcut::new(ShortcutKey::C, CTRL, NO_SHIFT, NO_ALT)) };
|
||||
const PASTE: Self = Self { label: "Paste", action: MenuAction::Paste, shortcut: Some(Shortcut::new(ShortcutKey::V, CTRL, NO_SHIFT, NO_ALT)) };
|
||||
const DELETE: Self = Self { label: "Delete", action: MenuAction::Delete, shortcut: Some(Shortcut::new(ShortcutKey::Delete, NO_CTRL, NO_SHIFT, NO_ALT)) };
|
||||
const SELECT_ALL: Self = Self { label: "Select All", action: MenuAction::SelectAll, shortcut: Some(Shortcut::new(ShortcutKey::A, CTRL, NO_SHIFT, NO_ALT)) };
|
||||
const SELECT_NONE: Self = Self { label: "Select None", action: MenuAction::SelectNone, shortcut: Some(Shortcut::new(ShortcutKey::A, CTRL, SHIFT, NO_ALT)) };
|
||||
const PREFERENCES: Self = Self { label: "Preferences", action: MenuAction::Preferences, shortcut: None };
|
||||
|
||||
// Modify menu items
|
||||
const GROUP: Self = Self { label: "Group", action: MenuAction::Group, shortcut: Some(Shortcut::new(ShortcutKey::G, CTRL, NO_SHIFT, NO_ALT)) };
|
||||
const SEND_TO_BACK: Self = Self { label: "Send to back", action: MenuAction::SendToBack, shortcut: None };
|
||||
const BRING_TO_FRONT: Self = Self { label: "Bring to front", action: MenuAction::BringToFront, shortcut: None };
|
||||
|
||||
// Layer menu items
|
||||
const ADD_LAYER: Self = Self { label: "Add Layer", action: MenuAction::AddLayer, shortcut: Some(Shortcut::new(ShortcutKey::L, CTRL, SHIFT, NO_ALT)) };
|
||||
const ADD_VIDEO_LAYER: Self = Self { label: "Add Video Layer", action: MenuAction::AddVideoLayer, shortcut: None };
|
||||
const ADD_AUDIO_TRACK: Self = Self { label: "Add Audio Track", action: MenuAction::AddAudioTrack, shortcut: None };
|
||||
const ADD_MIDI_TRACK: Self = Self { label: "Add MIDI Track", action: MenuAction::AddMidiTrack, shortcut: None };
|
||||
const DELETE_LAYER: Self = Self { label: "Delete Layer", action: MenuAction::DeleteLayer, shortcut: None };
|
||||
const TOGGLE_LAYER_VISIBILITY: Self = Self { label: "Hide/Show Layer", action: MenuAction::ToggleLayerVisibility, shortcut: None };
|
||||
|
||||
// Timeline menu items
|
||||
const NEW_KEYFRAME: Self = Self { label: "New Keyframe", action: MenuAction::NewKeyframe, shortcut: Some(Shortcut::new(ShortcutKey::K, NO_CTRL, NO_SHIFT, NO_ALT)) };
|
||||
const NEW_BLANK_KEYFRAME: Self = Self { label: "New Blank Keyframe", action: MenuAction::NewBlankKeyframe, shortcut: None };
|
||||
const DELETE_FRAME: Self = Self { label: "Delete Frame", action: MenuAction::DeleteFrame, shortcut: None };
|
||||
const DUPLICATE_KEYFRAME: Self = Self { label: "Duplicate Keyframe", action: MenuAction::DuplicateKeyframe, shortcut: None };
|
||||
const ADD_KEYFRAME_AT_PLAYHEAD: Self = Self { label: "Add Keyframe at Playhead", action: MenuAction::AddKeyframeAtPlayhead, shortcut: None };
|
||||
const ADD_MOTION_TWEEN: Self = Self { label: "Add Motion Tween", action: MenuAction::AddMotionTween, shortcut: None };
|
||||
const ADD_SHAPE_TWEEN: Self = Self { label: "Add Shape Tween", action: MenuAction::AddShapeTween, shortcut: None };
|
||||
const RETURN_TO_START: Self = Self { label: "Return to start", action: MenuAction::ReturnToStart, shortcut: None };
|
||||
const PLAY: Self = Self { label: "Play", action: MenuAction::Play, shortcut: None };
|
||||
|
||||
// View menu items
|
||||
const ZOOM_IN: Self = Self { label: "Zoom In", action: MenuAction::ZoomIn, shortcut: Some(Shortcut::new(ShortcutKey::Equals, CTRL, NO_SHIFT, NO_ALT)) };
|
||||
const ZOOM_OUT: Self = Self { label: "Zoom Out", action: MenuAction::ZoomOut, shortcut: Some(Shortcut::new(ShortcutKey::Minus, CTRL, NO_SHIFT, NO_ALT)) };
|
||||
const ACTUAL_SIZE: Self = Self { label: "Actual Size", action: MenuAction::ActualSize, shortcut: Some(Shortcut::new(ShortcutKey::Num0, CTRL, NO_SHIFT, NO_ALT)) };
|
||||
const RECENTER_VIEW: Self = Self { label: "Recenter View", action: MenuAction::RecenterView, shortcut: None };
|
||||
const NEXT_LAYOUT: Self = Self { label: "Next Layout", action: MenuAction::NextLayout, shortcut: Some(Shortcut::new(ShortcutKey::BracketRight, CTRL, NO_SHIFT, NO_ALT)) };
|
||||
const PREVIOUS_LAYOUT: Self = Self { label: "Previous Layout", action: MenuAction::PreviousLayout, shortcut: Some(Shortcut::new(ShortcutKey::BracketLeft, CTRL, NO_SHIFT, NO_ALT)) };
|
||||
|
||||
// Help menu items
|
||||
const ABOUT: Self = Self { label: "About...", action: MenuAction::About, shortcut: None };
|
||||
|
||||
// macOS app menu items
|
||||
const SETTINGS: Self = Self { label: "Settings", action: MenuAction::Settings, shortcut: Some(Shortcut::new(ShortcutKey::Comma, CTRL, NO_SHIFT, NO_ALT)) };
|
||||
const CLOSE_WINDOW: Self = Self { label: "Close Window", action: MenuAction::CloseWindow, shortcut: Some(Shortcut::new(ShortcutKey::W, CTRL, NO_SHIFT, NO_ALT)) };
|
||||
const QUIT_MACOS: Self = Self { label: "Quit Lightningbeam", action: MenuAction::Quit, shortcut: Some(Shortcut::new(ShortcutKey::Q, CTRL, NO_SHIFT, NO_ALT)) };
|
||||
const ABOUT_MACOS: Self = Self { label: "About Lightningbeam", action: MenuAction::About, shortcut: None };
|
||||
|
||||
/// Get all menu items with shortcuts (for keyboard handling)
|
||||
pub fn all_with_shortcuts() -> &'static [&'static MenuItemDef] {
|
||||
&[
|
||||
&Self::NEW_FILE, &Self::NEW_WINDOW, &Self::SAVE, &Self::SAVE_AS,
|
||||
&Self::OPEN_FILE, &Self::IMPORT, &Self::EXPORT, &Self::QUIT,
|
||||
&Self::UNDO, &Self::REDO, &Self::CUT, &Self::COPY, &Self::PASTE,
|
||||
&Self::DELETE, &Self::SELECT_ALL, &Self::SELECT_NONE,
|
||||
&Self::GROUP, &Self::ADD_LAYER, &Self::NEW_KEYFRAME,
|
||||
&Self::ZOOM_IN, &Self::ZOOM_OUT, &Self::ACTUAL_SIZE,
|
||||
&Self::NEXT_LAYOUT, &Self::PREVIOUS_LAYOUT,
|
||||
&Self::SETTINGS, &Self::CLOSE_WINDOW,
|
||||
]
|
||||
}
|
||||
|
||||
/// Get the complete menu structure definition
|
||||
pub const fn menu_structure() -> &'static [MenuDef] {
|
||||
&[
|
||||
// File menu
|
||||
MenuDef::Submenu {
|
||||
label: "File",
|
||||
children: &[
|
||||
MenuDef::Item(&Self::NEW_FILE),
|
||||
MenuDef::Item(&Self::NEW_WINDOW),
|
||||
MenuDef::Separator,
|
||||
MenuDef::Item(&Self::SAVE),
|
||||
MenuDef::Item(&Self::SAVE_AS),
|
||||
MenuDef::Separator,
|
||||
MenuDef::Submenu {
|
||||
label: "Open Recent",
|
||||
children: &[], // TODO: Dynamic recent files
|
||||
},
|
||||
MenuDef::Item(&Self::OPEN_FILE),
|
||||
MenuDef::Item(&Self::REVERT),
|
||||
MenuDef::Separator,
|
||||
MenuDef::Item(&Self::IMPORT),
|
||||
MenuDef::Item(&Self::EXPORT),
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
MenuDef::Separator,
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
MenuDef::Item(&Self::QUIT),
|
||||
],
|
||||
},
|
||||
// Edit menu
|
||||
MenuDef::Submenu {
|
||||
label: "Edit",
|
||||
children: &[
|
||||
MenuDef::Item(&Self::UNDO),
|
||||
MenuDef::Item(&Self::REDO),
|
||||
MenuDef::Separator,
|
||||
MenuDef::Item(&Self::CUT),
|
||||
MenuDef::Item(&Self::COPY),
|
||||
MenuDef::Item(&Self::PASTE),
|
||||
MenuDef::Item(&Self::DELETE),
|
||||
MenuDef::Separator,
|
||||
MenuDef::Item(&Self::SELECT_ALL),
|
||||
MenuDef::Item(&Self::SELECT_NONE),
|
||||
MenuDef::Separator,
|
||||
MenuDef::Item(&Self::PREFERENCES),
|
||||
],
|
||||
},
|
||||
// Modify menu
|
||||
MenuDef::Submenu {
|
||||
label: "Modify",
|
||||
children: &[
|
||||
MenuDef::Item(&Self::GROUP),
|
||||
MenuDef::Separator,
|
||||
MenuDef::Item(&Self::SEND_TO_BACK),
|
||||
MenuDef::Item(&Self::BRING_TO_FRONT),
|
||||
],
|
||||
},
|
||||
// Layer menu
|
||||
MenuDef::Submenu {
|
||||
label: "Layer",
|
||||
children: &[
|
||||
MenuDef::Item(&Self::ADD_LAYER),
|
||||
MenuDef::Item(&Self::ADD_VIDEO_LAYER),
|
||||
MenuDef::Item(&Self::ADD_AUDIO_TRACK),
|
||||
MenuDef::Item(&Self::ADD_MIDI_TRACK),
|
||||
MenuDef::Separator,
|
||||
MenuDef::Item(&Self::DELETE_LAYER),
|
||||
MenuDef::Item(&Self::TOGGLE_LAYER_VISIBILITY),
|
||||
],
|
||||
},
|
||||
// Timeline menu
|
||||
MenuDef::Submenu {
|
||||
label: "Timeline",
|
||||
children: &[
|
||||
MenuDef::Item(&Self::NEW_KEYFRAME),
|
||||
MenuDef::Item(&Self::NEW_BLANK_KEYFRAME),
|
||||
MenuDef::Item(&Self::DELETE_FRAME),
|
||||
MenuDef::Item(&Self::DUPLICATE_KEYFRAME),
|
||||
MenuDef::Item(&Self::ADD_KEYFRAME_AT_PLAYHEAD),
|
||||
MenuDef::Separator,
|
||||
MenuDef::Item(&Self::ADD_MOTION_TWEEN),
|
||||
MenuDef::Item(&Self::ADD_SHAPE_TWEEN),
|
||||
MenuDef::Separator,
|
||||
MenuDef::Item(&Self::RETURN_TO_START),
|
||||
MenuDef::Item(&Self::PLAY),
|
||||
],
|
||||
},
|
||||
// View menu
|
||||
MenuDef::Submenu {
|
||||
label: "View",
|
||||
children: &[
|
||||
MenuDef::Item(&Self::ZOOM_IN),
|
||||
MenuDef::Item(&Self::ZOOM_OUT),
|
||||
MenuDef::Item(&Self::ACTUAL_SIZE),
|
||||
MenuDef::Item(&Self::RECENTER_VIEW),
|
||||
MenuDef::Separator,
|
||||
MenuDef::Submenu {
|
||||
label: "Layout",
|
||||
children: &[
|
||||
MenuDef::Item(&Self::NEXT_LAYOUT),
|
||||
MenuDef::Item(&Self::PREVIOUS_LAYOUT),
|
||||
// TODO: Dynamic layout list
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
// Help menu
|
||||
MenuDef::Submenu {
|
||||
label: "Help",
|
||||
children: &[
|
||||
MenuDef::Item(&Self::ABOUT),
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/// Get macOS app menu structure
|
||||
#[cfg(target_os = "macos")]
|
||||
pub const fn macos_app_menu() -> MenuDef {
|
||||
MenuDef::Submenu {
|
||||
label: "Lightningbeam",
|
||||
children: &[
|
||||
MenuDef::Item(&Self::ABOUT_MACOS),
|
||||
MenuDef::Separator,
|
||||
MenuDef::Item(&Self::SETTINGS),
|
||||
MenuDef::Separator,
|
||||
MenuDef::Item(&Self::CLOSE_WINDOW),
|
||||
MenuDef::Item(&Self::QUIT_MACOS),
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Menu system that holds all menu items and can dispatch actions
|
||||
pub struct MenuSystem {
|
||||
#[allow(dead_code)]
|
||||
|
|
@ -90,416 +447,61 @@ impl MenuSystem {
|
|||
// Platform-specific: Add "Lightningbeam" menu on macOS
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let app_menu = Submenu::new("Lightningbeam", true);
|
||||
|
||||
let about_item = MenuItem::new("About Lightningbeam", true, None);
|
||||
items.push((about_item.clone(), MenuAction::About));
|
||||
app_menu.append(&about_item)?;
|
||||
|
||||
app_menu.append(&PredefinedMenuItem::separator())?;
|
||||
|
||||
let settings_item = MenuItem::new(
|
||||
"Settings",
|
||||
true,
|
||||
Some(Accelerator::new(Some(Modifiers::META), Code::Comma)),
|
||||
);
|
||||
items.push((settings_item.clone(), MenuAction::Settings));
|
||||
app_menu.append(&settings_item)?;
|
||||
|
||||
app_menu.append(&PredefinedMenuItem::separator())?;
|
||||
|
||||
let close_item = MenuItem::new(
|
||||
"Close Window",
|
||||
true,
|
||||
Some(Accelerator::new(Some(Modifiers::META), Code::KeyW)),
|
||||
);
|
||||
items.push((close_item.clone(), MenuAction::CloseWindow));
|
||||
app_menu.append(&close_item)?;
|
||||
|
||||
let quit_item = MenuItem::new(
|
||||
"Quit Lightningbeam",
|
||||
true,
|
||||
Some(Accelerator::new(Some(Modifiers::META), Code::KeyQ)),
|
||||
);
|
||||
items.push((quit_item.clone(), MenuAction::Quit));
|
||||
app_menu.append(&quit_item)?;
|
||||
|
||||
menu.append(&app_menu)?;
|
||||
Self::build_submenu(&menu, &MenuItemDef::macos_app_menu(), &mut items)?;
|
||||
}
|
||||
|
||||
// File menu
|
||||
let file_menu = Submenu::new("File", true);
|
||||
|
||||
let new_file = MenuItem::new(
|
||||
"New file...",
|
||||
true,
|
||||
Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyN)),
|
||||
);
|
||||
items.push((new_file.clone(), MenuAction::NewFile));
|
||||
file_menu.append(&new_file)?;
|
||||
|
||||
let new_window = MenuItem::new(
|
||||
"New Window",
|
||||
true,
|
||||
Some(Accelerator::new(
|
||||
Some(Modifiers::CONTROL | Modifiers::SHIFT),
|
||||
Code::KeyN,
|
||||
)),
|
||||
);
|
||||
items.push((new_window.clone(), MenuAction::NewWindow));
|
||||
file_menu.append(&new_window)?;
|
||||
|
||||
file_menu.append(&PredefinedMenuItem::separator())?;
|
||||
|
||||
let save = MenuItem::new(
|
||||
"Save",
|
||||
true,
|
||||
Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyS)),
|
||||
);
|
||||
items.push((save.clone(), MenuAction::Save));
|
||||
file_menu.append(&save)?;
|
||||
|
||||
let save_as = MenuItem::new(
|
||||
"Save As...",
|
||||
true,
|
||||
Some(Accelerator::new(
|
||||
Some(Modifiers::CONTROL | Modifiers::SHIFT),
|
||||
Code::KeyS,
|
||||
)),
|
||||
);
|
||||
items.push((save_as.clone(), MenuAction::SaveAs));
|
||||
file_menu.append(&save_as)?;
|
||||
|
||||
file_menu.append(&PredefinedMenuItem::separator())?;
|
||||
|
||||
// Open Recent submenu (placeholder for now)
|
||||
let open_recent = Submenu::new("Open Recent", true);
|
||||
file_menu.append(&open_recent)?;
|
||||
|
||||
let open_file = MenuItem::new(
|
||||
"Open File...",
|
||||
true,
|
||||
Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyO)),
|
||||
);
|
||||
items.push((open_file.clone(), MenuAction::OpenFile));
|
||||
file_menu.append(&open_file)?;
|
||||
|
||||
let revert = MenuItem::new("Revert", true, None);
|
||||
items.push((revert.clone(), MenuAction::Revert));
|
||||
file_menu.append(&revert)?;
|
||||
|
||||
file_menu.append(&PredefinedMenuItem::separator())?;
|
||||
|
||||
let import = MenuItem::new(
|
||||
"Import...",
|
||||
true,
|
||||
Some(Accelerator::new(
|
||||
Some(Modifiers::CONTROL | Modifiers::SHIFT),
|
||||
Code::KeyI,
|
||||
)),
|
||||
);
|
||||
items.push((import.clone(), MenuAction::Import));
|
||||
file_menu.append(&import)?;
|
||||
|
||||
let export = MenuItem::new(
|
||||
"Export...",
|
||||
true,
|
||||
Some(Accelerator::new(
|
||||
Some(Modifiers::CONTROL | Modifiers::SHIFT),
|
||||
Code::KeyE,
|
||||
)),
|
||||
);
|
||||
items.push((export.clone(), MenuAction::Export));
|
||||
file_menu.append(&export)?;
|
||||
|
||||
// On non-macOS, add Quit to File menu
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
file_menu.append(&PredefinedMenuItem::separator())?;
|
||||
let quit = MenuItem::new(
|
||||
"Quit",
|
||||
true,
|
||||
Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyQ)),
|
||||
);
|
||||
items.push((quit.clone(), MenuAction::Quit));
|
||||
file_menu.append(&quit)?;
|
||||
// Build all menus from the centralized structure
|
||||
for menu_def in MenuItemDef::menu_structure() {
|
||||
Self::build_submenu(&menu, menu_def, &mut items)?;
|
||||
}
|
||||
|
||||
menu.append(&file_menu)?;
|
||||
|
||||
// Edit menu
|
||||
let edit_menu = Submenu::new("Edit", true);
|
||||
|
||||
let undo = MenuItem::new(
|
||||
"Undo",
|
||||
true,
|
||||
Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyZ)),
|
||||
);
|
||||
items.push((undo.clone(), MenuAction::Undo));
|
||||
edit_menu.append(&undo)?;
|
||||
|
||||
let redo = MenuItem::new(
|
||||
"Redo",
|
||||
true,
|
||||
Some(Accelerator::new(
|
||||
Some(Modifiers::CONTROL | Modifiers::SHIFT),
|
||||
Code::KeyZ,
|
||||
)),
|
||||
);
|
||||
items.push((redo.clone(), MenuAction::Redo));
|
||||
edit_menu.append(&redo)?;
|
||||
|
||||
edit_menu.append(&PredefinedMenuItem::separator())?;
|
||||
|
||||
let cut = MenuItem::new(
|
||||
"Cut",
|
||||
true,
|
||||
Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyX)),
|
||||
);
|
||||
items.push((cut.clone(), MenuAction::Cut));
|
||||
edit_menu.append(&cut)?;
|
||||
|
||||
let copy = MenuItem::new(
|
||||
"Copy",
|
||||
true,
|
||||
Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyC)),
|
||||
);
|
||||
items.push((copy.clone(), MenuAction::Copy));
|
||||
edit_menu.append(©)?;
|
||||
|
||||
let paste = MenuItem::new(
|
||||
"Paste",
|
||||
true,
|
||||
Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyV)),
|
||||
);
|
||||
items.push((paste.clone(), MenuAction::Paste));
|
||||
edit_menu.append(&paste)?;
|
||||
|
||||
let delete = MenuItem::new(
|
||||
"Delete",
|
||||
true,
|
||||
Some(Accelerator::new(None, Code::Delete)),
|
||||
);
|
||||
items.push((delete.clone(), MenuAction::Delete));
|
||||
edit_menu.append(&delete)?;
|
||||
|
||||
edit_menu.append(&PredefinedMenuItem::separator())?;
|
||||
|
||||
let select_all = MenuItem::new(
|
||||
"Select All",
|
||||
true,
|
||||
Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyA)),
|
||||
);
|
||||
items.push((select_all.clone(), MenuAction::SelectAll));
|
||||
edit_menu.append(&select_all)?;
|
||||
|
||||
let select_none = MenuItem::new(
|
||||
"Select None",
|
||||
true,
|
||||
Some(Accelerator::new(
|
||||
Some(Modifiers::CONTROL | Modifiers::SHIFT),
|
||||
Code::KeyA,
|
||||
)),
|
||||
);
|
||||
items.push((select_none.clone(), MenuAction::SelectNone));
|
||||
edit_menu.append(&select_none)?;
|
||||
|
||||
edit_menu.append(&PredefinedMenuItem::separator())?;
|
||||
|
||||
let preferences = MenuItem::new("Preferences", true, None);
|
||||
items.push((preferences.clone(), MenuAction::Preferences));
|
||||
edit_menu.append(&preferences)?;
|
||||
|
||||
menu.append(&edit_menu)?;
|
||||
|
||||
// Modify menu
|
||||
let modify_menu = Submenu::new("Modify", true);
|
||||
|
||||
let group = MenuItem::new(
|
||||
"Group",
|
||||
true,
|
||||
Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyG)),
|
||||
);
|
||||
items.push((group.clone(), MenuAction::Group));
|
||||
modify_menu.append(&group)?;
|
||||
|
||||
modify_menu.append(&PredefinedMenuItem::separator())?;
|
||||
|
||||
let send_to_back = MenuItem::new("Send to back", true, None);
|
||||
items.push((send_to_back.clone(), MenuAction::SendToBack));
|
||||
modify_menu.append(&send_to_back)?;
|
||||
|
||||
let bring_to_front = MenuItem::new("Bring to front", true, None);
|
||||
items.push((bring_to_front.clone(), MenuAction::BringToFront));
|
||||
modify_menu.append(&bring_to_front)?;
|
||||
|
||||
menu.append(&modify_menu)?;
|
||||
|
||||
// Layer menu
|
||||
let layer_menu = Submenu::new("Layer", true);
|
||||
|
||||
let add_layer = MenuItem::new(
|
||||
"Add Layer",
|
||||
true,
|
||||
Some(Accelerator::new(
|
||||
Some(Modifiers::CONTROL | Modifiers::SHIFT),
|
||||
Code::KeyL,
|
||||
)),
|
||||
);
|
||||
items.push((add_layer.clone(), MenuAction::AddLayer));
|
||||
layer_menu.append(&add_layer)?;
|
||||
|
||||
let add_video_layer = MenuItem::new("Add Video Layer", true, None);
|
||||
items.push((add_video_layer.clone(), MenuAction::AddVideoLayer));
|
||||
layer_menu.append(&add_video_layer)?;
|
||||
|
||||
let add_audio_track = MenuItem::new("Add Audio Track", true, None);
|
||||
items.push((add_audio_track.clone(), MenuAction::AddAudioTrack));
|
||||
layer_menu.append(&add_audio_track)?;
|
||||
|
||||
let add_midi_track = MenuItem::new("Add MIDI Track", true, None);
|
||||
items.push((add_midi_track.clone(), MenuAction::AddMidiTrack));
|
||||
layer_menu.append(&add_midi_track)?;
|
||||
|
||||
layer_menu.append(&PredefinedMenuItem::separator())?;
|
||||
|
||||
let delete_layer = MenuItem::new("Delete Layer", true, None);
|
||||
items.push((delete_layer.clone(), MenuAction::DeleteLayer));
|
||||
layer_menu.append(&delete_layer)?;
|
||||
|
||||
let toggle_layer = MenuItem::new("Hide/Show Layer", true, None);
|
||||
items.push((toggle_layer.clone(), MenuAction::ToggleLayerVisibility));
|
||||
layer_menu.append(&toggle_layer)?;
|
||||
|
||||
menu.append(&layer_menu)?;
|
||||
|
||||
// Timeline menu
|
||||
let timeline_menu = Submenu::new("Timeline", true);
|
||||
|
||||
let new_keyframe = MenuItem::new(
|
||||
"New Keyframe",
|
||||
true,
|
||||
Some(Accelerator::new(None, Code::KeyK)),
|
||||
);
|
||||
items.push((new_keyframe.clone(), MenuAction::NewKeyframe));
|
||||
timeline_menu.append(&new_keyframe)?;
|
||||
|
||||
let new_blank_keyframe = MenuItem::new("New Blank Keyframe", true, None);
|
||||
items.push((new_blank_keyframe.clone(), MenuAction::NewBlankKeyframe));
|
||||
timeline_menu.append(&new_blank_keyframe)?;
|
||||
|
||||
let delete_frame = MenuItem::new("Delete Frame", true, None);
|
||||
items.push((delete_frame.clone(), MenuAction::DeleteFrame));
|
||||
timeline_menu.append(&delete_frame)?;
|
||||
|
||||
let duplicate_keyframe = MenuItem::new("Duplicate Keyframe", true, None);
|
||||
items.push((duplicate_keyframe.clone(), MenuAction::DuplicateKeyframe));
|
||||
timeline_menu.append(&duplicate_keyframe)?;
|
||||
|
||||
let add_keyframe_playhead = MenuItem::new("Add Keyframe at Playhead", true, None);
|
||||
items.push((add_keyframe_playhead.clone(), MenuAction::AddKeyframeAtPlayhead));
|
||||
timeline_menu.append(&add_keyframe_playhead)?;
|
||||
|
||||
timeline_menu.append(&PredefinedMenuItem::separator())?;
|
||||
|
||||
let motion_tween = MenuItem::new("Add Motion Tween", true, None);
|
||||
items.push((motion_tween.clone(), MenuAction::AddMotionTween));
|
||||
timeline_menu.append(&motion_tween)?;
|
||||
|
||||
let shape_tween = MenuItem::new("Add Shape Tween", true, None);
|
||||
items.push((shape_tween.clone(), MenuAction::AddShapeTween));
|
||||
timeline_menu.append(&shape_tween)?;
|
||||
|
||||
timeline_menu.append(&PredefinedMenuItem::separator())?;
|
||||
|
||||
let return_to_start = MenuItem::new("Return to start", true, None);
|
||||
items.push((return_to_start.clone(), MenuAction::ReturnToStart));
|
||||
timeline_menu.append(&return_to_start)?;
|
||||
|
||||
let play = MenuItem::new("Play", true, None);
|
||||
items.push((play.clone(), MenuAction::Play));
|
||||
timeline_menu.append(&play)?;
|
||||
|
||||
menu.append(&timeline_menu)?;
|
||||
|
||||
// View menu
|
||||
let view_menu = Submenu::new("View", true);
|
||||
|
||||
let zoom_in = MenuItem::new(
|
||||
"Zoom In",
|
||||
true,
|
||||
Some(Accelerator::new(Some(Modifiers::CONTROL), Code::Equal)),
|
||||
);
|
||||
items.push((zoom_in.clone(), MenuAction::ZoomIn));
|
||||
view_menu.append(&zoom_in)?;
|
||||
|
||||
let zoom_out = MenuItem::new(
|
||||
"Zoom Out",
|
||||
true,
|
||||
Some(Accelerator::new(Some(Modifiers::CONTROL), Code::Minus)),
|
||||
);
|
||||
items.push((zoom_out.clone(), MenuAction::ZoomOut));
|
||||
view_menu.append(&zoom_out)?;
|
||||
|
||||
let actual_size = MenuItem::new(
|
||||
"Actual Size",
|
||||
true,
|
||||
Some(Accelerator::new(Some(Modifiers::CONTROL), Code::Digit0)),
|
||||
);
|
||||
items.push((actual_size.clone(), MenuAction::ActualSize));
|
||||
view_menu.append(&actual_size)?;
|
||||
|
||||
let recenter = MenuItem::new("Recenter View", true, None);
|
||||
items.push((recenter.clone(), MenuAction::RecenterView));
|
||||
view_menu.append(&recenter)?;
|
||||
|
||||
view_menu.append(&PredefinedMenuItem::separator())?;
|
||||
|
||||
// Layout submenu
|
||||
let layout_submenu = Submenu::new("Layout", true);
|
||||
|
||||
let next_layout = MenuItem::new(
|
||||
"Next Layout",
|
||||
true,
|
||||
Some(Accelerator::new(Some(Modifiers::CONTROL), Code::BracketRight)),
|
||||
);
|
||||
items.push((next_layout.clone(), MenuAction::NextLayout));
|
||||
layout_submenu.append(&next_layout)?;
|
||||
|
||||
let prev_layout = MenuItem::new(
|
||||
"Previous Layout",
|
||||
true,
|
||||
Some(Accelerator::new(Some(Modifiers::CONTROL), Code::BracketLeft)),
|
||||
);
|
||||
items.push((prev_layout.clone(), MenuAction::PreviousLayout));
|
||||
layout_submenu.append(&prev_layout)?;
|
||||
|
||||
// TODO: Add dynamic layout list with checkmarks for current layout
|
||||
// This will need to be updated when layouts change
|
||||
|
||||
view_menu.append(&layout_submenu)?;
|
||||
menu.append(&view_menu)?;
|
||||
|
||||
// Help menu
|
||||
let help_menu = Submenu::new("Help", true);
|
||||
|
||||
let about = MenuItem::new("About...", true, None);
|
||||
items.push((about.clone(), MenuAction::About));
|
||||
help_menu.append(&about)?;
|
||||
|
||||
menu.append(&help_menu)?;
|
||||
|
||||
Ok(Self { menu, items })
|
||||
}
|
||||
|
||||
/// Initialize the menu for the application window
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn init_for_gtk(&self, window: >k::ApplicationWindow, container: Option<>k::Box>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
self.menu.init_for_gtk_window(window, container)?;
|
||||
/// Build a top-level submenu and append to menu
|
||||
fn build_submenu(
|
||||
menu: &Menu,
|
||||
def: &MenuDef,
|
||||
items: &mut Vec<(MenuItem, MenuAction)>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if let MenuDef::Submenu { label, children } = def {
|
||||
let submenu = Submenu::new(*label, true);
|
||||
for child in *children {
|
||||
Self::build_menu_item(&submenu, child, items)?;
|
||||
}
|
||||
menu.append(&submenu)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Initialize the menu for macOS (app-wide)
|
||||
/// Recursively build menu items within a submenu
|
||||
fn build_menu_item(
|
||||
parent: &Submenu,
|
||||
def: &MenuDef,
|
||||
items: &mut Vec<(MenuItem, MenuAction)>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
match def {
|
||||
MenuDef::Item(item_def) => {
|
||||
let accelerator = item_def.shortcut.as_ref().map(|s| s.to_muda_accelerator());
|
||||
let item = MenuItem::new(item_def.label, true, accelerator);
|
||||
items.push((item.clone(), item_def.action));
|
||||
parent.append(&item)?;
|
||||
}
|
||||
MenuDef::Separator => {
|
||||
parent.append(&PredefinedMenuItem::separator())?;
|
||||
}
|
||||
MenuDef::Submenu { label, children } => {
|
||||
let submenu = Submenu::new(*label, true);
|
||||
for child in *children {
|
||||
Self::build_menu_item(&submenu, child, items)?;
|
||||
}
|
||||
parent.append(&submenu)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Initialize native menus for macOS (app-wide, doesn't require window handle)
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn init_for_macos(&self) {
|
||||
self.menu.init_for_nsapp();
|
||||
|
|
@ -517,6 +519,135 @@ impl MenuSystem {
|
|||
None
|
||||
}
|
||||
|
||||
/// Check keyboard shortcuts from egui input and return the action
|
||||
/// This works cross-platform and complements native menus
|
||||
pub fn check_shortcuts(input: &egui::InputState) -> Option<MenuAction> {
|
||||
for def in MenuItemDef::all_with_shortcuts() {
|
||||
if let Some(shortcut) = &def.shortcut {
|
||||
if shortcut.matches_egui_input(input) {
|
||||
return Some(def.action);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Render egui menu bar from the same menu structure (for Linux/Windows)
|
||||
pub fn render_egui_menu_bar(ui: &mut egui::Ui) -> Option<MenuAction> {
|
||||
let mut action = None;
|
||||
|
||||
egui::menu::bar(ui, |ui| {
|
||||
for menu_def in MenuItemDef::menu_structure() {
|
||||
if let Some(a) = Self::render_menu_def(ui, menu_def) {
|
||||
action = Some(a);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
action
|
||||
}
|
||||
|
||||
/// Recursively render a MenuDef as egui UI
|
||||
fn render_menu_def(ui: &mut egui::Ui, def: &MenuDef) -> Option<MenuAction> {
|
||||
match def {
|
||||
MenuDef::Item(item_def) => {
|
||||
if Self::render_menu_item(ui, item_def) {
|
||||
Some(item_def.action)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
MenuDef::Separator => {
|
||||
ui.separator();
|
||||
None
|
||||
}
|
||||
MenuDef::Submenu { label, children } => {
|
||||
let mut action = None;
|
||||
ui.menu_button(*label, |ui| {
|
||||
for child in *children {
|
||||
if let Some(a) = Self::render_menu_def(ui, child) {
|
||||
action = Some(a);
|
||||
ui.close_menu();
|
||||
}
|
||||
}
|
||||
});
|
||||
action
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Render a single menu item with label and shortcut
|
||||
fn render_menu_item(ui: &mut egui::Ui, def: &MenuItemDef) -> bool {
|
||||
let shortcut_text = if let Some(shortcut) = &def.shortcut {
|
||||
Self::format_shortcut(shortcut)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
// Set minimum width for menu items to prevent cramping
|
||||
ui.set_min_width(180.0);
|
||||
|
||||
if shortcut_text.is_empty() {
|
||||
ui.add(egui::Button::new(def.label).min_size(egui::vec2(0.0, 0.0))).clicked()
|
||||
} else {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 20.0; // More space between label and shortcut
|
||||
|
||||
let button = ui.add(egui::Button::new(def.label).min_size(egui::vec2(0.0, 0.0)));
|
||||
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
ui.label(egui::RichText::new(&shortcut_text).weak().size(12.0));
|
||||
});
|
||||
|
||||
button.clicked()
|
||||
}).inner
|
||||
}
|
||||
}
|
||||
|
||||
/// Format shortcut for display (e.g., "Ctrl+S")
|
||||
fn format_shortcut(shortcut: &Shortcut) -> String {
|
||||
let mut parts = Vec::new();
|
||||
|
||||
if shortcut.ctrl {
|
||||
parts.push("Ctrl");
|
||||
}
|
||||
if shortcut.shift {
|
||||
parts.push("Shift");
|
||||
}
|
||||
if shortcut.alt {
|
||||
parts.push("Alt");
|
||||
}
|
||||
|
||||
let key_name = match shortcut.key {
|
||||
ShortcutKey::A => "A",
|
||||
ShortcutKey::C => "C",
|
||||
ShortcutKey::E => "E",
|
||||
ShortcutKey::G => "G",
|
||||
ShortcutKey::I => "I",
|
||||
ShortcutKey::K => "K",
|
||||
ShortcutKey::L => "L",
|
||||
ShortcutKey::N => "N",
|
||||
ShortcutKey::O => "O",
|
||||
ShortcutKey::Q => "Q",
|
||||
ShortcutKey::S => "S",
|
||||
ShortcutKey::V => "V",
|
||||
ShortcutKey::W => "W",
|
||||
ShortcutKey::X => "X",
|
||||
ShortcutKey::Z => "Z",
|
||||
ShortcutKey::Num0 => "0",
|
||||
ShortcutKey::Comma => ",",
|
||||
ShortcutKey::Minus => "-",
|
||||
ShortcutKey::Equals => "=",
|
||||
ShortcutKey::Plus => "+",
|
||||
ShortcutKey::BracketLeft => "[",
|
||||
ShortcutKey::BracketRight => "]",
|
||||
ShortcutKey::Delete => "Del",
|
||||
};
|
||||
parts.push(key_name);
|
||||
|
||||
parts.join("+")
|
||||
}
|
||||
|
||||
/// Update menu item text dynamically (e.g., for Undo/Redo with action names)
|
||||
#[allow(dead_code)]
|
||||
pub fn update_undo_text(&self, action_name: Option<&str>) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue