feat: use libcosmic's context_menu widget for the breadcrumb menu

This commit is contained in:
Hojjat 2026-09-02 19:20:30 -06:00 • committed by Michael Murphy
parent 77f062647c
commit b7005660f1
4 changed files with 55 additions and 142 deletions

View file

@ -1737,15 +1737,6 @@ impl App {
} }
} }
fn close_context_menus(&mut self) -> Task<Message> {
let active = self.tab_model.active();
if let Some(tab) = self.tab_model.data_mut::<Tab>(active) {
tab.location_context_menu_index = None;
}
Task::none()
}
fn update_nav_model(&mut self) { fn update_nav_model(&mut self) {
let mut nav_model = segmented_button::ModelBuilder::default(); let mut nav_model = segmented_button::ModelBuilder::default();
@ -2802,11 +2793,6 @@ impl Application for App {
return cosmic::task::message(cosmic::action::app(Message::SetShowDetails(false))); return cosmic::task::message(cosmic::action::app(Message::SetShowDetails(false)));
} }
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) { if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
if tab.location_context_menu_index.is_some() {
tab.location_context_menu_index = None;
return Task::none();
}
if tab.edit_location.is_some() { if tab.edit_location.is_some() {
tab.edit_location = None; tab.edit_location = None;
return Task::none(); return Task::none();
@ -3558,7 +3544,7 @@ impl Application for App {
Message::Mouse(window_id, _button) => { Message::Mouse(window_id, _button) => {
// Close context menu when clicking outside. // Close context menu when clicking outside.
if self.core.main_window_id() == Some(window_id) { if self.core.main_window_id() == Some(window_id) {
return self.close_context_menus(); return Task::none();
} }
} }
Message::MoveTo(entity_opt) => { Message::MoveTo(entity_opt) => {
@ -4348,7 +4334,7 @@ impl Application for App {
)); ));
} }
Message::SearchActivate => { Message::SearchActivate => {
let mut tasks = vec![self.close_context_menus()]; let mut tasks = vec![];
if self.search_get().is_none() { if self.search_get().is_none() {
tasks.push(self.search_set_active(Some(String::new()))); tasks.push(self.search_set_active(Some(String::new())));
@ -4359,7 +4345,7 @@ impl Application for App {
return Task::batch(tasks); return Task::batch(tasks);
} }
Message::SearchClear => { Message::SearchClear => {
return Task::batch([self.close_context_menus(), self.search_set_active(None)]); return self.search_set_active(None);
} }
Message::SearchInput(input) => { Message::SearchInput(input) => {
return self.search_set_active(Some(input)); return self.search_set_active(Some(input));
@ -4380,7 +4366,7 @@ impl Application for App {
return self.update_config(); return self.update_config();
} }
Message::TabActivate(entity) => { Message::TabActivate(entity) => {
let mut tasks = vec![self.close_context_menus()]; let mut tasks = vec![];
// Activate new tab // Activate new tab
self.tab_model.activate(entity); self.tab_model.activate(entity);
@ -4584,7 +4570,6 @@ impl Application for App {
} }
tab::Command::OpenFile(paths) => commands.push(self.open_file(&paths)), tab::Command::OpenFile(paths) => commands.push(self.open_file(&paths)),
tab::Command::OpenInNewTab(path) => { tab::Command::OpenInNewTab(path) => {
commands.push(self.close_context_menus());
commands.push(self.open_tab(Location::Path(path), false, None)); commands.push(self.open_tab(Location::Path(path), false, None));
} }
tab::Command::OpenInNewWindow(path) => match env::current_exe() { tab::Command::OpenInNewWindow(path) => match env::current_exe() {
@ -5021,8 +5006,6 @@ impl Application for App {
if let Some(tab) = self.tab_model.data_mut::<Tab>(tab_entity) { if let Some(tab) = self.tab_model.data_mut::<Tab>(tab_entity) {
// Close location editing if enabled // Close location editing if enabled
tab.edit_location = None; tab.edit_location = None;
// Close other context menus.
tab.location_context_menu_index = None;
} }
} }
Message::NavMenuAction(action) => match action { Message::NavMenuAction(action) => match action {
@ -5120,7 +5103,7 @@ impl Application for App {
_ => Task::none(), _ => Task::none(),
}; };
return Task::batch([self.close_context_menus(), open_task]); return open_task;
} }
// Open the selected path in a new cosmic-files window. // Open the selected path in a new cosmic-files window.

View file

@ -864,12 +864,6 @@ impl App {
} }
} }
fn close_context_menus(&mut self) -> Task<Message> {
self.tab.location_context_menu_index = None;
Task::none()
}
fn update_nav_model(&mut self) { fn update_nav_model(&mut self) {
let mut nav_model = segmented_button::ModelBuilder::default(); let mut nav_model = segmented_button::ModelBuilder::default();
@ -1314,11 +1308,6 @@ impl Application for App {
return Task::none(); return Task::none();
} }
if self.tab.location_context_menu_index.is_some() {
self.tab.location_context_menu_index = None;
return Task::none();
}
if self.tab.edit_location.is_some() { if self.tab.edit_location.is_some() {
// Close location editing if enabled // Close location editing if enabled
self.tab.edit_location = None; self.tab.edit_location = None;
@ -1553,7 +1542,7 @@ impl Application for App {
Message::Mouse(window_id, _button) => { Message::Mouse(window_id, _button) => {
// Close context menu when clicking outside. // Close context menu when clicking outside.
if self.core.main_window_id() == Some(window_id) { if self.core.main_window_id() == Some(window_id) {
return self.close_context_menus(); return Task::none();
} }
} }
Message::NewFolder => { Message::NewFolder => {
@ -1713,7 +1702,7 @@ impl Application for App {
))); )));
} }
Message::SearchActivate => { Message::SearchActivate => {
let mut tasks = vec![self.close_context_menus()]; let mut tasks = vec![];
if self.search_get().is_none() { if self.search_get().is_none() {
tasks.push(self.search_set(Some(String::new()))); tasks.push(self.search_set(Some(String::new())));
@ -1724,7 +1713,7 @@ impl Application for App {
return Task::batch(tasks); return Task::batch(tasks);
} }
Message::SearchClear => { Message::SearchClear => {
return Task::batch([self.close_context_menus(), self.search_set(None)]); return self.search_set(None);
} }
Message::SearchInput(input) => { Message::SearchInput(input) => {
return self.search_set(Some(input)); return self.search_set(Some(input));

View file

@ -1,13 +1,12 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
use cosmic::{Element, theme};
use cosmic::app::Core; use cosmic::app::Core;
use cosmic::iced::keyboard::Modifiers; use cosmic::iced::keyboard::Modifiers;
use cosmic::iced::{Alignment, Background, Border, Length};
use cosmic::widget::menu::action::MenuAction; use cosmic::widget::menu::action::MenuAction;
use cosmic::widget::menu::key_bind::KeyBind; use cosmic::widget::menu::key_bind::KeyBind;
use cosmic::widget::menu::{self, ItemHeight, ItemWidth, MenuBar}; use cosmic::widget::menu::{self, ItemHeight, ItemWidth, MenuBar};
use cosmic::widget::{self, Row, button, column, container, divider, responsive_menu_bar, text}; use cosmic::widget::{self, responsive_menu_bar};
use cosmic::{Element, theme};
use i18n_embed::LanguageLoader; use i18n_embed::LanguageLoader;
use mime_guess::Mime; use mime_guess::Mime;
use std::collections::HashMap; use std::collections::HashMap;
@ -24,21 +23,6 @@ use crate::trash::{Trash, TrashExt};
static MENU_ID: LazyLock<cosmic::widget::Id> = static MENU_ID: LazyLock<cosmic::widget::Id> =
LazyLock::new(|| cosmic::widget::Id::new("responsive-menu")); LazyLock::new(|| cosmic::widget::Id::new("responsive-menu"));
macro_rules! menu_button {
($($x:expr),+ $(,)?) => (
button::custom(
Row::with_children(
[$(Element::from($x)),+]
)
.height(Length::Fixed(24.0))
.align_y(Alignment::Center)
)
.padding([theme::spacing().space_xxs, 16])
.width(Length::Fill)
.class(theme::Button::MenuItem)
);
}
const fn menu_button_optional( const fn menu_button_optional(
label: String, label: String,
action: Action, action: Action,
@ -765,50 +749,33 @@ pub fn menu_bar<'a>(
) )
} }
pub fn location_context_menu<'a>(ancestor_index: usize) -> Element<'a, tab::Message> { pub fn location_context_menu(ancestor_index: usize) -> Vec<menu::Tree<tab::Message>> {
//TODO: only add some of these when in App mode //TODO: only add some of these when in App mode
let children = [ menu::items(
menu_button!(text::body(fl!("open-in-new-tab"))) &HashMap::new(),
.on_press(tab::Message::LocationMenuAction( vec![
menu::Item::Button(
fl!("open-in-new-tab"),
None,
LocationMenuAction::OpenInNewTab(ancestor_index), LocationMenuAction::OpenInNewTab(ancestor_index),
)) ),
.into(), menu::Item::Button(
menu_button!(text::body(fl!("open-in-new-window"))) fl!("open-in-new-window"),
.on_press(tab::Message::LocationMenuAction( None,
LocationMenuAction::OpenInNewWindow(ancestor_index), LocationMenuAction::OpenInNewWindow(ancestor_index),
)) ),
.into(), menu::Item::Divider,
divider::horizontal::light().into(), menu::Item::Button(
menu_button!(text::body(fl!("show-details"))) fl!("show-details"),
.on_press(tab::Message::LocationMenuAction( None,
LocationMenuAction::Preview(ancestor_index), LocationMenuAction::Preview(ancestor_index),
)) ),
.into(), menu::Item::Divider,
divider::horizontal::light().into(), menu::Item::Button(
menu_button!(text::body(fl!("add-to-sidebar"))) fl!("add-to-sidebar"),
.on_press(tab::Message::LocationMenuAction( None,
LocationMenuAction::AddToSidebar(ancestor_index), LocationMenuAction::AddToSidebar(ancestor_index),
)) ),
.into(), ],
]; )
container(column::with_children(children))
.padding(1)
.style(|theme| {
let cosmic = theme.cosmic();
let component = &cosmic.background(theme.transparent).component;
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()
}
})
.width(Length::Fixed(360.0))
.into()
} }

View file

@ -1779,8 +1779,7 @@ pub enum Message {
ContextAction(Action), ContextAction(Action),
RightClickBackground, RightClickBackground,
Surface(cosmic::surface::Action), Surface(cosmic::surface::Action),
LocationContextMenuPoint(Option<Point>), LocationContextMenuIndex(Option<usize>),
LocationContextMenuIndex(Option<Point>, Option<usize>),
LocationMenuAction(LocationMenuAction), LocationMenuAction(LocationMenuAction),
Drag(Option<Rectangle>), Drag(Option<Rectangle>),
DragEnd, DragEnd,
@ -2806,7 +2805,7 @@ pub struct Tab {
pub location: Location, pub location: Location,
pub location_ancestors: Vec<(Location, String)>, pub location_ancestors: Vec<(Location, String)>,
pub location_title: String, pub location_title: String,
pub location_context_menu_point: Option<Point>, /// Breadcrumb whose context menu is open, drawn as active while it shows
pub location_context_menu_index: Option<usize>, pub location_context_menu_index: Option<usize>,
pub mode: Mode, pub mode: Mode,
pub scroll_opt: Option<AbsoluteOffset>, pub scroll_opt: Option<AbsoluteOffset>,
@ -2952,7 +2951,6 @@ impl Tab {
location, location,
location_ancestors, location_ancestors,
location_title, location_title,
location_context_menu_point: None,
location_context_menu_index: None, location_context_menu_index: None,
mode: Mode::App, mode: Mode::App,
scroll_opt: None, scroll_opt: None,
@ -3572,7 +3570,6 @@ impl Tab {
} }
if click_i_opt != self.clicked.take() { if click_i_opt != self.clicked.take() {
self.location_context_menu_index = None;
if let Some(ref mut items) = self.items_opt { if let Some(ref mut items) = self.items_opt {
for (i, item) in items.iter_mut().enumerate() { for (i, item) in items.iter_mut().enumerate() {
if mod_ctrl { if mod_ctrl {
@ -3615,7 +3612,6 @@ impl Tab {
Message::Click(click_i_opt) => { Message::Click(click_i_opt) => {
self.selected_clicked = false; self.selected_clicked = false;
self.edit_location = None; self.edit_location = None;
self.location_context_menu_index = None;
if click_i_opt.is_none() { if click_i_opt.is_none() {
self.clicked = click_i_opt; self.clicked = click_i_opt;
} }
@ -3764,7 +3760,6 @@ impl Tab {
} }
Message::RightClickBackground => { Message::RightClickBackground => {
self.edit_location = None; self.edit_location = None;
self.location_context_menu_index = None;
//TODO: hack for clearing selecting when right clicking empty space //TODO: hack for clearing selecting when right clicking empty space
if self.last_right_click.take().is_none() if self.last_right_click.take().is_none()
@ -3778,15 +3773,10 @@ impl Tab {
Message::Surface(action) => { Message::Surface(action) => {
commands.push(Command::Surface(action)); commands.push(Command::Surface(action));
} }
Message::LocationContextMenuPoint(point_opt) => { Message::LocationContextMenuIndex(index) => {
self.location_context_menu_point = point_opt; self.location_context_menu_index = index;
}
Message::LocationContextMenuIndex(p, index_opt) => {
self.location_context_menu_point = p;
self.location_context_menu_index = index_opt;
} }
Message::LocationMenuAction(action) => { Message::LocationMenuAction(action) => {
self.location_context_menu_index = None;
let path_for_index = |ancestor_index| { let path_for_index = |ancestor_index| {
self.location self.location
.path_opt() .path_opt()
@ -3838,7 +3828,6 @@ impl Tab {
Message::Drag(rect_opt) => { Message::Drag(rect_opt) => {
self.watch_drag = false; self.watch_drag = false;
if let Some(rect) = rect_opt { if let Some(rect) = rect_opt {
self.location_context_menu_index = None;
if self.mode.multiple() { if self.mode.multiple() {
self.select_rect(rect, mod_ctrl, mod_shift); self.select_rect(rect, mod_ctrl, mod_shift);
} }
@ -5570,10 +5559,14 @@ impl Tab {
} }
let location = self.location.with_path(ancestor.to_path_buf()); let location = self.location.with_path(ancestor.to_path_buf());
let mut mouse_area = crate::mouse_area::MouseArea::new( let mouse_area = crate::mouse_area::MouseArea::new(
widget::button::custom(row) widget::button::custom(row)
.padding(space_xxxs) .padding(space_xxxs)
.class(theme::Button::Link) .class(if self.location_context_menu_index == Some(index) {
theme::Button::LinkActive
} else {
theme::Button::Link
})
.on_press(if ancestor == path { .on_press(if ancestor == path {
Message::EditLocation(Some(self.location.clone().into())) Message::EditLocation(Some(self.location.clone().into()))
} else { } else {
@ -5581,21 +5574,6 @@ impl Tab {
}), }),
); );
if self.location_context_menu_index.is_some() {
mouse_area = mouse_area
.on_right_press(move |point_opt| {
Message::LocationContextMenuIndex(point_opt, None)
})
.wayland_on_right_press_window_position();
} else {
mouse_area = mouse_area
.on_right_press_no_capture()
.on_right_press(move |point_opt| {
Message::LocationContextMenuIndex(point_opt, Some(index))
})
.wayland_on_right_press_window_position();
}
let mouse_area = if let Location::Path(_) = &self.location { let mouse_area = if let Location::Path(_) = &self.location {
mouse_area mouse_area
.on_middle_press(move |_| Message::OpenInNewTab(ancestor.to_path_buf())) .on_middle_press(move |_| Message::OpenInNewTab(ancestor.to_path_buf()))
@ -5603,7 +5581,16 @@ impl Tab {
mouse_area mouse_area
}; };
children.push(self.dnd_dest(&location, mouse_area)); // Each breadcrumb carries the menu for its own ancestor index
let mut context_menu =
widget::context_menu(mouse_area, Some(menu::location_context_menu(index)))
.on_open(Message::LocationContextMenuIndex(Some(index)))
.on_close(Message::LocationContextMenuIndex(None))
.on_surface_action(Message::Surface);
if let Some(window_id) = self.window_id {
context_menu = context_menu.window_id(window_id);
}
children.push(self.dnd_dest(&location, context_menu));
if found_home || overflow { if found_home || overflow {
break; break;
@ -5654,20 +5641,7 @@ impl Tab {
column = column.push(heading_rule); column = column.push(heading_rule);
} }
let mouse_area = crate::mouse_area::MouseArea::new(column) column.into()
.on_right_press(Message::LocationContextMenuPoint);
let mut popover = widget::popover(mouse_area);
if let (Some(point), Some(index)) = (
self.location_context_menu_point,
self.location_context_menu_index,
) {
popover = popover
.popup(menu::location_context_menu(index))
.position(widget::popover::Position::Point(point));
}
popover.into()
} }
pub fn empty_view(&self, has_hidden: bool) -> Element<'_, Message> { pub fn empty_view(&self, has_hidden: bool) -> Element<'_, Message> {