From 9307793661ee487ff8a344289fd3d1b985b93d3a Mon Sep 17 00:00:00 2001 From: Hojjat Date: Wed, 9 Sep 2026 14:19:09 -0600 Subject: [PATCH] feat: use libcosmic's context_menu widget --- src/main.rs | 31 +++++++------ src/menu.rs | 119 +++++++++++++++++------------------------------- src/tab.rs | 9 ++-- src/text_box.rs | 18 ++------ 4 files changed, 66 insertions(+), 111 deletions(-) diff --git a/src/main.rs b/src/main.rs index 1b62962..0d00f81 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,7 @@ use cosmic::{ cosmic_theme, executor, font::Font, iced::{ - self, Alignment, Background, Color, Length, Limits, Point, Subscription, + self, Alignment, Background, Color, Length, Limits, Subscription, advanced::graphics::text::font_system, clipboard, event, futures::{self, SinkExt}, @@ -412,7 +412,7 @@ pub enum Message { TabClose(segmented_button::Entity), TabCloseForce(segmented_button::Entity), TabContextAction(segmented_button::Entity, Action), - TabContextMenu(segmented_button::Entity, Option), + TabContextMenu(segmented_button::Entity, bool), TabNext, TabPrev, TabSetCursor(segmented_button::Entity, Cursor), @@ -2820,15 +2820,15 @@ impl Application for App { Message::TabContextAction(entity, action) => { if let Some(Tab::Editor(tab)) = self.tab_model.data_mut::(entity) { // Close context menu - tab.context_menu = None; + tab.context_menu_open = false; // Run action's message return self.update(action.message(None)); } } - Message::TabContextMenu(entity, position_opt) => { + Message::TabContextMenu(entity, open) => { if let Some(Tab::Editor(tab)) = self.tab_model.data_mut::(entity) { // Update context menu - tab.context_menu = position_opt; + tab.context_menu_open = open; } } Message::TabNext => { @@ -3090,24 +3090,25 @@ impl Application for App { .on_focus(Message::FindFocused(false)) .on_auto_scroll(Message::AutoScroll) .on_changed(Message::TabChanged(tab_id)) - .has_context_menu(tab.context_menu.is_some()) - .on_context_menu(move |position_opt| { - Message::TabContextMenu(tab_id, position_opt) - }); + .has_context_menu(tab.context_menu_open) + .on_context_menu(move |open| Message::TabContextMenu(tab_id, open)); if self.config.highlight_current_line { text_box = text_box.highlight_current_line(); } if self.config.line_numbers { text_box = text_box.line_numbers(); } - let mut popover = widget::popover(text_box); let has_selection = tab.editor.lock().unwrap().selection() != Selection::None; - if let Some(point) = tab.context_menu { - popover = popover - .popup(menu::context_menu(&self.key_binds, tab_id, has_selection)) - .position(widget::popover::Position::Point(point)); + let mut context_menu = widget::context_menu( + text_box, + Some(menu::context_menu(&self.key_binds, tab_id, has_selection)), + ) + .on_close(Message::TabContextMenu(tab_id, false)) + .on_surface_action(Message::Surface); + if let Some(window_id) = self.core().main_window_id() { + context_menu = context_menu.window_id(window_id); } - tab_column = tab_column.push(popover); + tab_column = tab_column.push(context_menu); if self.config.vim_bindings { let status = { let editor = tab.editor.lock().unwrap(); diff --git a/src/menu.rs b/src/menu.rs index 7474645..18e5bdb 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -1,18 +1,15 @@ // SPDX-License-Identifier: GPL-3.0-only use cosmic::widget::menu::Item as MenuItem; +use cosmic::widget::menu::action::MenuAction; use cosmic::widget::menu::key_bind::KeyBind; use cosmic::{ Element, app::Core, - iced::{ - Background, Border, Length, advanced::widget::text::Style as TextStyle, widget::column, - }, - theme, widget::{ - self, divider, - menu::{ItemHeight, ItemWidth, menu_button}, - responsive_menu_bar, segmented_button, space, + self, + menu::{ItemHeight, ItemWidth}, + responsive_menu_bar, segmented_button, }, }; use std::{collections::HashMap, path::PathBuf, sync::LazyLock}; @@ -116,81 +113,49 @@ fn format_recent_menu_path(path: &PathBuf, home_dir_opt: Option<&PathBuf>) -> St truncate_middle(&display, RECENT_MENU_LABEL_MAX_CHARS) } -pub fn context_menu<'a>( +/// A context menu action dispatched to the tab the menu was opened on. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct TabAction(pub segmented_button::Entity, pub Action); + +impl MenuAction for TabAction { + type Message = Message; + + fn message(&self) -> Message { + Message::TabContextAction(self.0, self.1) + } +} + +pub fn context_menu( key_binds: &HashMap, entity: segmented_button::Entity, has_selection: bool, -) -> Element<'a, Message> { - fn key_style(theme: &cosmic::Theme) -> TextStyle { - // TODO use wayland popups - let mut color = theme.cosmic().background(false).component.on; - color.alpha *= 0.75; - TextStyle { - color: Some(color.into()), - ..Default::default() - } - } - - let base_menu_item = |menu_label, menu_action| { - let mut key = String::new(); - for (key_bind, key_action) in key_binds.iter() { - if key_action == &menu_action { - key = key_bind.to_string(); - break; - } - } - menu_button(vec![ - widget::text(menu_label).into(), - space::horizontal().into(), - widget::text(key) - .class(theme::Text::Custom(key_style)) - .into(), - ]) - }; - - let optional_menu_item = |menu_label, menu_action, disabled| { - base_menu_item(menu_label, menu_action).on_press_maybe(if disabled { - None +) -> Vec> { + let item = + |label: String, action: Action| MenuItem::Button(label, None, TabAction(entity, action)); + let optional = |label: String, action: Action, enabled: bool| { + if enabled { + MenuItem::Button(label, None, TabAction(entity, action)) } else { - Some(Message::TabContextAction(entity, menu_action)) - }) - }; - - let menu_item = |menu_label, menu_action| { - base_menu_item(menu_label, menu_action) - .on_press(Message::TabContextAction(entity, menu_action)) - }; - - widget::container( - cosmic::widget::menu::menu_column::MenuColumn::with_children([ - menu_item(fl!("undo"), Action::Undo).into(), - menu_item(fl!("redo"), Action::Redo).into(), - divider::horizontal::light().into(), - optional_menu_item(fl!("cut"), Action::Cut, !has_selection).into(), - optional_menu_item(fl!("copy"), Action::Copy, !has_selection).into(), - menu_item(fl!("paste"), Action::Paste).into(), - menu_item(fl!("select-all"), Action::SelectAll).into(), - ]), - ) - .padding(1) - //TODO: move style to libcosmic - .style(|theme| { - let cosmic = theme.cosmic(); - let component = &cosmic.background(false).component; - widget::container::Style { - icon_color: Some(component.on.into()), - text_color: Some(component.on.into()), - background: Some(Background::Color(component.base.into())), - border: Border { - radius: cosmic.radius_s().map(|x| x + 1.0).into(), - width: 1.0, - color: component.divider.into(), - }, - ..Default::default() + MenuItem::ButtonDisabled(label, None, TabAction(entity, action)) } - }) - .width(Length::Fixed(240.0)) - .into() + }; + + let key_binds: HashMap = key_binds + .iter() + .map(|(key_bind, action)| (key_bind.clone(), TabAction(entity, *action))) + .collect(); + widget::menu::items( + &key_binds, + vec![ + item(fl!("undo"), Action::Undo), + item(fl!("redo"), Action::Redo), + MenuItem::Divider, + optional(fl!("cut"), Action::Cut, has_selection), + optional(fl!("copy"), Action::Copy, has_selection), + item(fl!("paste"), Action::Paste), + item(fl!("select-all"), Action::SelectAll), + ], + ) } pub fn menu_bar<'a>( diff --git a/src/tab.rs b/src/tab.rs index ee2a058..d9a8e7a 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -1,9 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-only -use cosmic::{ - iced::{Point, advanced::graphics::text::font_system}, - widget::icon, -}; +use cosmic::{iced::advanced::graphics::text::font_system, widget::icon}; use cosmic_files::mime_icon::{FALLBACK_MIME_ICON, mime_for_path, mime_icon}; use cosmic_text::{Attrs, Buffer, Cursor, Edit, Selection, Shaping, SyntaxEditor, ViEditor, Wrap}; use regex::Regex; @@ -51,7 +48,7 @@ pub struct EditorTab { pub path_opt: Option, attrs: Attrs<'static>, pub editor: Mutex>, - pub context_menu: Option, + pub context_menu_open: bool, pub zoom_adj: i8, } @@ -75,7 +72,7 @@ impl EditorTab { path_opt: None, attrs, editor: Mutex::new(ViEditor::new(editor)), - context_menu: None, + context_menu_open: false, zoom_adj, }; diff --git a/src/text_box.rs b/src/text_box.rs index ea1f412..3b5dacd 100644 --- a/src/text_box.rs +++ b/src/text_box.rs @@ -51,7 +51,7 @@ pub struct TextBox<'a, Message> { on_focus: Option, click_timing: Duration, has_context_menu: bool, - on_context_menu: Option) -> Message + 'a>>, + on_context_menu: Option Message + 'a>>, highlight_current_line: bool, line_numbers: bool, } @@ -107,10 +107,7 @@ where self } - pub fn on_context_menu( - mut self, - on_context_menu: impl Fn(Option) -> Message + 'a, - ) -> Self { + pub fn on_context_menu(mut self, on_context_menu: impl Fn(bool) -> Message + 'a) -> Self { self.on_context_menu = Some(Box::new(on_context_menu)); self } @@ -1267,14 +1264,9 @@ where // Update context menu state if let Some(on_context_menu) = &self.on_context_menu { - shell.publish((on_context_menu)(if self.has_context_menu { - None - } else { - match button { - Button::Right => Some(p), - _ => None, - } - })); + shell.publish((on_context_menu)( + !self.has_context_menu && matches!(button, Button::Right), + )); } shell.capture_event();