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);
|
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 {
|
let options = eframe::NativeOptions {
|
||||||
viewport: egui::ViewportBuilder::default()
|
viewport: egui::ViewportBuilder::default()
|
||||||
.with_inner_size([1920.0, 1080.0])
|
.with_inner_size([1920.0, 1080.0])
|
||||||
|
|
@ -180,8 +189,7 @@ struct EditorApp {
|
||||||
fill_color: egui::Color32, // Fill color for drawing
|
fill_color: egui::Color32, // Fill color for drawing
|
||||||
stroke_color: egui::Color32, // Stroke color for drawing
|
stroke_color: egui::Color32, // Stroke color for drawing
|
||||||
pane_instances: HashMap<NodePath, PaneInstance>, // Pane instances per path
|
pane_instances: HashMap<NodePath, PaneInstance>, // Pane instances per path
|
||||||
menu_system: Option<MenuSystem>, // Native menu system
|
menu_system: Option<MenuSystem>, // Native menu system for event checking
|
||||||
menu_initialized: bool, // Track if menu has been initialized with window
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EditorApp {
|
impl EditorApp {
|
||||||
|
|
@ -206,7 +214,6 @@ impl EditorApp {
|
||||||
stroke_color: egui::Color32::from_rgb(0, 0, 0), // Default black stroke
|
stroke_color: egui::Color32::from_rgb(0, 0, 0), // Default black stroke
|
||||||
pane_instances: HashMap::new(), // Initialize empty, panes created on-demand
|
pane_instances: HashMap::new(), // Initialize empty, panes created on-demand
|
||||||
menu_system,
|
menu_system,
|
||||||
menu_initialized: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -462,41 +469,27 @@ impl EditorApp {
|
||||||
|
|
||||||
impl eframe::App for EditorApp {
|
impl eframe::App for EditorApp {
|
||||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
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(menu_system) = &self.menu_system {
|
||||||
if let Some(action) = menu_system.check_events() {
|
if let Some(action) = menu_system.check_events() {
|
||||||
self.handle_menu_action(action);
|
self.handle_menu_action(action);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Top menu bar
|
// Check keyboard shortcuts (works on all platforms)
|
||||||
let mut layout_to_switch: Option<usize> = None;
|
ctx.input(|i| {
|
||||||
let current_name = self.current_layout_def().name.clone();
|
if let Some(action) = MenuSystem::check_shortcuts(i) {
|
||||||
|
self.handle_menu_action(action);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Top menu bar (egui-rendered on all platforms)
|
||||||
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
|
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
|
||||||
egui::menu::bar(ui, |ui| {
|
if let Some(action) = MenuSystem::render_egui_menu_bar(ui) {
|
||||||
ui.menu_button("Layout", |ui| {
|
self.handle_menu_action(action);
|
||||||
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));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Switch layout after menu closes to avoid borrow issues
|
|
||||||
if let Some(index) = layout_to_switch {
|
|
||||||
self.switch_layout(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main pane area
|
// Main pane area
|
||||||
let mut layout_action: Option<LayoutAction> = None;
|
let mut layout_action: Option<LayoutAction> = None;
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,133 @@
|
||||||
///
|
///
|
||||||
/// This module creates the native menu bar with all menu items matching
|
/// This module creates the native menu bar with all menu items matching
|
||||||
/// the JavaScript version's menu structure.
|
/// 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::{
|
use muda::{
|
||||||
accelerator::{Accelerator, Code, Modifiers},
|
accelerator::{Accelerator, Code, Modifiers},
|
||||||
Menu, MenuItem, PredefinedMenuItem, Submenu,
|
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
|
/// All possible menu actions that can be triggered
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum MenuAction {
|
pub enum MenuAction {
|
||||||
|
|
@ -74,6 +195,242 @@ pub enum MenuAction {
|
||||||
CloseWindow,
|
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
|
/// Menu system that holds all menu items and can dispatch actions
|
||||||
pub struct MenuSystem {
|
pub struct MenuSystem {
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
|
@ -90,416 +447,61 @@ impl MenuSystem {
|
||||||
// Platform-specific: Add "Lightningbeam" menu on macOS
|
// Platform-specific: Add "Lightningbeam" menu on macOS
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
let app_menu = Submenu::new("Lightningbeam", true);
|
Self::build_submenu(&menu, &MenuItemDef::macos_app_menu(), &mut items)?;
|
||||||
|
|
||||||
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)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// File menu
|
// Build all menus from the centralized structure
|
||||||
let file_menu = Submenu::new("File", true);
|
for menu_def in MenuItemDef::menu_structure() {
|
||||||
|
Self::build_submenu(&menu, menu_def, &mut items)?;
|
||||||
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)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 })
|
Ok(Self { menu, items })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize the menu for the application window
|
/// Build a top-level submenu and append to menu
|
||||||
#[cfg(target_os = "linux")]
|
fn build_submenu(
|
||||||
pub fn init_for_gtk(&self, window: >k::ApplicationWindow, container: Option<>k::Box>) -> Result<(), Box<dyn std::error::Error>> {
|
menu: &Menu,
|
||||||
self.menu.init_for_gtk_window(window, container)?;
|
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(())
|
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")]
|
#[cfg(target_os = "macos")]
|
||||||
pub fn init_for_macos(&self) {
|
pub fn init_for_macos(&self) {
|
||||||
self.menu.init_for_nsapp();
|
self.menu.init_for_nsapp();
|
||||||
|
|
@ -517,6 +519,135 @@ impl MenuSystem {
|
||||||
None
|
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)
|
/// Update menu item text dynamically (e.g., for Undo/Redo with action names)
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn update_undo_text(&self, action_name: Option<&str>) {
|
pub fn update_undo_text(&self, action_name: Option<&str>) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue