feat: responsive menu bar (#938)

This commit is contained in:
Ashley Wulber 2025-04-15 20:04:07 -04:00 committed by GitHub
parent b6c033562b
commit eea916d783
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 373 additions and 286 deletions

571
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -73,7 +73,7 @@ uzers = "0.12.1"
git = "https://github.com/pop-os/libcosmic.git" git = "https://github.com/pop-os/libcosmic.git"
default-features = false default-features = false
#TODO: a11y feature crashes #TODO: a11y feature crashes
features = ["multi-window", "tokio", "winit"] features = ["multi-window", "tokio", "winit", "surface-message"]
[features] [features]
default = [ default = [

View file

@ -4892,6 +4892,7 @@ impl Application for App {
fn header_start(&self) -> Vec<Element<Self::Message>> { fn header_start(&self) -> Vec<Element<Self::Message>> {
vec![menu::menu_bar( vec![menu::menu_bar(
&self.core,
self.tab_model.active_data::<Tab>(), self.tab_model.active_data::<Tab>(),
&self.config, &self.config,
&self.key_binds, &self.key_binds,

View file

@ -1,18 +1,19 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
use cosmic::{ use cosmic::{
app::Core,
iced::{Alignment, Background, Border, Length}, iced::{Alignment, Background, Border, Length},
theme, theme,
widget::{ widget::{
self, button, column, container, divider, horizontal_space, self, button, column, container, divider, horizontal_space,
menu::{self, key_bind::KeyBind, ItemHeight, ItemWidth, MenuBar}, menu::{self, key_bind::KeyBind, ItemHeight, ItemWidth, MenuBar},
text, Row, responsive_menu_bar, text, Row,
}, },
Element, Element,
}; };
use i18n_embed::LanguageLoader; use i18n_embed::LanguageLoader;
use mime_guess::Mime; use mime_guess::Mime;
use std::collections::HashMap; use std::{collections::HashMap, sync::LazyLock};
use crate::{ use crate::{
app::{Action, Message}, app::{Action, Message},
@ -21,6 +22,9 @@ use crate::{
tab::{self, HeadingOptions, Location, LocationMenuAction, Tab}, tab::{self, HeadingOptions, Location, LocationMenuAction, Tab},
}; };
static MENU_ID: LazyLock<cosmic::widget::Id> =
LazyLock::new(|| cosmic::widget::Id::new("responsive-menu"));
macro_rules! menu_button { macro_rules! menu_button {
($($x:expr),+ $(,)?) => ( ($($x:expr),+ $(,)?) => (
button::custom( button::custom(
@ -121,7 +125,7 @@ pub fn context_menu<'a>(
let lang_id = crate::localize::LANGUAGE_LOADER.current_language(); let lang_id = crate::localize::LANGUAGE_LOADER.current_language();
let language = lang_id.language.as_str(); let language = lang_id.language.as_str();
// Cache? // Cache?
cosmic::desktop::load_desktop_file(Some(language), path) cosmic::desktop::load_desktop_file(&[language.into()], path.into())
} else { } else {
None None
} }
@ -485,6 +489,7 @@ pub fn dialog_menu(
} }
pub fn menu_bar<'a>( pub fn menu_bar<'a>(
core: &Core,
tab_opt: Option<&Tab>, tab_opt: Option<&Tab>,
config: &Config, config: &Config,
key_binds: &HashMap<KeyBind, Action>, key_binds: &HashMap<KeyBind, Action>,
@ -519,11 +524,18 @@ pub fn menu_bar<'a>(
} }
}; };
MenuBar::new(vec![ responsive_menu_bar()
menu::Tree::with_children( .item_height(ItemHeight::Dynamic(40))
menu::root(fl!("file")), .item_width(ItemWidth::Uniform(360))
menu::items( .spacing(theme::active().cosmic().spacing.space_xxxs.into())
key_binds, .into_element(
core,
key_binds,
MENU_ID.clone(),
Message::Surface,
vec![
(
fl!("file"),
vec![ vec![
menu::Item::Button(fl!("new-tab"), None, Action::TabNew), menu::Item::Button(fl!("new-tab"), None, Action::TabNew),
menu::Item::Button(fl!("new-window"), None, Action::WindowNew), menu::Item::Button(fl!("new-window"), None, Action::WindowNew),
@ -554,11 +566,8 @@ pub fn menu_bar<'a>(
menu::Item::Button(fl!("quit"), None, Action::WindowClose), menu::Item::Button(fl!("quit"), None, Action::WindowClose),
], ],
), ),
), (
menu::Tree::with_children( (fl!("edit")),
menu::root(fl!("edit")),
menu::items(
key_binds,
vec![ vec![
menu_button_optional(fl!("cut"), Action::Cut, selected > 0), menu_button_optional(fl!("cut"), Action::Cut, selected > 0),
menu_button_optional(fl!("copy"), Action::Copy, selected > 0), menu_button_optional(fl!("copy"), Action::Copy, selected > 0),
@ -568,11 +577,8 @@ pub fn menu_bar<'a>(
menu::Item::Button(fl!("history"), None, Action::EditHistory), menu::Item::Button(fl!("history"), None, Action::EditHistory),
], ],
), ),
), (
menu::Tree::with_children( (fl!("view")),
menu::root(fl!("view")),
menu::items(
key_binds,
vec![ vec![
menu::Item::Button(fl!("zoom-in"), None, Action::ZoomIn), menu::Item::Button(fl!("zoom-in"), None, Action::ZoomIn),
menu::Item::Button(fl!("default-size"), None, Action::ZoomDefault), menu::Item::Button(fl!("default-size"), None, Action::ZoomDefault),
@ -621,11 +627,8 @@ pub fn menu_bar<'a>(
menu::Item::Button(fl!("menu-about"), None, Action::About), menu::Item::Button(fl!("menu-about"), None, Action::About),
], ],
), ),
), (
menu::Tree::with_children( (fl!("sort")),
menu::root(fl!("sort")),
menu::items(
key_binds,
vec![ vec![
sort_item(fl!("sort-a-z"), tab::HeadingOptions::Name, true), sort_item(fl!("sort-a-z"), tab::HeadingOptions::Name, true),
sort_item(fl!("sort-z-a"), tab::HeadingOptions::Name, false), sort_item(fl!("sort-z-a"), tab::HeadingOptions::Name, false),
@ -660,13 +663,9 @@ pub fn menu_bar<'a>(
//TODO: sort by type //TODO: sort by type
], ],
), ),
), ],
]) )
.item_height(ItemHeight::Dynamic(40)) }
.item_width(ItemWidth::Uniform(360))
.spacing(theme::active().cosmic().spacing.space_xxxs.into())
.into()
}
pub fn location_context_menu<'a>(ancestor_index: usize) -> Element<'a, tab::Message> { pub fn location_context_menu<'a>(ancestor_index: usize) -> Element<'a, tab::Message> {
//TODO: only add some of these when in App mode //TODO: only add some of these when in App mode

View file

@ -200,10 +200,10 @@ impl From<&desktop::DesktopEntryData> for MimeApp {
name: app.name.clone(), name: app.name.clone(),
exec: app.exec.clone(), exec: app.exec.clone(),
icon: match &app.icon { icon: match &app.icon {
desktop::IconSource::Name(name) => { desktop::fde::IconSource::Name(name) => {
widget::icon::from_name(name.as_str()).size(32).handle() widget::icon::from_name(name.as_str()).size(32).handle()
} }
desktop::IconSource::Path(path) => widget::icon::from_path(path.clone()), desktop::fde::IconSource::Path(path) => widget::icon::from_path(path.clone()),
}, },
is_default: false, is_default: false,
} }
@ -251,24 +251,24 @@ impl MimeAppCache {
self.terminals.clear(); self.terminals.clear();
//TODO: get proper locale? //TODO: get proper locale?
let locale = None; let locale = &[];
// Load desktop applications by supported mime types // Load desktop applications by supported mime types
//TODO: hashmap for all apps by id? //TODO: hashmap for all apps by id?
let all_apps = desktop::load_applications(locale, false); let mut all_apps = desktop::load_applications(locale, false);
for app in all_apps.iter() { for app in &mut all_apps {
for mime in app.mime_types.iter() { for mime in app.mime_types.iter() {
let apps = self let apps = self
.cache .cache
.entry(mime.clone()) .entry(mime.clone())
.or_insert_with(|| Vec::with_capacity(1)); .or_insert_with(|| Vec::with_capacity(1));
if !apps.iter().any(|x| x.id == app.id) { if !apps.iter().any(|x| x.id == app.id) {
apps.push(MimeApp::from(app)); apps.push(MimeApp::from(&app));
} }
} }
for category in app.categories.iter() { for category in app.categories.iter() {
if category == "TerminalEmulator" { if category == "TerminalEmulator" {
self.terminals.push(MimeApp::from(app)); self.terminals.push(MimeApp::from(&app));
break; break;
} }
} }
@ -335,9 +335,9 @@ impl MimeAppCache {
.or_insert_with(|| Vec::with_capacity(1)); .or_insert_with(|| Vec::with_capacity(1));
if !apps.iter().any(|x| filename_eq(&x.path, filename)) { if !apps.iter().any(|x| filename_eq(&x.path, filename)) {
if let Some(app) = if let Some(app) =
all_apps.iter().find(|x| filename_eq(&x.path, filename)) all_apps.find(|x| filename_eq(&x.path, filename))
{ {
apps.push(MimeApp::from(app)); apps.push(MimeApp::from(&app));
} else { } else {
log::debug!("failed to add association for {:?}: application {:?} not found", mime, filename); log::debug!("failed to add association for {:?}: application {:?} not found", mime, filename);
} }

View file

@ -1537,7 +1537,7 @@ impl Item {
widget::image(handle.clone()).into() widget::image(handle.clone()).into()
} }
ItemThumbnail::Svg(handle) => widget::svg(handle.clone()).into(), ItemThumbnail::Svg(handle) => widget::svg(handle.clone()).into(),
ItemThumbnail::Text(content) => widget::text_editor(content) ItemThumbnail::Text(content) => widget::text_editor(&content)
.class(cosmic::theme::iced::TextEditor::Custom(Box::new( .class(cosmic::theme::iced::TextEditor::Custom(Box::new(
text_editor_class, text_editor_class,
))) )))
@ -2721,10 +2721,10 @@ impl Tab {
items.iter().find(|item| item.selected).and_then(|item| { items.iter().find(|item| item.selected).and_then(|item| {
let location = item.location_opt.as_ref()?; let location = item.location_opt.as_ref()?;
let path = location.path_opt()?; let path = location.path_opt()?;
cosmic::desktop::load_desktop_file(Some(language), path) cosmic::desktop::load_desktop_file(&[language.into()], path.into())
}) })
}, },
|path| cosmic::desktop::load_desktop_file(Some(language), path), |path| cosmic::desktop::load_desktop_file(&[language.into()], path),
) { ) {
Some(entry) => commands.push(Command::ExecEntryAction(entry, action)), Some(entry) => commands.push(Command::ExecEntryAction(entry, action)),
None => log::warn!("Invalid desktop entry path passed to ExecEntryAction"), None => log::warn!("Invalid desktop entry path passed to ExecEntryAction"),
@ -3633,7 +3633,7 @@ impl Tab {
ItemThumbnail::Text(text) => { ItemThumbnail::Text(text) => {
element_opt = Some( element_opt = Some(
widget::container( widget::container(
widget::text_editor(text).padding(space_xxs).class( widget::text_editor(&text).padding(space_xxs).class(
cosmic::theme::iced::TextEditor::Custom(Box::new( cosmic::theme::iced::TextEditor::Custom(Box::new(
text_editor_class, text_editor_class,
)), )),