From 3d357209e6d96f5979b856aae283196165c2bcdd Mon Sep 17 00:00:00 2001 From: jess Date: Wed, 29 Apr 2026 17:35:38 -0700 Subject: [PATCH] - Added Settings menu into windows and linux - Switched from 3 Menu implementations / removed Muda on Windows. [Iced Menu Bar for both Linux and Windows now.] --- linux/src/app.rs | 70 +++++++++++- linux/src/config.rs | 11 -- viewport/src/editor.rs | 235 +++++++++++++++++++++++++++++++++++++-- windows/Cargo.toml | 1 - windows/src/app.rs | 141 ++++++++++++++++------- windows/src/main.rs | 2 +- windows/src/menu.rs | 153 ------------------------- windows/src/shortcuts.rs | 66 +++++++++++ 8 files changed, 455 insertions(+), 224 deletions(-) delete mode 100644 windows/src/menu.rs create mode 100644 windows/src/shortcuts.rs diff --git a/linux/src/app.rs b/linux/src/app.rs index 94c1013..3c05483 100644 --- a/linux/src/app.rs +++ b/linux/src/app.rs @@ -60,7 +60,7 @@ impl App { } } - fn sync_settings(&self) { + fn sync_settings(&mut self) { if self.handle.is_null() { return; } let theme = match self.config.theme_mode() { "dark" => "kicad", @@ -74,6 +74,14 @@ impl App { viewport_set_line_indicator(self.handle, ind.as_ptr()); viewport_set_gutter_rainbow(self.handle, self.config.gutter_rainbow()); viewport_set_auto_pair_flags(self.handle, self.config.auto_pair_flags()); + + let view = acord_viewport::editor::SettingsView { + theme_mode: self.config.theme_mode().to_string(), + line_indicator: self.config.line_indicator().to_string(), + gutter_rainbow: self.config.gutter_rainbow(), + auto_save_dir: self.config.notes_dir().to_string_lossy().into_owned(), + }; + unsafe { (*self.handle).state.settings_view = view; } } fn dispatch_menu(&mut self, action: MenuAction, event_loop: &ActiveEventLoop) { @@ -170,9 +178,29 @@ impl App { 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::Settings => {} ShellAction::ExportCrate => self.dispatch_menu(MenuAction::ExportCrate, event_loop), ShellAction::ToggleBrowser => self.toggle_browser(event_loop), + ShellAction::SetThemeMode(v) => { + self.config.set("themeMode", &v); + self.sync_settings(); + } + ShellAction::SetLineIndicator(v) => { + self.config.set("lineIndicatorMode", &v); + self.sync_settings(); + } + ShellAction::SetGutterRainbow(b) => { + self.config.set("gutterRainbow", if b { "true" } else { "false" }); + self.sync_settings(); + } + ShellAction::PickAutoSaveDir => { + let dialog = rfd::FileDialog::new() + .set_directory(self.config.notes_dir()); + if let Some(path) = dialog.pick_folder() { + self.config.set("autoSaveDirectory", &path.to_string_lossy()); + self.sync_settings(); + } + } } } @@ -294,10 +322,15 @@ impl App { } fn save_file(&mut self) { - match self.current_file.clone() { - Some(path) => self.write_to(&path), - None => self.save_file_as(), + if let Some(path) = self.current_file.clone() { + self.write_to(&path); + return; } + let notes_dir = self.config.notes_dir(); + let _ = std::fs::create_dir_all(¬es_dir); + let path = notes_dir.join(format!("{}.md", self.derive_default_filename())); + self.write_to(&path); + self.current_file = Some(path); } fn save_file_as(&mut self) { @@ -307,7 +340,7 @@ impl App { .add_filter("Markdown", &["md"]) .add_filter("All Files", &["*"]) .set_directory(¬es_dir) - .set_file_name("note.md"); + .set_file_name(format!("{}.md", self.derive_default_filename())); if let Some(path) = dialog.save_file() { self.write_to(&path); self.current_file = Some(path); @@ -326,6 +359,31 @@ impl App { } } + fn derive_default_filename(&self) -> String { + let text_ptr = viewport_get_text(self.handle); + let text = if text_ptr.is_null() { + String::new() + } else { + let s = unsafe { std::ffi::CStr::from_ptr(text_ptr) } + .to_string_lossy() + .into_owned(); + viewport_free_string(text_ptr); + s + }; + let title = text.lines().next().unwrap_or("").trim_start(); + let title = title.trim_start_matches('#').trim(); + let cleaned: String = title + .chars() + .filter(|c| !matches!(c, '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|')) + .collect(); + let cleaned = cleaned.trim(); + if cleaned.is_empty() { + "Untitled".to_string() + } else { + cleaned.chars().take(60).collect() + } + } + fn new_note(&mut self) { viewport_send_command(self.handle, 12); let stub = CString::new("# ").unwrap(); diff --git a/linux/src/config.rs b/linux/src/config.rs index a1ac1ea..dd53870 100644 --- a/linux/src/config.rs +++ b/linux/src/config.rs @@ -63,18 +63,7 @@ impl Config { } } -/// XDG-friendly config dir with `~/.acord` fallback for parity with the -/// Windows shell. `$XDG_CONFIG_HOME/acord` if set, else `~/.config/acord`, -/// else `~/.acord`. fn config_dir() -> PathBuf { - if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") { - if !xdg.is_empty() { - return PathBuf::from(xdg).join("acord"); - } - } - if let Some(cfg) = dirs::config_dir() { - return cfg.join("acord"); - } dirs::home_dir() .unwrap_or_else(|| PathBuf::from(".")) .join(".acord") diff --git a/viewport/src/editor.rs b/viewport/src/editor.rs index c32d89d..3e3b51f 100644 --- a/viewport/src/editor.rs +++ b/viewport/src/editor.rs @@ -186,7 +186,7 @@ pub enum MenuCategory { View, } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum ShellAction { NewNote, Open, @@ -196,9 +196,13 @@ pub enum ShellAction { Settings, ExportCrate, ToggleBrowser, + SetThemeMode(String), + SetLineIndicator(String), + SetGutterRainbow(bool), + PickAutoSaveDir, } -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "windows"))] const MENU_CATS: [(MenuCategory, &'static str); 4] = [ (MenuCategory::File, "File"), (MenuCategory::Edit, "Edit"), @@ -206,7 +210,7 @@ const MENU_CATS: [(MenuCategory, &'static str); 4] = [ (MenuCategory::View, "View"), ]; -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "windows"))] fn cat_btn_width(label: &str, char_w: f32, pad_x: f32) -> f32 { label.chars().count() as f32 * char_w + pad_x * 2.0 } @@ -430,6 +434,27 @@ pub struct EditorState { pub menu_open: Option, pub pending_shell_action: Option, + pub settings_open: bool, + pub settings_view: SettingsView, +} + +#[derive(Debug, Clone)] +pub struct SettingsView { + pub theme_mode: String, + pub line_indicator: String, + pub gutter_rainbow: bool, + pub auto_save_dir: String, +} + +impl Default for SettingsView { + fn default() -> Self { + Self { + theme_mode: "auto".to_string(), + line_indicator: "on".to_string(), + gutter_rainbow: true, + auto_save_dir: String::new(), + } + } } /// per-eval table name to id bookkeeping @@ -525,6 +550,8 @@ impl EditorState { prev_cursor_line: 0, menu_open: None, pending_shell_action: None, + settings_open: false, + settings_view: SettingsView::default(), } } @@ -3091,8 +3118,15 @@ impl EditorState { self.menu_open = None; } Message::Shell(action) => { - self.pending_shell_action = Some(action); self.menu_open = None; + match action { + ShellAction::Settings => { + self.settings_open = !self.settings_open; + } + other => { + self.pending_shell_action = Some(other); + } + } } Message::CopyLiteral(text) => { self.pending_clipboard = Some(text); @@ -3332,7 +3366,7 @@ impl EditorState { let mut col_items: Vec> = Vec::new(); - #[cfg(target_os = "linux")] + #[cfg(any(target_os = "linux", target_os = "windows"))] col_items.push(self.menu_strip()); col_items.push(main_content); @@ -3348,7 +3382,12 @@ impl EditorState { .height(Length::Fill) .into(); - #[cfg(target_os = "linux")] + #[cfg(any(target_os = "linux", target_os = "windows"))] + if self.settings_open { + return iced_widget::stack![body, self.settings_panel()].into(); + } + + #[cfg(any(target_os = "linux", target_os = "windows"))] if let Some(cat) = self.menu_open { return iced_widget::stack![body, self.menu_dropdown(cat)].into(); } @@ -3364,9 +3403,9 @@ impl EditorState { && self.block_at(0).map(|b| b.as_any().is::()).unwrap_or(false) && !has_computed_layers; - #[cfg(target_os = "linux")] + #[cfg(any(target_os = "linux", target_os = "windows"))] let title_bar_h = 0.0_f32; - #[cfg(not(target_os = "linux"))] + #[cfg(not(any(target_os = "linux", target_os = "windows")))] let title_bar_h = 38.0_f32; let mut block_elements: Vec> = Vec::new(); @@ -3994,7 +4033,7 @@ impl EditorState { .into() } - #[cfg(target_os = "linux")] + #[cfg(any(target_os = "linux", target_os = "windows"))] fn menu_strip(&self) -> Element<'_, Message, Theme, iced_wgpu::Renderer> { let p = palette::current(); let f = self.font_size; @@ -4039,7 +4078,7 @@ impl EditorState { } /// returns the dropdown panel for the open category, anchored under its strip button - #[cfg(target_os = "linux")] + #[cfg(any(target_os = "linux", target_os = "windows"))] fn menu_dropdown(&self, cat: MenuCategory) -> Element<'_, Message, Theme, iced_wgpu::Renderer> { let p = palette::current(); let f = self.font_size; @@ -4169,6 +4208,182 @@ impl EditorState { .into() } + #[cfg(any(target_os = "linux", target_os = "windows"))] + fn settings_panel(&self) -> Element<'_, Message, Theme, iced_wgpu::Renderer> { + let p = palette::current(); + let f = self.font_size; + let item_pad_x = f * 0.95; + let item_pad_y = f * 0.32; + let panel_radius = f * 0.30; + let label_size = f * 0.92; + let title_size = f * 1.05; + let row_gap = f * 0.55; + let panel_width = f * 28.0; + + let title = iced_widget::text("Settings") + .size(title_size) + .font(syntax::EDITOR_FONT) + .color(p.text); + + let theme_row = self.settings_segment_row( + "Theme", + label_size, + &[ + ("Auto", "auto"), + ("Light", "light"), + ("Dark", "dark"), + ], + &self.settings_view.theme_mode, + |v| Message::Shell(ShellAction::SetThemeMode(v.to_string())), + ); + + let line_row = self.settings_segment_row( + "Line indicator", + label_size, + &[ + ("Off", "off"), + ("Line", "line"), + ("On", "on"), + ], + &self.settings_view.line_indicator, + |v| Message::Shell(ShellAction::SetLineIndicator(v.to_string())), + ); + + let rainbow_row = self.settings_segment_row( + "Gutter rainbow", + label_size, + &[ + ("Off", "false"), + ("On", "true"), + ], + if self.settings_view.gutter_rainbow { "true" } else { "false" }, + |v| Message::Shell(ShellAction::SetGutterRainbow(v == "true")), + ); + + let dir_label = iced_widget::text("Auto-save folder") + .size(label_size) + .font(syntax::EDITOR_FONT) + .color(p.text) + .width(Length::Fill); + let dir_value = iced_widget::text(self.settings_view.auto_save_dir.clone()) + .size(label_size) + .font(syntax::EDITOR_FONT) + .color(p.subtext0) + .width(Length::Fill); + let dir_btn = iced_widget::button( + iced_widget::text("Choose…") + .size(label_size) + .font(syntax::EDITOR_FONT) + ) + .padding(Padding { top: item_pad_y * 0.6, right: item_pad_x * 0.7, bottom: item_pad_y * 0.6, left: item_pad_x * 0.7 }) + .on_press(Message::Shell(ShellAction::PickAutoSaveDir)) + .style(context_menu_item_style); + let dir_row: Element<'_, Message, Theme, iced_wgpu::Renderer> = iced_widget::column![ + dir_label, + iced_widget::row![dir_value, dir_btn].spacing(f * 0.5), + ] + .spacing(f * 0.2) + .into(); + + let close_btn = iced_widget::button( + iced_widget::text("Close") + .size(label_size) + .font(syntax::EDITOR_FONT) + ) + .padding(Padding { top: item_pad_y * 0.6, right: item_pad_x, bottom: item_pad_y * 0.6, left: item_pad_x }) + .on_press(Message::Shell(ShellAction::Settings)) + .style(context_menu_item_style); + + let panel = iced_widget::container( + iced_widget::column![ + title, + theme_row, + line_row, + rainbow_row, + dir_row, + iced_widget::row![ + iced_widget::Space::new().width(Length::Fill).height(Length::Shrink), + close_btn, + ], + ] + .spacing(row_gap) + .width(Length::Fixed(panel_width)) + ) + .padding(Padding { top: f, right: f, bottom: f, left: f }) + .style(move |_t: &Theme| iced_widget::container::Style { + background: Some(Background::Color(p.surface0)), + border: Border { + color: p.surface1, + width: 1.0, + radius: panel_radius.into(), + }, + text_color: Some(p.text), + shadow: Shadow::default(), + snap: false, + }); + + iced_widget::container(panel) + .width(Length::Fill) + .height(Length::Fill) + .center_x(Length::Fill) + .center_y(Length::Fill) + .style(move |_t: &Theme| iced_widget::container::Style { + background: Some(Background::Color(Color { r: 0.0, g: 0.0, b: 0.0, a: 0.4 })), + border: Border::default(), + text_color: None, + shadow: Shadow::default(), + snap: false, + }) + .into() + } + + #[cfg(any(target_os = "linux", target_os = "windows"))] + fn settings_segment_row<'a>( + &'a self, + label: &str, + label_size: f32, + options: &[(&str, &'a str)], + current: &str, + msg_for: impl Fn(&'a str) -> Message, + ) -> Element<'a, Message, Theme, iced_wgpu::Renderer> { + let p = palette::current(); + let f = self.font_size; + let mut buttons: Vec> = Vec::new(); + for (display, value) in options { + let active = *value == current; + let display = display.to_string(); + let value = *value; + buttons.push( + iced_widget::button( + iced_widget::text(display) + .size(label_size) + .font(syntax::EDITOR_FONT) + ) + .padding(Padding { top: f * 0.18, right: f * 0.55, bottom: f * 0.18, left: f * 0.55 }) + .style(move |_t: &Theme, _s| iced_widget::button::Style { + background: if active { Some(Background::Color(p.surface2)) } else { Some(Background::Color(p.surface1)) }, + text_color: if active { p.text } else { p.subtext0 }, + border: Border { color: p.surface2, width: 1.0, radius: (f * 0.18).into() }, + shadow: Shadow::default(), + snap: false, + }) + .on_press(msg_for(value)) + .into() + ); + } + let label_w = iced_widget::text(label.to_string()) + .size(label_size) + .font(syntax::EDITOR_FONT) + .color(p.text) + .width(Length::Fill); + iced_widget::row![ + label_w, + iced_widget::row(buttons).spacing(f * 0.25), + ] + .spacing(f) + .into() + } + fn find_bar(&self) -> Element<'_, Message, Theme, iced_wgpu::Renderer> { let p = palette::current(); diff --git a/windows/Cargo.toml b/windows/Cargo.toml index f1a63f7..461940f 100644 --- a/windows/Cargo.toml +++ b/windows/Cargo.toml @@ -11,7 +11,6 @@ path = "src/main.rs" acord-core = { path = "../core" } acord-viewport = { path = "../viewport" } winit = "0.30" -muda = "0.16" arboard = "3" rfd = "0.15" raw-window-handle = "0.6" diff --git a/windows/src/app.rs b/windows/src/app.rs index e0d1742..7a5ac03 100644 --- a/windows/src/app.rs +++ b/windows/src/app.rs @@ -22,13 +22,13 @@ use acord_viewport::{ use acord_viewport::browser::{self, BrowserHandle}; use crate::config::Config; -use crate::menu::{AppMenu, MenuAction}; +use crate::shortcuts::{match_shortcut, MenuAction}; +use acord_viewport::editor::ShellAction; pub struct App { window: Option, handle: *mut ViewportHandle, config: Config, - _menu: Option, cursor_pos: PhysicalPosition, scale: f32, modifiers: ModifiersState, @@ -48,7 +48,6 @@ impl App { window: None, handle: std::ptr::null_mut(), config: Config::load(), - _menu: None, cursor_pos: PhysicalPosition::new(0.0, 0.0), scale: 1.0, modifiers: ModifiersState::empty(), @@ -62,12 +61,12 @@ impl App { } } - fn sync_settings(&self) { + fn sync_settings(&mut self) { if self.handle.is_null() { return; } let theme = match self.config.theme_mode() { "dark" => "kicad", "light" => "latte", - _ => "kicad", // Windows: default dark. No NSAppearance auto-detect. + _ => "kicad", }; let c_theme = CString::new(theme).unwrap(); viewport_set_theme(self.handle, c_theme.as_ptr()); @@ -76,6 +75,14 @@ impl App { viewport_set_line_indicator(self.handle, ind.as_ptr()); viewport_set_gutter_rainbow(self.handle, self.config.gutter_rainbow()); viewport_set_auto_pair_flags(self.handle, self.config.auto_pair_flags()); + + let view = acord_viewport::editor::SettingsView { + theme_mode: self.config.theme_mode().to_string(), + line_indicator: self.config.line_indicator().to_string(), + gutter_rainbow: self.config.gutter_rainbow(), + auto_save_dir: self.config.notes_dir().to_string_lossy().into_owned(), + }; + unsafe { (*self.handle).state.settings_view = view; } } fn dispatch_menu(&mut self, action: MenuAction, event_loop: &ActiveEventLoop) { @@ -97,26 +104,47 @@ impl App { MenuAction::Save => self.save_file(), MenuAction::SaveAs => self.save_file_as(), MenuAction::NewNote => self.new_note(), - MenuAction::Settings => { - // Open config file in the default editor. - let cfg = dirs::home_dir() - .unwrap_or_default() - .join(".acord") - .join("config.json"); - let _ = std::process::Command::new("notepad").arg(&cfg).spawn(); + MenuAction::Settings => unsafe { + (*self.handle).state.settings_open = !(*self.handle).state.settings_open; + }, + MenuAction::ExportCrate => {} + MenuAction::ToggleBrowser => self.toggle_browser(event_loop), + } + } + + 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 => {} + ShellAction::ExportCrate => {} + ShellAction::ToggleBrowser => self.toggle_browser(event_loop), + ShellAction::SetThemeMode(v) => { + self.config.set("themeMode", &v); + self.sync_settings(); } - MenuAction::Undo => { /* TODO */ }, - MenuAction::Redo => { /* TODO */ }, - MenuAction::ExportCrate => { /* TODO */ }, - MenuAction::ToggleAutoPair(bit) => { - let new_flags = self.config.auto_pair_flags() ^ bit; - self.config.set_auto_pair_flags(new_flags); - viewport_set_auto_pair_flags(self.handle, new_flags); - if let Some(menu) = &self._menu { - menu.set_auto_pair_check(bit, (new_flags & bit) != 0); + ShellAction::SetLineIndicator(v) => { + self.config.set("lineIndicatorMode", &v); + self.sync_settings(); + } + ShellAction::SetGutterRainbow(b) => { + self.config.set("gutterRainbow", if b { "true" } else { "false" }); + self.sync_settings(); + } + ShellAction::PickAutoSaveDir => { + let dialog = rfd::FileDialog::new() + .set_directory(self.config.notes_dir()); + if let Some(path) = dialog.pick_folder() { + self.config.set("autoSaveDirectory", &path.to_string_lossy()); + self.sync_settings(); } } - MenuAction::ToggleBrowser => self.toggle_browser(event_loop), } } @@ -217,17 +245,25 @@ impl App { } fn save_file(&mut self) { - match self.current_file.clone() { - Some(path) => self.write_to(&path), - None => self.save_file_as(), + if let Some(path) = self.current_file.clone() { + self.write_to(&path); + return; } + let notes_dir = self.config.notes_dir(); + let _ = std::fs::create_dir_all(¬es_dir); + let path = notes_dir.join(format!("{}.md", self.derive_default_filename())); + self.write_to(&path); + self.current_file = Some(path); } fn save_file_as(&mut self) { + let notes_dir = self.config.notes_dir(); + let _ = std::fs::create_dir_all(¬es_dir); let dialog = rfd::FileDialog::new() .add_filter("Markdown", &["md"]) .add_filter("All Files", &["*"]) - .set_file_name("note.md"); + .set_directory(¬es_dir) + .set_file_name(format!("{}.md", self.derive_default_filename())); if let Some(path) = dialog.save_file() { self.write_to(&path); self.current_file = Some(path); @@ -246,6 +282,31 @@ impl App { } } + fn derive_default_filename(&self) -> String { + let text_ptr = viewport_get_text(self.handle); + let text = if text_ptr.is_null() { + String::new() + } else { + let s = unsafe { std::ffi::CStr::from_ptr(text_ptr) } + .to_string_lossy() + .into_owned(); + viewport_free_string(text_ptr); + s + }; + let title = text.lines().next().unwrap_or("").trim_start(); + let title = title.trim_start_matches('#').trim(); + let cleaned: String = title + .chars() + .filter(|c| !matches!(c, '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|')) + .collect(); + let cleaned = cleaned.trim(); + if cleaned.is_empty() { + "Untitled".to_string() + } else { + cleaned.chars().take(60).collect() + } + } + fn new_note(&mut self) { viewport_send_command(self.handle, 12); let stub = CString::new("# ").unwrap(); @@ -416,19 +477,9 @@ impl ApplicationHandler for App { self.handle = viewport_create(hwnd, w, h, self.scale); self.sync_settings(); - - let app_menu = AppMenu::new(self.config.auto_pair_flags()); - #[cfg(target_os = "windows")] - { - if let raw_window_handle::RawWindowHandle::Win32(h) = raw { - let theme = match self.config.theme_mode() { - "light" => muda::MenuTheme::Light, - _ => muda::MenuTheme::Dark, - }; - unsafe { app_menu.menu.init_for_hwnd_with_theme(h.hwnd.get(), theme).ok(); } - } - } - self._menu = Some(app_menu); + viewport_send_command(self.handle, 12); + let stub = CString::new("# ").unwrap(); + viewport_set_text(self.handle, stub.as_ptr()); self.window = Some(window); } @@ -500,6 +551,14 @@ impl ApplicationHandler for App { WindowEvent::KeyboardInput { event, .. } => { let pressed = event.state == ElementState::Pressed; + + if pressed { + if let Some(action) = match_shortcut(self.modifiers, &event.logical_key) { + self.dispatch_menu(action, event_loop); + return; + } + } + let text_str = event.text.as_ref().map(|s| s.to_string()); let text_c = text_str.as_deref() .and_then(|s| CString::new(s).ok()); @@ -534,13 +593,11 @@ impl ApplicationHandler for App { } fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { - while let Some(action) = AppMenu::poll() { - self.dispatch_menu(action, _event_loop); - } if self.last_autosave_attempt.elapsed() >= Duration::from_millis(500) { 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/windows/src/main.rs b/windows/src/main.rs index 04a1859..60942fc 100644 --- a/windows/src/main.rs +++ b/windows/src/main.rs @@ -3,7 +3,7 @@ mod app; mod config; -mod menu; +mod shortcuts; fn main() { let event_loop = winit::event_loop::EventLoop::new().expect("event loop"); diff --git a/windows/src/menu.rs b/windows/src/menu.rs deleted file mode 100644 index 6bf84dc..0000000 --- a/windows/src/menu.rs +++ /dev/null @@ -1,153 +0,0 @@ -use muda::{CheckMenuItem, Menu, MenuEvent, MenuItem, PredefinedMenuItem, Submenu, accelerator::Accelerator}; -use muda::accelerator::{Code, Modifiers}; - -pub const AP_PAREN: u32 = 1; -pub const AP_BRACKET: u32 = 2; -pub const AP_BRACE: u32 = 4; -pub const AP_SINGLE: u32 = 8; -pub const AP_DOUBLE: u32 = 16; -pub const AP_BACKTICK: u32 = 32; - -pub struct AppMenu { - #[allow(dead_code)] - pub menu: Menu, - auto_pair_items: Vec<(u32, CheckMenuItem)>, -} - -pub enum MenuAction { - NewNote, - Open, - Save, - SaveAs, - Quit, - Undo, - Redo, - Bold, - Italic, - InsertTable, - Evaluate, - LiveMode, - EditorMode, - ViewMode, - ZoomIn, - ZoomOut, - ZoomReset, - Find, - Settings, - ExportCrate, - ToggleAutoPair(u32), - ToggleBrowser, -} - -impl AppMenu { - pub fn new(auto_pair_flags: u32) -> Self { - let menu = Menu::new(); - - let file = Submenu::new("File", true); - file.append(&MenuItem::with_id("new", "New Note", true, Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyN)))).ok(); - file.append(&MenuItem::with_id("open", "Open...", true, Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyO)))).ok(); - file.append(&MenuItem::with_id("browse", "Documents...", true, Some(Accelerator::new(Some(Modifiers::ALT), Code::KeyB)))).ok(); - file.append(&PredefinedMenuItem::separator()).ok(); - file.append(&MenuItem::with_id("save", "Save", true, Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyS)))).ok(); - file.append(&MenuItem::with_id("save_as", "Save As...", true, Some(Accelerator::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::KeyS)))).ok(); - file.append(&PredefinedMenuItem::separator()).ok(); - file.append(&MenuItem::with_id("export_crate", "Export as Rust Library", true, Some(Accelerator::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::KeyE)))).ok(); - file.append(&PredefinedMenuItem::separator()).ok(); - file.append(&MenuItem::with_id("quit", "Quit", true, Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyQ)))).ok(); - - let edit = Submenu::new("Edit", true); - edit.append(&MenuItem::with_id("undo", "Undo", true, Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyZ)))).ok(); - edit.append(&MenuItem::with_id("redo", "Redo", true, Some(Accelerator::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::KeyZ)))).ok(); - edit.append(&PredefinedMenuItem::separator()).ok(); - edit.append(&PredefinedMenuItem::cut(None)).ok(); - edit.append(&PredefinedMenuItem::copy(None)).ok(); - edit.append(&PredefinedMenuItem::paste(None)).ok(); - edit.append(&PredefinedMenuItem::select_all(None)).ok(); - edit.append(&PredefinedMenuItem::separator()).ok(); - edit.append(&MenuItem::with_id("find", "Find...", true, Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyF)))).ok(); - edit.append(&PredefinedMenuItem::separator()).ok(); - edit.append(&MenuItem::with_id("settings", "Settings...", true, Some(Accelerator::new(Some(Modifiers::CONTROL), Code::Comma)))).ok(); - edit.append(&PredefinedMenuItem::separator()).ok(); - edit.append(&MenuItem::with_id("bold", "Bold", true, Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyB)))).ok(); - edit.append(&MenuItem::with_id("italic", "Italic", true, Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyI)))).ok(); - edit.append(&MenuItem::with_id("table", "Insert Table", true, Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyT)))).ok(); - - edit.append(&PredefinedMenuItem::separator()).ok(); - let auto_pair_sub = Submenu::new("Auto Pair", true); - let pair_specs: [(u32, &str, &str); 6] = [ - (AP_PAREN, "ap_paren", "Parens ( )"), - (AP_BRACKET, "ap_bracket", "Brackets [ ]"), - (AP_BRACE, "ap_brace", "Braces { }"), - (AP_SINGLE, "ap_single", "Single quotes ' '"), - (AP_DOUBLE, "ap_double", "Double quotes \" \""), - (AP_BACKTICK, "ap_backtick", "Backticks ` `"), - ]; - let mut auto_pair_items: Vec<(u32, CheckMenuItem)> = Vec::with_capacity(6); - for (bit, id, label) in pair_specs { - let item = CheckMenuItem::with_id(id, label, true, (auto_pair_flags & bit) != 0, None); - auto_pair_sub.append(&item).ok(); - auto_pair_items.push((bit, item)); - } - edit.append(&auto_pair_sub).ok(); - - let render = Submenu::new("Render", true); - render.append(&MenuItem::with_id("live", "Live", true, None)).ok(); - render.append(&MenuItem::with_id("editor", "Editor", true, None)).ok(); - render.append(&MenuItem::with_id("view", "View", true, None)).ok(); - render.append(&PredefinedMenuItem::separator()).ok(); - render.append(&MenuItem::with_id("eval", "Evaluate", true, Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyE)))).ok(); - - let view = Submenu::new("View", true); - view.append(&MenuItem::with_id("zoom_in", "Zoom In", true, Some(Accelerator::new(Some(Modifiers::CONTROL), Code::Equal)))).ok(); - view.append(&MenuItem::with_id("zoom_out", "Zoom Out", true, Some(Accelerator::new(Some(Modifiers::CONTROL), Code::Minus)))).ok(); - view.append(&MenuItem::with_id("zoom_reset", "Reset Zoom", true, Some(Accelerator::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::Digit0)))).ok(); - - menu.append(&file).ok(); - menu.append(&edit).ok(); - menu.append(&render).ok(); - menu.append(&view).ok(); - - Self { menu, auto_pair_items } - } - - pub fn set_auto_pair_check(&self, bit: u32, checked: bool) { - for (b, item) in &self.auto_pair_items { - if *b == bit { item.set_checked(checked); } - } - } - - pub fn poll() -> Option { - MenuEvent::receiver().try_recv().ok().and_then(|e| { - match e.id().0.as_str() { - "new" => Some(MenuAction::NewNote), - "open" => Some(MenuAction::Open), - "browse" => Some(MenuAction::ToggleBrowser), - "save" => Some(MenuAction::Save), - "save_as" => Some(MenuAction::SaveAs), - "quit" => Some(MenuAction::Quit), - "undo" => Some(MenuAction::Undo), - "redo" => Some(MenuAction::Redo), - "bold" => Some(MenuAction::Bold), - "italic" => Some(MenuAction::Italic), - "table" => Some(MenuAction::InsertTable), - "eval" => Some(MenuAction::Evaluate), - "live" => Some(MenuAction::LiveMode), - "editor" => Some(MenuAction::EditorMode), - "view" => Some(MenuAction::ViewMode), - "zoom_in" => Some(MenuAction::ZoomIn), - "zoom_out" => Some(MenuAction::ZoomOut), - "zoom_reset" => Some(MenuAction::ZoomReset), - "find" => Some(MenuAction::Find), - "settings" => Some(MenuAction::Settings), - "export_crate" => Some(MenuAction::ExportCrate), - "ap_paren" => Some(MenuAction::ToggleAutoPair(AP_PAREN)), - "ap_bracket" => Some(MenuAction::ToggleAutoPair(AP_BRACKET)), - "ap_brace" => Some(MenuAction::ToggleAutoPair(AP_BRACE)), - "ap_single" => Some(MenuAction::ToggleAutoPair(AP_SINGLE)), - "ap_double" => Some(MenuAction::ToggleAutoPair(AP_DOUBLE)), - "ap_backtick" => Some(MenuAction::ToggleAutoPair(AP_BACKTICK)), - _ => None, - } - }) - } -} diff --git a/windows/src/shortcuts.rs b/windows/src/shortcuts.rs new file mode 100644 index 0000000..d8c2ac0 --- /dev/null +++ b/windows/src/shortcuts.rs @@ -0,0 +1,66 @@ +use winit::keyboard::{Key, ModifiersState, SmolStr}; + +#[derive(Clone, Copy)] +#[allow(dead_code)] +pub enum MenuAction { + NewNote, + Open, + Save, + SaveAs, + Quit, + Bold, + Italic, + InsertTable, + Evaluate, + LiveMode, + EditorMode, + ViewMode, + ZoomIn, + ZoomOut, + ZoomReset, + Find, + Settings, + ExportCrate, + ToggleBrowser, +} + +pub fn match_shortcut(modifiers: ModifiersState, key: &Key) -> Option { + if modifiers.alt_key() && !modifiers.control_key() && !modifiers.super_key() { + if let Key::Character(s) = key { + if ascii_lower(s) == 'b' { + return Some(MenuAction::ToggleBrowser); + } + } + } + + if !modifiers.control_key() { + return None; + } + let shift = modifiers.shift_key(); + + match key { + Key::Character(s) => match (shift, ascii_lower(s)) { + (false, 'n') => Some(MenuAction::NewNote), + (false, 'o') => Some(MenuAction::Open), + (false, 's') => Some(MenuAction::Save), + (true, 's') => Some(MenuAction::SaveAs), + (false, 'q') => Some(MenuAction::Quit), + (false, 'b') => Some(MenuAction::Bold), + (false, 'i') => Some(MenuAction::Italic), + (false, 't') => Some(MenuAction::InsertTable), + (false, 'f') => Some(MenuAction::Find), + (false, 'e') => Some(MenuAction::Evaluate), + (true, 'e') => Some(MenuAction::ExportCrate), + (false, ',') => Some(MenuAction::Settings), + (false, '=') | (false, '+') => Some(MenuAction::ZoomIn), + (false, '-') => Some(MenuAction::ZoomOut), + (true, '0') => Some(MenuAction::ZoomReset), + _ => None, + }, + _ => None, + } +} + +fn ascii_lower(s: &SmolStr) -> char { + s.chars().next().map(|c| c.to_ascii_lowercase()).unwrap_or('\0') +}