diff --git a/linux/src/app.rs b/linux/src/app.rs index bd6df90..63eb8f5 100644 --- a/linux/src/app.rs +++ b/linux/src/app.rs @@ -19,6 +19,7 @@ use acord_viewport::{ }; use acord_viewport::browser::{self, BrowserHandle}; use acord_viewport::handle as viewport_handle; +use acord_viewport::editor::ShellAction; use crate::config::Config; use crate::shortcuts::{match_shortcut, MenuAction}; @@ -159,6 +160,22 @@ impl App { self.browser_window = None; } + fn drain_shell_actions(&mut self, event_loop: &ActiveEventLoop) { + if self.handle.is_null() { return; } + let action = unsafe { (*self.handle).state.take_pending_shell_action() }; + let Some(action) = action else { return }; + match action { + ShellAction::NewNote => self.new_note(), + ShellAction::Open => self.open_file(), + ShellAction::Save => self.save_file(), + ShellAction::SaveAs => self.save_file_as(), + ShellAction::Quit => event_loop.exit(), + ShellAction::Settings => self.dispatch_menu(MenuAction::Settings, event_loop), + ShellAction::ExportCrate => self.dispatch_menu(MenuAction::ExportCrate, event_loop), + ShellAction::ToggleBrowser => self.toggle_browser(event_loop), + } + } + fn drain_browser_open(&mut self) { let Some(handle) = self.browser_handle.as_mut() else { return }; let Some(path) = browser::handle::take_pending_open(handle) else { return }; @@ -499,6 +516,7 @@ impl ApplicationHandler for App { self.last_autosave_attempt = Instant::now(); self.try_autosave(); } + self.drain_shell_actions(_event_loop); self.drain_browser_open(); if let Some(w) = &self.window { if !self.handle.is_null() { diff --git a/viewport/src/editor.rs b/viewport/src/editor.rs index 7dddf53..622697e 100644 --- a/viewport/src/editor.rs +++ b/viewport/src/editor.rs @@ -173,6 +173,29 @@ pub enum Message { InlineResultRelease, /// double-click on an inline result InlineResultDoubleClick { block_id: crate::selection::BlockId, after_line: usize }, + ToggleMenu(MenuCategory), + CloseMenu, + Shell(ShellAction), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MenuCategory { + File, + Edit, + Render, + View, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ShellAction { + NewNote, + Open, + Save, + SaveAs, + Quit, + Settings, + ExportCrate, + ToggleBrowser, } pub const RESULT_PREFIX: &str = "→ "; @@ -391,6 +414,9 @@ pub struct EditorState { /// previous global cursor line, used to detect line changes prev_cursor_line: usize, + + pub menu_open: Option, + pub pending_shell_action: Option, } /// per-eval table name to id bookkeeping @@ -484,9 +510,16 @@ impl EditorState { computed_images: Vec::new(), image_cache: HashMap::new(), prev_cursor_line: 0, + menu_open: None, + pending_shell_action: None, } } + /// returns the queued shell action and clears it + pub fn take_pending_shell_action(&mut self) -> Option { + self.pending_shell_action.take() + } + fn vec_to_registry(blocks: Vec) -> (HashMap, Vec) { let mut registry = HashMap::with_capacity(blocks.len()); @@ -2332,6 +2365,7 @@ impl EditorState { | Message::TableMoveRight => true, Message::SelectAllBlocks => true, Message::ShowContextMenu { .. } | Message::HideContextMenu => true, + Message::ToggleMenu(_) | Message::CloseMenu | Message::Shell(_) => true, Message::CopyLiteral(_) | Message::CopyFocusedTableSelection => true, Message::InlineResultPress { .. } | Message::InlineResultRelease => true, Message::EvalAll => true, @@ -2365,6 +2399,14 @@ impl EditorState { self.context_menu = None; } + let preserve_menu_strip = matches!( + &message, + Message::ToggleMenu(_), + ); + if !preserve_menu_strip && self.menu_open.is_some() { + self.menu_open = None; + } + match message { Message::EditorAction(action) => { let is_edit = action.is_edit(); @@ -3029,6 +3071,16 @@ impl EditorState { Message::HideContextMenu => { self.context_menu = None; } + Message::ToggleMenu(cat) => { + self.menu_open = if self.menu_open == Some(cat) { None } else { Some(cat) }; + } + Message::CloseMenu => { + self.menu_open = None; + } + Message::Shell(action) => { + self.pending_shell_action = Some(action); + self.menu_open = None; + } Message::CopyLiteral(text) => { self.pending_clipboard = Some(text); } @@ -3267,6 +3319,9 @@ impl EditorState { let mut col_items: Vec> = Vec::new(); + #[cfg(target_os = "linux")] + col_items.push(self.menu_section()); + col_items.push(main_content); if self.find.visible { @@ -3916,6 +3971,184 @@ impl EditorState { .into() } + /// returns the menu strip, plus the open category's dropdown composed below it + #[cfg(target_os = "linux")] + fn menu_section(&self) -> Element<'_, Message, Theme, iced_wgpu::Renderer> { + let p = palette::current(); + let f = self.font_size; + let char_w = f * 0.6; + let cat_pad_x = f * 0.85; + let strip_pad_y = f * 0.18; + let item_pad_x = f * 0.95; + let item_pad_y = f * 0.32; + let dropdown_radius = f * 0.30; + let separator_h = (f * 0.08).max(1.0); + let label_size = f * 0.85; + let hint_size = f * 0.78; + let strip_label_size = f * 0.92; + + let cats: [(MenuCategory, &'static str); 4] = [ + (MenuCategory::File, "File"), + (MenuCategory::Edit, "Edit"), + (MenuCategory::Render, "Render"), + (MenuCategory::View, "View"), + ]; + + let cat_btn_width = |label: &str| -> f32 { + label.chars().count() as f32 * char_w + cat_pad_x * 2.0 + }; + + let mut strip_row: Vec> = Vec::new(); + for (cat, label) in cats { + let active = self.menu_open == Some(cat); + strip_row.push( + iced_widget::button( + iced_widget::text(label.to_string()) + .size(strip_label_size) + .font(syntax::EDITOR_FONT) + ) + .width(Length::Fixed(cat_btn_width(label))) + .padding(Padding { top: strip_pad_y, right: cat_pad_x, bottom: strip_pad_y, left: cat_pad_x }) + .style(move |_t: &Theme, _s| iced_widget::button::Style { + background: if active { Some(Background::Color(p.surface1)) } else { None }, + text_color: p.text, + border: Border::default(), + shadow: Shadow::default(), + snap: false, + }) + .on_press(Message::ToggleMenu(cat)) + .into() + ); + } + + let strip = iced_widget::container(iced_widget::row(strip_row).spacing(0.0)) + .width(Length::Fill) + .style(move |_t: &Theme| iced_widget::container::Style { + background: Some(Background::Color(p.mantle)), + border: Border::default(), + text_color: Some(p.text), + shadow: Shadow::default(), + snap: false, + }); + + let mut section: Vec> = vec![strip.into()]; + + if let Some(cat) = self.menu_open { + let item = |label: &str, shortcut: &str, msg: Message| -> Element<'_, Message, Theme, iced_wgpu::Renderer> { + let label_w = iced_widget::text(label.to_string()) + .size(label_size) + .font(syntax::EDITOR_FONT) + .width(Length::Fill); + let hint_w = iced_widget::text(shortcut.to_string()) + .size(hint_size) + .font(syntax::EDITOR_FONT) + .color(p.overlay0); + iced_widget::button( + iced_widget::row![label_w, hint_w].spacing(f) + ) + .width(Length::Fill) + .padding(Padding { top: item_pad_y, right: item_pad_x, bottom: item_pad_y, left: item_pad_x }) + .style(context_menu_item_style) + .on_press(msg) + .into() + }; + + let sep = || -> Element<'_, Message, Theme, iced_wgpu::Renderer> { + iced_widget::container(iced_widget::text("")) + .width(Length::Fill) + .height(Length::Fixed(separator_h)) + .style(move |_t: &Theme| iced_widget::container::Style { + background: Some(Background::Color(p.surface1)), + border: Border::default(), + text_color: None, + shadow: Shadow::default(), + snap: false, + }) + .into() + }; + + let items: Vec> = match cat { + MenuCategory::File => vec![ + item("New Note", "Ctrl+N", Message::Shell(ShellAction::NewNote)), + item("Open...", "Ctrl+O", Message::Shell(ShellAction::Open)), + item("Documents...", "Alt+B", Message::Shell(ShellAction::ToggleBrowser)), + sep(), + item("Save", "Ctrl+S", Message::Shell(ShellAction::Save)), + item("Save As...", "Ctrl+Shift+S", Message::Shell(ShellAction::SaveAs)), + sep(), + item("Export as Rust Library", "Ctrl+Shift+E", Message::Shell(ShellAction::ExportCrate)), + sep(), + item("Settings...", "Ctrl+,", Message::Shell(ShellAction::Settings)), + item("Quit", "Ctrl+Q", Message::Shell(ShellAction::Quit)), + ], + MenuCategory::Edit => vec![ + item("Undo", "Ctrl+Z", Message::Undo), + item("Redo", "Ctrl+Shift+Z", Message::Redo), + sep(), + item("Bold", "Ctrl+B", Message::ToggleBold), + item("Italic", "Ctrl+I", Message::ToggleItalic), + item("Insert Table", "Ctrl+T", Message::InsertTable), + sep(), + item("Find...", "Ctrl+F", Message::ToggleFind), + ], + MenuCategory::Render => vec![ + item("Live", "", Message::SetRenderMode(RenderMode::Live)), + item("Editor", "", Message::SetRenderMode(RenderMode::Editor)), + item("View", "", Message::SetRenderMode(RenderMode::View)), + sep(), + item("Evaluate", "Ctrl+E", Message::SmartEval), + ], + MenuCategory::View => vec![ + item("Zoom In", "Ctrl+=", Message::ZoomIn), + item("Zoom Out", "Ctrl+-", Message::ZoomOut), + item("Reset Zoom", "Ctrl+Shift+0", Message::ZoomReset), + ], + }; + + let mut x_offset = 0.0_f32; + for (c, label) in cats { + if c == cat { break; } + x_offset += cat_btn_width(label); + } + + let dropdown_width = { + let max_label_chars = match cat { + MenuCategory::File => "Export as Rust Library".len(), + MenuCategory::Edit => "Insert Table".len(), + MenuCategory::Render => "Evaluate".len(), + MenuCategory::View => "Reset Zoom".len(), + }; + let max_hint_chars = 13_usize; // widest hint string in chars + (max_label_chars + max_hint_chars) as f32 * char_w + item_pad_x * 2.0 + f + }; + + let dropdown_box = iced_widget::container( + iced_widget::column(items).spacing(0.0).width(Length::Fixed(dropdown_width)) + ) + .style(move |_t: &Theme| iced_widget::container::Style { + background: Some(Background::Color(p.surface0)), + border: Border { + color: p.surface1, + width: 1.0, + radius: dropdown_radius.into(), + }, + text_color: Some(p.text), + shadow: Shadow::default(), + snap: false, + }); + + let dropdown_row: Element<'_, Message, Theme, iced_wgpu::Renderer> = iced_widget::row![ + iced_widget::Space::new().width(Length::Fixed(x_offset)).height(Length::Shrink), + dropdown_box, + ] + .into(); + + section.push(dropdown_row); + } + + iced_widget::column(section).spacing(0.0).width(Length::Fill).into() + } + fn find_bar(&self) -> Element<'_, Message, Theme, iced_wgpu::Renderer> { let p = palette::current(); diff --git a/viewport/src/lib.rs b/viewport/src/lib.rs index 98b029c..c08d205 100644 --- a/viewport/src/lib.rs +++ b/viewport/src/lib.rs @@ -4,7 +4,7 @@ pub mod block; pub mod blocks; mod bridge; pub mod browser; -mod editor; +pub mod editor; pub mod export; pub mod handle; pub mod heading_block;