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; }
|
if self.handle.is_null() { return; }
|
||||||
let theme = match self.config.theme_mode() {
|
let theme = match self.config.theme_mode() {
|
||||||
"dark" => "kicad",
|
"dark" => "kicad",
|
||||||
|
|
@ -74,6 +74,14 @@ impl App {
|
||||||
viewport_set_line_indicator(self.handle, ind.as_ptr());
|
viewport_set_line_indicator(self.handle, ind.as_ptr());
|
||||||
viewport_set_gutter_rainbow(self.handle, self.config.gutter_rainbow());
|
viewport_set_gutter_rainbow(self.handle, self.config.gutter_rainbow());
|
||||||
viewport_set_auto_pair_flags(self.handle, self.config.auto_pair_flags());
|
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) {
|
fn dispatch_menu(&mut self, action: MenuAction, event_loop: &ActiveEventLoop) {
|
||||||
|
|
@ -170,9 +178,29 @@ impl App {
|
||||||
ShellAction::Save => self.save_file(),
|
ShellAction::Save => self.save_file(),
|
||||||
ShellAction::SaveAs => self.save_file_as(),
|
ShellAction::SaveAs => self.save_file_as(),
|
||||||
ShellAction::Quit => event_loop.exit(),
|
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::ExportCrate => self.dispatch_menu(MenuAction::ExportCrate, event_loop),
|
||||||
ShellAction::ToggleBrowser => self.toggle_browser(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) {
|
fn save_file(&mut self) {
|
||||||
match self.current_file.clone() {
|
if let Some(path) = self.current_file.clone() {
|
||||||
Some(path) => self.write_to(&path),
|
self.write_to(&path);
|
||||||
None => self.save_file_as(),
|
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) {
|
fn save_file_as(&mut self) {
|
||||||
|
|
@ -307,7 +340,7 @@ impl App {
|
||||||
.add_filter("Markdown", &["md"])
|
.add_filter("Markdown", &["md"])
|
||||||
.add_filter("All Files", &["*"])
|
.add_filter("All Files", &["*"])
|
||||||
.set_directory(¬es_dir)
|
.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() {
|
if let Some(path) = dialog.save_file() {
|
||||||
self.write_to(&path);
|
self.write_to(&path);
|
||||||
self.current_file = Some(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) {
|
fn new_note(&mut self) {
|
||||||
viewport_send_command(self.handle, 12);
|
viewport_send_command(self.handle, 12);
|
||||||
let stub = CString::new("# ").unwrap();
|
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 {
|
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()
|
dirs::home_dir()
|
||||||
.unwrap_or_else(|| PathBuf::from("."))
|
.unwrap_or_else(|| PathBuf::from("."))
|
||||||
.join(".acord")
|
.join(".acord")
|
||||||
|
|
|
||||||
|
|
@ -186,7 +186,7 @@ pub enum MenuCategory {
|
||||||
View,
|
View,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum ShellAction {
|
pub enum ShellAction {
|
||||||
NewNote,
|
NewNote,
|
||||||
Open,
|
Open,
|
||||||
|
|
@ -196,9 +196,13 @@ pub enum ShellAction {
|
||||||
Settings,
|
Settings,
|
||||||
ExportCrate,
|
ExportCrate,
|
||||||
ToggleBrowser,
|
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] = [
|
const MENU_CATS: [(MenuCategory, &'static str); 4] = [
|
||||||
(MenuCategory::File, "File"),
|
(MenuCategory::File, "File"),
|
||||||
(MenuCategory::Edit, "Edit"),
|
(MenuCategory::Edit, "Edit"),
|
||||||
|
|
@ -206,7 +210,7 @@ const MENU_CATS: [(MenuCategory, &'static str); 4] = [
|
||||||
(MenuCategory::View, "View"),
|
(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 {
|
fn cat_btn_width(label: &str, char_w: f32, pad_x: f32) -> f32 {
|
||||||
label.chars().count() as f32 * char_w + pad_x * 2.0
|
label.chars().count() as f32 * char_w + pad_x * 2.0
|
||||||
}
|
}
|
||||||
|
|
@ -430,6 +434,27 @@ pub struct EditorState {
|
||||||
|
|
||||||
pub menu_open: Option<MenuCategory>,
|
pub menu_open: Option<MenuCategory>,
|
||||||
pub pending_shell_action: Option<ShellAction>,
|
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
|
/// per-eval table name to id bookkeeping
|
||||||
|
|
@ -525,6 +550,8 @@ impl EditorState {
|
||||||
prev_cursor_line: 0,
|
prev_cursor_line: 0,
|
||||||
menu_open: None,
|
menu_open: None,
|
||||||
pending_shell_action: None,
|
pending_shell_action: None,
|
||||||
|
settings_open: false,
|
||||||
|
settings_view: SettingsView::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3091,8 +3118,15 @@ impl EditorState {
|
||||||
self.menu_open = None;
|
self.menu_open = None;
|
||||||
}
|
}
|
||||||
Message::Shell(action) => {
|
Message::Shell(action) => {
|
||||||
self.pending_shell_action = Some(action);
|
|
||||||
self.menu_open = None;
|
self.menu_open = None;
|
||||||
|
match action {
|
||||||
|
ShellAction::Settings => {
|
||||||
|
self.settings_open = !self.settings_open;
|
||||||
|
}
|
||||||
|
other => {
|
||||||
|
self.pending_shell_action = Some(other);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Message::CopyLiteral(text) => {
|
Message::CopyLiteral(text) => {
|
||||||
self.pending_clipboard = Some(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();
|
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(self.menu_strip());
|
||||||
|
|
||||||
col_items.push(main_content);
|
col_items.push(main_content);
|
||||||
|
|
@ -3348,7 +3382,12 @@ impl EditorState {
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
.into();
|
.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 {
|
if let Some(cat) = self.menu_open {
|
||||||
return iced_widget::stack![body, self.menu_dropdown(cat)].into();
|
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)
|
&& self.block_at(0).map(|b| b.as_any().is::<TextBlock>()).unwrap_or(false)
|
||||||
&& !has_computed_layers;
|
&& !has_computed_layers;
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||||
let title_bar_h = 0.0_f32;
|
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 title_bar_h = 38.0_f32;
|
||||||
|
|
||||||
let mut block_elements: Vec<Element<'_, Message, Theme, iced_wgpu::Renderer>> = Vec::new();
|
let mut block_elements: Vec<Element<'_, Message, Theme, iced_wgpu::Renderer>> = Vec::new();
|
||||||
|
|
@ -3994,7 +4033,7 @@ impl EditorState {
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||||
fn menu_strip(&self) -> Element<'_, Message, Theme, iced_wgpu::Renderer> {
|
fn menu_strip(&self) -> Element<'_, Message, Theme, iced_wgpu::Renderer> {
|
||||||
let p = palette::current();
|
let p = palette::current();
|
||||||
let f = self.font_size;
|
let f = self.font_size;
|
||||||
|
|
@ -4039,7 +4078,7 @@ impl EditorState {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// returns the dropdown panel for the open category, anchored under its strip button
|
/// 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> {
|
fn menu_dropdown(&self, cat: MenuCategory) -> Element<'_, Message, Theme, iced_wgpu::Renderer> {
|
||||||
let p = palette::current();
|
let p = palette::current();
|
||||||
let f = self.font_size;
|
let f = self.font_size;
|
||||||
|
|
@ -4169,6 +4208,182 @@ impl EditorState {
|
||||||
.into()
|
.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> {
|
fn find_bar(&self) -> Element<'_, Message, Theme, iced_wgpu::Renderer> {
|
||||||
let p = palette::current();
|
let p = palette::current();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ path = "src/main.rs"
|
||||||
acord-core = { path = "../core" }
|
acord-core = { path = "../core" }
|
||||||
acord-viewport = { path = "../viewport" }
|
acord-viewport = { path = "../viewport" }
|
||||||
winit = "0.30"
|
winit = "0.30"
|
||||||
muda = "0.16"
|
|
||||||
arboard = "3"
|
arboard = "3"
|
||||||
rfd = "0.15"
|
rfd = "0.15"
|
||||||
raw-window-handle = "0.6"
|
raw-window-handle = "0.6"
|
||||||
|
|
|
||||||
|
|
@ -22,13 +22,13 @@ use acord_viewport::{
|
||||||
use acord_viewport::browser::{self, BrowserHandle};
|
use acord_viewport::browser::{self, BrowserHandle};
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::menu::{AppMenu, MenuAction};
|
use crate::shortcuts::{match_shortcut, MenuAction};
|
||||||
|
use acord_viewport::editor::ShellAction;
|
||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
window: Option<Window>,
|
window: Option<Window>,
|
||||||
handle: *mut ViewportHandle,
|
handle: *mut ViewportHandle,
|
||||||
config: Config,
|
config: Config,
|
||||||
_menu: Option<AppMenu>,
|
|
||||||
cursor_pos: PhysicalPosition<f64>,
|
cursor_pos: PhysicalPosition<f64>,
|
||||||
scale: f32,
|
scale: f32,
|
||||||
modifiers: ModifiersState,
|
modifiers: ModifiersState,
|
||||||
|
|
@ -48,7 +48,6 @@ impl App {
|
||||||
window: None,
|
window: None,
|
||||||
handle: std::ptr::null_mut(),
|
handle: std::ptr::null_mut(),
|
||||||
config: Config::load(),
|
config: Config::load(),
|
||||||
_menu: None,
|
|
||||||
cursor_pos: PhysicalPosition::new(0.0, 0.0),
|
cursor_pos: PhysicalPosition::new(0.0, 0.0),
|
||||||
scale: 1.0,
|
scale: 1.0,
|
||||||
modifiers: ModifiersState::empty(),
|
modifiers: ModifiersState::empty(),
|
||||||
|
|
@ -62,12 +61,12 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sync_settings(&self) {
|
fn sync_settings(&mut self) {
|
||||||
if self.handle.is_null() { return; }
|
if self.handle.is_null() { return; }
|
||||||
let theme = match self.config.theme_mode() {
|
let theme = match self.config.theme_mode() {
|
||||||
"dark" => "kicad",
|
"dark" => "kicad",
|
||||||
"light" => "latte",
|
"light" => "latte",
|
||||||
_ => "kicad", // Windows: default dark. No NSAppearance auto-detect.
|
_ => "kicad",
|
||||||
};
|
};
|
||||||
let c_theme = CString::new(theme).unwrap();
|
let c_theme = CString::new(theme).unwrap();
|
||||||
viewport_set_theme(self.handle, c_theme.as_ptr());
|
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_line_indicator(self.handle, ind.as_ptr());
|
||||||
viewport_set_gutter_rainbow(self.handle, self.config.gutter_rainbow());
|
viewport_set_gutter_rainbow(self.handle, self.config.gutter_rainbow());
|
||||||
viewport_set_auto_pair_flags(self.handle, self.config.auto_pair_flags());
|
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) {
|
fn dispatch_menu(&mut self, action: MenuAction, event_loop: &ActiveEventLoop) {
|
||||||
|
|
@ -97,29 +104,50 @@ impl App {
|
||||||
MenuAction::Save => self.save_file(),
|
MenuAction::Save => self.save_file(),
|
||||||
MenuAction::SaveAs => self.save_file_as(),
|
MenuAction::SaveAs => self.save_file_as(),
|
||||||
MenuAction::NewNote => self.new_note(),
|
MenuAction::NewNote => self.new_note(),
|
||||||
MenuAction::Settings => {
|
MenuAction::Settings => unsafe {
|
||||||
// Open config file in the default editor.
|
(*self.handle).state.settings_open = !(*self.handle).state.settings_open;
|
||||||
let cfg = dirs::home_dir()
|
},
|
||||||
.unwrap_or_default()
|
MenuAction::ExportCrate => {}
|
||||||
.join(".acord")
|
|
||||||
.join("config.json");
|
|
||||||
let _ = std::process::Command::new("notepad").arg(&cfg).spawn();
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
MenuAction::ToggleBrowser => self.toggle_browser(event_loop),
|
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();
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn toggle_browser(&mut self, event_loop: &ActiveEventLoop) {
|
fn toggle_browser(&mut self, event_loop: &ActiveEventLoop) {
|
||||||
if self.browser_window.is_some() {
|
if self.browser_window.is_some() {
|
||||||
self.close_browser();
|
self.close_browser();
|
||||||
|
|
@ -217,17 +245,25 @@ impl App {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save_file(&mut self) {
|
fn save_file(&mut self) {
|
||||||
match self.current_file.clone() {
|
if let Some(path) = self.current_file.clone() {
|
||||||
Some(path) => self.write_to(&path),
|
self.write_to(&path);
|
||||||
None => self.save_file_as(),
|
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) {
|
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()
|
let dialog = rfd::FileDialog::new()
|
||||||
.add_filter("Markdown", &["md"])
|
.add_filter("Markdown", &["md"])
|
||||||
.add_filter("All Files", &["*"])
|
.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() {
|
if let Some(path) = dialog.save_file() {
|
||||||
self.write_to(&path);
|
self.write_to(&path);
|
||||||
self.current_file = Some(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) {
|
fn new_note(&mut self) {
|
||||||
viewport_send_command(self.handle, 12);
|
viewport_send_command(self.handle, 12);
|
||||||
let stub = CString::new("# ").unwrap();
|
let stub = CString::new("# ").unwrap();
|
||||||
|
|
@ -416,19 +477,9 @@ impl ApplicationHandler for App {
|
||||||
|
|
||||||
self.handle = viewport_create(hwnd, w, h, self.scale);
|
self.handle = viewport_create(hwnd, w, h, self.scale);
|
||||||
self.sync_settings();
|
self.sync_settings();
|
||||||
|
viewport_send_command(self.handle, 12);
|
||||||
let app_menu = AppMenu::new(self.config.auto_pair_flags());
|
let stub = CString::new("# ").unwrap();
|
||||||
#[cfg(target_os = "windows")]
|
viewport_set_text(self.handle, stub.as_ptr());
|
||||||
{
|
|
||||||
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);
|
|
||||||
self.window = Some(window);
|
self.window = Some(window);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -500,6 +551,14 @@ impl ApplicationHandler for App {
|
||||||
|
|
||||||
WindowEvent::KeyboardInput { event, .. } => {
|
WindowEvent::KeyboardInput { event, .. } => {
|
||||||
let pressed = event.state == ElementState::Pressed;
|
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_str = event.text.as_ref().map(|s| s.to_string());
|
||||||
let text_c = text_str.as_deref()
|
let text_c = text_str.as_deref()
|
||||||
.and_then(|s| CString::new(s).ok());
|
.and_then(|s| CString::new(s).ok());
|
||||||
|
|
@ -534,13 +593,11 @@ impl ApplicationHandler for App {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
|
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) {
|
if self.last_autosave_attempt.elapsed() >= Duration::from_millis(500) {
|
||||||
self.last_autosave_attempt = Instant::now();
|
self.last_autosave_attempt = Instant::now();
|
||||||
self.try_autosave();
|
self.try_autosave();
|
||||||
}
|
}
|
||||||
|
self.drain_shell_actions(_event_loop);
|
||||||
self.drain_browser_open();
|
self.drain_browser_open();
|
||||||
if let Some(w) = &self.window {
|
if let Some(w) = &self.window {
|
||||||
if !self.handle.is_null() {
|
if !self.handle.is_null() {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
mod app;
|
mod app;
|
||||||
mod config;
|
mod config;
|
||||||
mod menu;
|
mod shortcuts;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let event_loop = winit::event_loop::EventLoop::new().expect("event loop");
|
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