From 858bad0f50a8a6aab4f119febffe05d3c07a3e77 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Mon, 20 Nov 2023 11:26:26 -0700 Subject: [PATCH] Add simple context menu --- Cargo.lock | 76 ++++++++++++++++++++++--------------------- src/main.rs | 54 ++++++++++++++++++++++++++----- src/menu.rs | 71 ++++++++++++++++++++++++++++++---------- src/tab.rs | 7 +++- src/text_box.rs | 86 +++++++++++++++++++++++++++++++++---------------- 5 files changed, 205 insertions(+), 89 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 724c39b..3d22f20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -289,15 +289,15 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.6.0" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0c4a4f319e45986f347ee47fef8bf5e81c9abc3f6f58dc2391439f30df65f0" +checksum = "fc5ea910c42e5ab19012bab31f53cb4d63d54c3a27730f9a833a88efcf4bb52d" dependencies = [ - "async-lock 2.8.0", + "async-lock 3.1.1", "async-task", "concurrent-queue", "fastrand 2.0.1", - "futures-lite 1.13.0", + "futures-lite 2.0.1", "slab", ] @@ -339,14 +339,14 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41ed9d5715c2d329bf1b4da8d60455b99b187f27ba726df2883799af9af60997" dependencies = [ - "async-lock 3.1.0", + "async-lock 3.1.1", "cfg-if 1.0.0", "concurrent-queue", "futures-io", "futures-lite 2.0.1", "parking", "polling 3.3.0", - "rustix 0.38.24", + "rustix 0.38.25", "slab", "tracing", "waker-fn", @@ -364,9 +364,9 @@ dependencies = [ [[package]] name = "async-lock" -version = "3.1.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb2ab2aa8a746e221ab826c73f48bc6ba41be6763f0855cb249eb6d154cf1d7" +checksum = "655b9c7fe787d3b25cc0f804a1a8401790f0c5bc395beb5a64dc77d8de079105" dependencies = [ "event-listener 3.1.0", "event-listener-strategy", @@ -386,7 +386,7 @@ dependencies = [ "cfg-if 1.0.0", "event-listener 3.1.0", "futures-lite 1.13.0", - "rustix 0.38.24", + "rustix 0.38.25", "windows-sys 0.48.0", ] @@ -413,7 +413,7 @@ dependencies = [ "cfg-if 1.0.0", "futures-core", "futures-io", - "rustix 0.38.24", + "rustix 0.38.25", "signal-hook-registry", "slab", "windows-sys 0.48.0", @@ -459,7 +459,7 @@ name = "atomicwrites" version = "0.4.2" source = "git+https://github.com/jackpot51/rust-atomicwrites#043ab4859d53ffd3d55334685303d8df39c9f768" dependencies = [ - "rustix 0.38.24", + "rustix 0.38.25", "tempfile", "windows-sys 0.48.0", ] @@ -604,7 +604,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" dependencies = [ "async-channel 2.1.0", - "async-lock 3.1.0", + "async-lock 3.1.1", "async-task", "fastrand 2.0.1", "futures-io", @@ -932,7 +932,7 @@ dependencies = [ [[package]] name = "cosmic-config" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic#1caae11e8f11a1e838e950b7e25050f6428be63d" +source = "git+https://github.com/pop-os/libcosmic#001fd744c5f80c9ce058eb0e22ae92f19d12c844" dependencies = [ "atomicwrites", "cosmic-config-derive", @@ -946,7 +946,7 @@ dependencies = [ [[package]] name = "cosmic-config-derive" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic#1caae11e8f11a1e838e950b7e25050f6428be63d" +source = "git+https://github.com/pop-os/libcosmic#001fd744c5f80c9ce058eb0e22ae92f19d12c844" dependencies = [ "quote", "syn 1.0.109", @@ -1019,7 +1019,7 @@ dependencies = [ [[package]] name = "cosmic-theme" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic#1caae11e8f11a1e838e950b7e25050f6428be63d" +source = "git+https://github.com/pop-os/libcosmic#001fd744c5f80c9ce058eb0e22ae92f19d12c844" dependencies = [ "almost", "cosmic-config", @@ -1938,7 +1938,11 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3831c2651acb5177cbd83943f3d9c8912c5ad03c76afcc0e9511ba568ec5ebb" dependencies = [ + "fastrand 2.0.1", "futures-core", + "futures-io", + "memchr", + "parking", "pin-project-lite", ] @@ -2385,7 +2389,7 @@ dependencies = [ [[package]] name = "iced" version = "0.10.0" -source = "git+https://github.com/pop-os/libcosmic#1caae11e8f11a1e838e950b7e25050f6428be63d" +source = "git+https://github.com/pop-os/libcosmic#001fd744c5f80c9ce058eb0e22ae92f19d12c844" dependencies = [ "iced_accessibility", "iced_core", @@ -2400,7 +2404,7 @@ dependencies = [ [[package]] name = "iced_accessibility" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic#1caae11e8f11a1e838e950b7e25050f6428be63d" +source = "git+https://github.com/pop-os/libcosmic#001fd744c5f80c9ce058eb0e22ae92f19d12c844" dependencies = [ "accesskit", "accesskit_winit", @@ -2409,7 +2413,7 @@ dependencies = [ [[package]] name = "iced_core" version = "0.10.0" -source = "git+https://github.com/pop-os/libcosmic#1caae11e8f11a1e838e950b7e25050f6428be63d" +source = "git+https://github.com/pop-os/libcosmic#001fd744c5f80c9ce058eb0e22ae92f19d12c844" dependencies = [ "bitflags 1.3.2", "instant", @@ -2423,7 +2427,7 @@ dependencies = [ [[package]] name = "iced_futures" version = "0.7.0" -source = "git+https://github.com/pop-os/libcosmic#1caae11e8f11a1e838e950b7e25050f6428be63d" +source = "git+https://github.com/pop-os/libcosmic#001fd744c5f80c9ce058eb0e22ae92f19d12c844" dependencies = [ "futures", "iced_core", @@ -2435,7 +2439,7 @@ dependencies = [ [[package]] name = "iced_graphics" version = "0.9.0" -source = "git+https://github.com/pop-os/libcosmic#1caae11e8f11a1e838e950b7e25050f6428be63d" +source = "git+https://github.com/pop-os/libcosmic#001fd744c5f80c9ce058eb0e22ae92f19d12c844" dependencies = [ "bitflags 1.3.2", "bytemuck", @@ -2453,7 +2457,7 @@ dependencies = [ [[package]] name = "iced_renderer" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic#1caae11e8f11a1e838e950b7e25050f6428be63d" +source = "git+https://github.com/pop-os/libcosmic#001fd744c5f80c9ce058eb0e22ae92f19d12c844" dependencies = [ "iced_graphics", "iced_tiny_skia", @@ -2466,7 +2470,7 @@ dependencies = [ [[package]] name = "iced_runtime" version = "0.1.1" -source = "git+https://github.com/pop-os/libcosmic#1caae11e8f11a1e838e950b7e25050f6428be63d" +source = "git+https://github.com/pop-os/libcosmic#001fd744c5f80c9ce058eb0e22ae92f19d12c844" dependencies = [ "iced_core", "iced_futures", @@ -2476,7 +2480,7 @@ dependencies = [ [[package]] name = "iced_style" version = "0.9.0" -source = "git+https://github.com/pop-os/libcosmic#1caae11e8f11a1e838e950b7e25050f6428be63d" +source = "git+https://github.com/pop-os/libcosmic#001fd744c5f80c9ce058eb0e22ae92f19d12c844" dependencies = [ "iced_core", "once_cell", @@ -2486,7 +2490,7 @@ dependencies = [ [[package]] name = "iced_tiny_skia" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic#1caae11e8f11a1e838e950b7e25050f6428be63d" +source = "git+https://github.com/pop-os/libcosmic#001fd744c5f80c9ce058eb0e22ae92f19d12c844" dependencies = [ "bytemuck", "cosmic-text 0.9.0", @@ -2504,7 +2508,7 @@ dependencies = [ [[package]] name = "iced_wgpu" version = "0.11.1" -source = "git+https://github.com/pop-os/libcosmic#1caae11e8f11a1e838e950b7e25050f6428be63d" +source = "git+https://github.com/pop-os/libcosmic#001fd744c5f80c9ce058eb0e22ae92f19d12c844" dependencies = [ "bitflags 1.3.2", "bytemuck", @@ -2526,7 +2530,7 @@ dependencies = [ [[package]] name = "iced_widget" version = "0.1.3" -source = "git+https://github.com/pop-os/libcosmic#1caae11e8f11a1e838e950b7e25050f6428be63d" +source = "git+https://github.com/pop-os/libcosmic#001fd744c5f80c9ce058eb0e22ae92f19d12c844" dependencies = [ "iced_renderer", "iced_runtime", @@ -2540,7 +2544,7 @@ dependencies = [ [[package]] name = "iced_winit" version = "0.10.0" -source = "git+https://github.com/pop-os/libcosmic#1caae11e8f11a1e838e950b7e25050f6428be63d" +source = "git+https://github.com/pop-os/libcosmic#001fd744c5f80c9ce058eb0e22ae92f19d12c844" dependencies = [ "iced_graphics", "iced_runtime", @@ -2703,7 +2707,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi", - "rustix 0.38.24", + "rustix 0.38.25", "windows-sys 0.48.0", ] @@ -2838,7 +2842,7 @@ checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" [[package]] name = "libcosmic" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic#1caae11e8f11a1e838e950b7e25050f6428be63d" +source = "git+https://github.com/pop-os/libcosmic#001fd744c5f80c9ce058eb0e22ae92f19d12c844" dependencies = [ "apply", "cosmic-config", @@ -3002,9 +3006,9 @@ dependencies = [ [[package]] name = "lyon_algorithms" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00a0349cd8f0270781bb93a824b63df6178e3b4a27794e7be3ce3763f5a44d6e" +checksum = "a3bca95f9a4955b3e4a821fbbcd5edfbd9be2a9a50bb5758173e5358bfb4c623" dependencies = [ "lyon_path", "num-traits", @@ -4074,7 +4078,7 @@ dependencies = [ "cfg-if 1.0.0", "concurrent-queue", "pin-project-lite", - "rustix 0.38.24", + "rustix 0.38.25", "tracing", "windows-sys 0.48.0", ] @@ -4508,9 +4512,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.24" +version = "0.38.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ad981d6c340a49cdc40a1028d9c6084ec7e9fa33fcb839cab656a267071e234" +checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e" dependencies = [ "bitflags 2.4.1", "errno", @@ -4863,7 +4867,7 @@ dependencies = [ "objc", "raw-window-handle 0.5.2", "redox_syscall 0.4.1", - "rustix 0.38.24", + "rustix 0.38.25", "tiny-xlib", "wasm-bindgen", "wayland-backend", @@ -5058,7 +5062,7 @@ dependencies = [ "cfg-if 1.0.0", "fastrand 2.0.1", "redox_syscall 0.4.1", - "rustix 0.38.24", + "rustix 0.38.25", "windows-sys 0.48.0", ] diff --git a/src/main.rs b/src/main.rs index 81ccdc5..f563caa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ use cosmic::{ iced::{ clipboard, event, keyboard, subscription, widget::{row, text}, - window, Alignment, Length, + window, Alignment, Length, Point, }, style, widget::{self, button, icon, nav_bar, segmented_button, view_switcher}, @@ -21,7 +21,7 @@ use std::{ sync::Mutex, }; -use config::{AppTheme, Config, CONFIG_VERSION}; +use config::{Action, AppTheme, Config, CONFIG_VERSION}; mod config; mod localize; @@ -105,7 +105,7 @@ pub struct Flags { } #[allow(dead_code)] -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub enum Message { AppTheme(AppTheme), Config(Config), @@ -133,6 +133,8 @@ pub enum Message { TabActivate(segmented_button::Entity), TabChanged(segmented_button::Entity), TabClose(segmented_button::Entity), + TabContextAction(segmented_button::Entity, Action), + TabContextMenu(segmented_button::Entity, Option), TabWidth(u16), Todo, ToggleAutoIndent, @@ -829,6 +831,30 @@ impl Application for App { return self.update_tab(); } + Message::TabContextAction(entity, action) => { + match self.tab_model.data_mut::(entity) { + Some(tab) => { + // Close context menu + tab.context_menu = None; + // Hack to ensure editor redraws + tab.editor.lock().unwrap().buffer_mut().set_redraw(true); + // Run action's message + return self.update(action.message()); + } + None => {} + } + } + Message::TabContextMenu(entity, position_opt) => { + match self.tab_model.data_mut::(entity) { + Some(tab) => { + // Update context menu + tab.context_menu = position_opt; + // Hack to ensure editor redraws + tab.editor.lock().unwrap().buffer_mut().set_redraw(true); + } + None => {} + } + } Message::TabWidth(tab_width) => { self.config.tab_width = tab_width; return self.save_config(); @@ -1024,7 +1050,8 @@ impl Application for App { .align_items(Alignment::Center), ); - match self.active_tab() { + let tab_id = self.tab_model.active(); + match self.tab_model.data::(tab_id) { Some(tab) => { let status = { let editor = tab.editor.lock().unwrap(); @@ -1060,10 +1087,21 @@ impl Application for App { } } }; - tab_column = tab_column.push( - text_box(&tab.editor, self.config.metrics()) - .on_changed(Message::TabChanged(self.tab_model.active())), - ); + let text_box = text_box(&tab.editor, self.config.metrics()) + .on_changed(Message::TabChanged(tab_id)) + .on_context_menu(move |position_opt| { + Message::TabContextMenu(tab_id, position_opt) + }); + let tab_element: Element<'_, Message> = match tab.context_menu { + Some(position) => widget::popover( + text_box.context_menu(position), + menu::context_menu(&self.config, tab_id), + ) + .position(position) + .into(), + None => text_box.into(), + }; + tab_column = tab_column.push(tab_element); tab_column = tab_column.push(text(status).font(cosmic::font::Font::MONOSPACE)); } None => { diff --git a/src/menu.rs b/src/menu.rs index 65c073b..cb1af13 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -1,17 +1,69 @@ // SPDX-License-Identifier: GPL-3.0-only use cosmic::{ + cosmic_theme, //TODO: export in cosmic::widget - iced::{widget::horizontal_rule, Alignment, Length}, + iced::{ + widget::{column, horizontal_rule}, + Alignment, Length, + }, theme, widget::{ self, horizontal_space, menu::{ItemHeight, ItemWidth, MenuBar, MenuTree}, + segmented_button, }, Element, }; -use crate::{fl, Config, ContextPage, Message}; +use crate::{fl, Action, Config, ContextPage, Message}; + +macro_rules! menu_button { + ($($x:expr),+ $(,)?) => ( + widget::button( + widget::Row::with_children( + vec![$(Element::from($x)),+] + ) + .align_items(Alignment::Center) + ) + .height(Length::Fixed(32.0)) + .padding([4, 16]) + .width(Length::Fill) + .style(theme::Button::MenuItem) + ); +} + +pub fn context_menu<'a>(config: &Config, entity: segmented_button::Entity) -> Element<'a, Message> { + let menu_item = |label, action| { + let mut key = String::new(); + for (key_bind, action) in config.keybinds.iter() { + if action == action { + key = key_bind.to_string(); + break; + } + } + menu_button!( + widget::text(label), + horizontal_space(Length::Fill), + widget::text(key) + ) + .on_press(Message::TabContextAction(entity, action)) + }; + + widget::cosmic_container::container(column!( + menu_item(fl!("undo"), Action::Undo), + menu_item(fl!("redo"), Action::Redo), + horizontal_rule(1), + menu_item(fl!("cut"), Action::Cut), + menu_item(fl!("copy"), Action::Copy), + menu_item(fl!("paste"), Action::Paste), + menu_item(fl!("select-all"), Action::SelectAll), + )) + .layer(cosmic_theme::Layer::Background) + .style(theme::Container::Card) + .width(Length::Fixed(240.0)) + .into() +} pub fn menu_bar<'a>(config: &Config) -> Element<'a, Message> { //TODO: port to libcosmic @@ -21,21 +73,6 @@ pub fn menu_bar<'a>(config: &Config) -> Element<'a, Message> { .style(theme::Button::MenuRoot) }; - macro_rules! menu_button { - ($($x:expr),+ $(,)?) => ( - widget::button( - widget::Row::with_children( - vec![$(Element::from($x)),+] - ) - .align_items(Alignment::Center) - ) - .height(Length::Fixed(32.0)) - .padding([4, 16]) - .width(Length::Fill) - .style(theme::Button::MenuItem) - ); - } - let menu_folder = |label| menu_button!(widget::text(label), horizontal_space(Length::Fill), ">"); diff --git a/src/tab.rs b/src/tab.rs index cf20a7f..23d36aa 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -1,6 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-only -use cosmic::widget::{icon, Icon}; +use cosmic::{ + iced::Point, + widget::{icon, Icon}, +}; use cosmic_text::{Attrs, Buffer, Edit, Shaping, SyntaxEditor, ViEditor, Wrap}; use std::{fs, path::PathBuf, sync::Mutex}; @@ -10,6 +13,7 @@ pub struct Tab { pub path_opt: Option, attrs: Attrs<'static>, pub editor: Mutex>, + pub context_menu: Option, } impl Tab { @@ -31,6 +35,7 @@ impl Tab { path_opt: None, attrs, editor: Mutex::new(ViEditor::new(editor)), + context_menu: None, }; // Update any other config settings diff --git a/src/text_box.rs b/src/text_box.rs index e4cb730..d1f392c 100644 --- a/src/text_box.rs +++ b/src/text_box.rs @@ -62,6 +62,8 @@ pub struct TextBox<'a, Message> { metrics: Metrics, padding: Padding, on_changed: Option, + context_menu: Option, + on_context_menu: Option) -> Message + 'a>>, } impl<'a, Message> TextBox<'a, Message> @@ -74,6 +76,8 @@ where metrics, padding: Padding::new(0.0), on_changed: None, + context_menu: None, + on_context_menu: None, } } @@ -86,6 +90,19 @@ where self.on_changed = Some(on_changed); self } + + pub fn context_menu(mut self, position: Point) -> Self { + self.context_menu = Some(position); + self + } + + pub fn on_context_menu( + mut self, + on_context_menu: impl Fn(Option) -> Message + 'a, + ) -> Self { + self.on_context_menu = Some(Box::new(on_context_menu)); + self + } } pub fn text_box<'a, Message>( @@ -498,35 +515,50 @@ where status = Status::Captured; } } - Event::Mouse(MouseEvent::ButtonPressed(Button::Left)) => { + Event::Mouse(MouseEvent::ButtonPressed(button)) => { if let Some(p) = cursor_position.position_in(layout.bounds()) { - let x_logical = p.x - self.padding.left; - let y_logical = p.y - self.padding.top; - let x = x_logical * scale_factor; - let y = y_logical * scale_factor; - if x >= 0.0 && x < buffer_size.0 && y >= 0.0 && y < buffer_size.1 { - editor.action(Action::Click { - x: x as i32, - y: y as i32, - }); - state.dragging = Some(Dragging::Buffer); - } else if scrollbar_rect.contains(Point::new(x_logical, y_logical)) { - state.dragging = Some(Dragging::Scrollbar { - start_y: y, - start_scroll: editor.buffer().scroll(), - }); - } else if x_logical >= scrollbar_rect.x - && x_logical < (scrollbar_rect.x + scrollbar_rect.width) - { - let mut buffer = editor.buffer_mut(); - let scroll_offset = - ((y / buffer.size().1) * buffer.lines.len() as f32) as i32; - buffer.set_scroll(scroll_offset); - state.dragging = Some(Dragging::Scrollbar { - start_y: y, - start_scroll: editor.buffer().scroll(), - }); + // Handle left click drag + if let Button::Left = button { + let x_logical = p.x - self.padding.left; + let y_logical = p.y - self.padding.top; + let x = x_logical * scale_factor; + let y = y_logical * scale_factor; + if x >= 0.0 && x < buffer_size.0 && y >= 0.0 && y < buffer_size.1 { + editor.action(Action::Click { + x: x as i32, + y: y as i32, + }); + state.dragging = Some(Dragging::Buffer); + } else if scrollbar_rect.contains(Point::new(x_logical, y_logical)) { + state.dragging = Some(Dragging::Scrollbar { + start_y: y, + start_scroll: editor.buffer().scroll(), + }); + } else if x_logical >= scrollbar_rect.x + && x_logical < (scrollbar_rect.x + scrollbar_rect.width) + { + let mut buffer = editor.buffer_mut(); + let scroll_offset = + ((y / buffer.size().1) * buffer.lines.len() as f32) as i32; + buffer.set_scroll(scroll_offset); + state.dragging = Some(Dragging::Scrollbar { + start_y: y, + start_scroll: editor.buffer().scroll(), + }); + } } + + // Update context menu state + if let Some(on_context_menu) = &self.on_context_menu { + shell.publish((on_context_menu)(match self.context_menu { + Some(_) => None, + None => match button { + Button::Right => Some(p), + _ => None, + }, + })); + } + status = Status::Captured; } }