forked from jess/Acord
- 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.]
This commit is contained in:
parent
33ab38d5a9
commit
3d357209e6
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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<MenuCategory>,
|
||||
pub pending_shell_action: Option<ShellAction>,
|
||||
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<Element<'_, Message, Theme, iced_wgpu::Renderer>> = 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::<TextBlock>()).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<Element<'_, Message, Theme, iced_wgpu::Renderer>> = 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<Element<'a, Message, Theme, iced_wgpu::Renderer>> = 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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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<Window>,
|
||||
handle: *mut ViewportHandle,
|
||||
config: Config,
|
||||
_menu: Option<AppMenu>,
|
||||
cursor_pos: PhysicalPosition<f64>,
|
||||
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() {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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<MenuAction> {
|
||||
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,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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<MenuAction> {
|
||||
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')
|
||||
}
|
||||
Loading…
Reference in New Issue