use crate::behaviors; use crate::editor::Message as EditorMessage; use iced::widget::{button, container, row, text, Column, Space}; use iced::{Alignment, Background, Border, Color, Element, Length, Padding, Theme}; pub const BAR_HEIGHT: f32 = 24.0; const TITLE_WIDTH: f32 = 75.0; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum MenuId { File, Edit, Transport, View, } impl MenuId { const ALL: [MenuId; 4] = [MenuId::File, MenuId::Edit, MenuId::Transport, MenuId::View]; fn label(&self) -> &'static str { match self { MenuId::File => "File", MenuId::Edit => "Edit", MenuId::Transport => "Transport", MenuId::View => "View", } } } pub struct State { pub open: Option, } impl State { pub fn new() -> Self { Self { open: None } } } #[derive(Debug, Clone)] pub enum Message { Open(MenuId), Close, Action(behaviors::Action), ShowNewTrackWizard, } struct MenuItem { label: &'static str, shortcut: &'static str, message: Message, } enum MenuEntry { Item(MenuItem), Separator, } fn file_entries() -> Vec { use behaviors::Action::*; vec![ MenuEntry::Item(MenuItem { label: "New Project", shortcut: "\u{2318}N", message: Message::Action(NewProject), }), MenuEntry::Item(MenuItem { label: "Open\u{2026}", shortcut: "\u{2318}O", message: Message::Action(OpenProject), }), MenuEntry::Separator, MenuEntry::Item(MenuItem { label: "Save", shortcut: "\u{2318}S", message: Message::Action(SaveProject), }), MenuEntry::Item(MenuItem { label: "Save As\u{2026}", shortcut: "\u{21E7}\u{2318}S", message: Message::Action(SaveProjectAs), }), MenuEntry::Separator, MenuEntry::Item(MenuItem { label: "Close", shortcut: "\u{2318}W", message: Message::Action(CloseProject), }), MenuEntry::Separator, MenuEntry::Item(MenuItem { label: "Settings", shortcut: "\u{2318},", message: Message::Action(OpenSettings), }), ] } fn edit_entries() -> Vec { use behaviors::Action::*; vec![ MenuEntry::Item(MenuItem { label: "Undo", shortcut: "\u{2318}Z", message: Message::Action(Undo), }), MenuEntry::Item(MenuItem { label: "Redo", shortcut: "\u{21E7}\u{2318}Z", message: Message::Action(Redo), }), MenuEntry::Separator, MenuEntry::Item(MenuItem { label: "Cut", shortcut: "\u{2318}X", message: Message::Action(Cut), }), MenuEntry::Item(MenuItem { label: "Copy", shortcut: "\u{2318}C", message: Message::Action(Copy), }), MenuEntry::Item(MenuItem { label: "Paste", shortcut: "\u{2318}V", message: Message::Action(Paste), }), MenuEntry::Item(MenuItem { label: "Duplicate", shortcut: "\u{2318}D", message: Message::Action(Duplicate), }), MenuEntry::Separator, MenuEntry::Item(MenuItem { label: "Select All", shortcut: "\u{2318}A", message: Message::Action(SelectAll), }), MenuEntry::Item(MenuItem { label: "Delete", shortcut: "\u{232B}", message: Message::Action(Delete), }), ] } fn transport_entries() -> Vec { use behaviors::Action::*; vec![ MenuEntry::Item(MenuItem { label: "Play/Pause", shortcut: "Space", message: Message::Action(EditorTogglePlayback), }), MenuEntry::Item(MenuItem { label: "Stop", shortcut: "", message: Message::Action(EditorStop), }), MenuEntry::Item(MenuItem { label: "Record", shortcut: "R", message: Message::Action(EditorToggleRecord), }), MenuEntry::Separator, MenuEntry::Item(MenuItem { label: "From Start", shortcut: "\u{21B5}", message: Message::Action(EditorPlayFromBeginning), }), MenuEntry::Item(MenuItem { label: "Rewind", shortcut: ",", message: Message::Action(EditorRewind), }), MenuEntry::Separator, MenuEntry::Item(MenuItem { label: "New Track\u{2026}", shortcut: "", message: Message::ShowNewTrackWizard, }), ] } fn view_entries() -> Vec { use behaviors::Action::*; vec![ MenuEntry::Item(MenuItem { label: "Inspector", shortcut: "I", message: Message::Action(EditorToggleInspector), }), MenuEntry::Item(MenuItem { label: "Bottom Panel", shortcut: "E", message: Message::Action(EditorToggleBottomPanel), }), MenuEntry::Item(MenuItem { label: "Mixer", shortcut: "X", message: Message::Action(EditorToggleMixer), }), MenuEntry::Separator, MenuEntry::Item(MenuItem { label: "Cycle", shortcut: "C", message: Message::Action(EditorToggleCycle), }), MenuEntry::Item(MenuItem { label: "Metronome", shortcut: "K", message: Message::Action(EditorToggleMetronome), }), MenuEntry::Separator, MenuEntry::Item(MenuItem { label: "Zoom In H", shortcut: "\u{2318}\u{2192}", message: Message::Action(ZoomInH), }), MenuEntry::Item(MenuItem { label: "Zoom Out H", shortcut: "\u{2318}\u{2190}", message: Message::Action(ZoomOutH), }), MenuEntry::Item(MenuItem { label: "Zoom In V", shortcut: "\u{2318}\u{2191}", message: Message::Action(ZoomInV), }), MenuEntry::Item(MenuItem { label: "Zoom Out V", shortcut: "\u{2318}\u{2193}", message: Message::Action(ZoomOutV), }), ] } fn entries_for(id: MenuId) -> Vec { match id { MenuId::File => file_entries(), MenuId::Edit => edit_entries(), MenuId::Transport => transport_entries(), MenuId::View => view_entries(), } } fn dropdown_x_offset(id: MenuId) -> f32 { match id { MenuId::File => 0.0, MenuId::Edit => TITLE_WIDTH, MenuId::Transport => TITLE_WIDTH * 2.0, MenuId::View => TITLE_WIDTH * 3.0, } } pub fn view(state: &State) -> Element<'_, EditorMessage> { let titles: Vec> = MenuId::ALL .iter() .map(|&id| { let is_open = state.open == Some(id); let label_color = if is_open { Color::WHITE } else { Color::from_rgb8(0xAA, 0xAA, 0xAA) }; button(text(id.label()).size(12).color(label_color)) .on_press(EditorMessage::MenuBar(Message::Open(id))) .padding([4, 12]) .width(TITLE_WIDTH) .style(move |_theme: &Theme, status| { let bg = match status { button::Status::Hovered | button::Status::Pressed => { Color::from_rgb8(0x2A, 0x2C, 0x2E) } _ if is_open => Color::from_rgb8(0x2A, 0x2C, 0x2E), _ => Color::TRANSPARENT, }; button::Style { background: Some(Background::Color(bg)), text_color: label_color, border: Border::default(), ..button::Style::default() } }) .into() }) .collect(); let bar = row(titles).spacing(0).align_y(Alignment::Center); container(bar) .width(Length::Fill) .height(BAR_HEIGHT) .style(|_theme: &Theme| container::Style { background: Some(Background::Color(Color::from_rgb8(0x1A, 0x1C, 0x1E))), ..container::Style::default() }) .into() } pub fn dropdown_view(state: &State) -> Option> { let menu_id = state.open?; let entries = entries_for(menu_id); let mut items: Column<'_, EditorMessage> = Column::new().spacing(0).width(200); for entry in entries { match entry { MenuEntry::Item(item) => { let msg = EditorMessage::MenuBar(item.message); let shortcut_el: Element<'_, EditorMessage> = if item.shortcut.is_empty() { Space::new(0, 0).into() } else { text(item.shortcut) .size(11) .color(Color::from_rgb8(0x66, 0x66, 0x66)) .into() }; let item_row = row![ text(item.label).size(12), Space::with_width(Length::Fill), shortcut_el, ] .align_y(Alignment::Center) .spacing(8); let item_btn = button(item_row) .on_press(msg) .width(Length::Fill) .padding([5, 12]) .style(|_theme: &Theme, status| { let bg = match status { button::Status::Hovered => Color::from_rgb8(0x00, 0x7A, 0xFF), button::Status::Pressed => Color::from_rgb8(0x00, 0x6A, 0xDD), _ => Color::TRANSPARENT, }; let text_color = match status { button::Status::Hovered | button::Status::Pressed => Color::WHITE, _ => Color::from_rgb8(0xCC, 0xCC, 0xCC), }; button::Style { background: Some(Background::Color(bg)), text_color, border: Border::default(), ..button::Style::default() } }); items = items.push(item_btn); } MenuEntry::Separator => { let line = container(Space::new(0, 0)) .width(Length::Fill) .height(1) .style(|_theme: &Theme| container::Style { background: Some(Background::Color(Color::from_rgb8(0x30, 0x32, 0x34))), ..container::Style::default() }); items = items.push(container(line).padding([4, 8])); } } } let dropdown = container(items) .style(|_theme: &Theme| container::Style { background: Some(Background::Color(Color::from_rgb8(0x22, 0x24, 0x26))), border: Border { color: Color::from_rgb8(0x35, 0x37, 0x39), width: 1.0, radius: 4.0.into(), }, ..container::Style::default() }) .padding(4); let offset = dropdown_x_offset(menu_id); let positioned: Element<'_, EditorMessage> = container(row![ Space::new(offset, 0), dropdown, Space::with_width(Length::Fill), ]) .width(Length::Fill) .height(Length::Fill) .padding(Padding { top: BAR_HEIGHT, right: 0.0, bottom: 0.0, left: 0.0, }) .into(); Some(positioned) }