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