From 5ae1b961dcd46ad4897b54e420b2c125f5075a13 Mon Sep 17 00:00:00 2001 From: Bryan Hyland Date: Mon, 19 Jan 2026 14:30:45 -0800 Subject: [PATCH 1/3] menu_bar: allow custom width on menu items --- src/widget/menu.rs | 3 +- src/widget/menu/menu_tree.rs | 126 +++++++++++++++++++++++++++-------- 2 files changed, 99 insertions(+), 30 deletions(-) diff --git a/src/widget/menu.rs b/src/widget/menu.rs index 9d4ce4b1..fc8ca907 100644 --- a/src/widget/menu.rs +++ b/src/widget/menu.rs @@ -69,7 +69,8 @@ pub use menu_bar::{MenuBar, menu_bar as bar}; mod menu_inner; mod menu_tree; pub use menu_tree::{ - MenuItem as Item, MenuTree as Tree, menu_button, menu_items as items, menu_root as root, + MenuItem as Item, MenuItemKind as ItemKind, MenuTree as Tree, menu_button, menu_items as items, + menu_root as root, }; pub use crate::style::menu_bar::{Appearance, StyleSheet}; diff --git a/src/widget/menu/menu_tree.rs b/src/widget/menu/menu_tree.rs index 15dd5810..378e0939 100644 --- a/src/widget/menu/menu_tree.rs +++ b/src/widget/menu/menu_tree.rs @@ -155,24 +155,12 @@ where .class(theme::Button::MenuItem) } +/// The type of menu item #[derive(Clone)] -/// Represents a menu item that performs an action when selected or a separator between menu items. -/// -/// - `Action` - Represents a menu item that performs an action when selected. -/// - `L` - The label of the menu item. -/// - `A` - The action to perform when the menu item is selected, the action must implement the `MenuAction` trait. -/// - `CheckBox` - Represents a checkbox menu item. -/// - `L` - The label of the menu item. -/// - `bool` - The state of the checkbox. -/// - `A` - The action to perform when the menu item is selected, the action must implement the `MenuAction` trait. -/// - `Folder` - Represents a folder menu item. -/// - `L` - The label of the menu item. -/// - `Vec>` - A vector of menu items. -/// - `Divider` - Represents a divider between menu items. -pub enum MenuItem>> { +pub enum MenuItemKind>> { /// Represents a button menu item. Button(L, Option, A), - /// Represents a button menu item that is disabled. + /// Represents a button menu item that's disabled. ButtonDisabled(L, Option, A), /// Represents a checkbox menu item. CheckBox(L, Option, bool, A), @@ -182,6 +170,54 @@ pub enum MenuItem>> { Divider, } +#[derive(Clone)] +/// A menu item with optional width configuration +pub struct MenuItem>> { + /// Kind of menu item. + kind: MenuItemKind, + /// Optional width override for this item's submenu. + width: Option, +} + +impl>> MenuItem { + /// Create from a kind with no width set + pub fn new(kind: MenuItemKind) -> Self { + Self { kind, width: None } + } + + /// Builder method to set width + pub fn width(mut self, width: u16) -> Self { + self.width = Some(width); + self + } + + pub fn button(label: L, icon: Option, action: A) -> Self { + Self::new(MenuItemKind::Button(label, icon, action)) + } + + pub fn button_disabled(label: L, icon: Option, action: A) -> Self { + Self::new(MenuItemKind::ButtonDisabled(label, icon, action)) + } + + pub fn checkbox(label: L, icon: Option, checked: bool, action: A) -> Self { + Self::new(MenuItemKind::CheckBox(label, icon, checked, action)) + } + + pub fn folder(label: L, children: Vec>) -> Self { + Self::new(MenuItemKind::Folder(label, children)) + } + + pub fn divider() -> Self { + Self::new(MenuItemKind::Divider) + } +} + +impl>> From> for MenuItem { + fn from(kind: MenuItemKind) -> Self { + Self::new(kind) + } +} + /// Create a root menu item. /// /// # Arguments @@ -246,9 +282,10 @@ pub fn menu_items< .flat_map(|(i, item)| { let mut trees = vec![]; let spacing = crate::theme::spacing(); + let item_width = item.width; - match item { - MenuItem::Button(label, icon, action) => { + match item.kind { + MenuItemKind::Button(label, icon, action) => { let l: Cow<'static, str> = label.into(); let key = find_key(&action, key_binds); let mut items = vec![ @@ -264,9 +301,16 @@ pub fn menu_items< let menu_button = menu_button(items).on_press(action.message()); - trees.push(MenuTree::::from(Element::from(menu_button))); + // Add a user designated width + let mut tree = MenuTree::::from(Element::from(menu_button)); + + if let Some(width) = item_width { + tree = tree.width(width); + } + + trees.push(tree); } - MenuItem::ButtonDisabled(label, icon, action) => { + MenuItemKind::ButtonDisabled(label, icon, action) => { let l: Cow<'static, str> = label.into(); let key = find_key(&action, key_binds); @@ -284,9 +328,15 @@ pub fn menu_items< let menu_button = menu_button(items); - trees.push(MenuTree::::from(Element::from(menu_button))); + let mut tree = MenuTree::::from(Element::from(menu_button)); + + if let Some(width) = item_width { + tree = tree.width(width); + } + + trees.push(tree); } - MenuItem::CheckBox(label, icon, value, action) => { + MenuItemKind::CheckBox(label, icon, value, action) => { let key = find_key(&action, key_binds); let mut items = vec![ if value { @@ -314,14 +364,20 @@ pub fn menu_items< items.insert(2, widget::icon::icon(icon).size(14).into()); } - trees.push(MenuTree::from(Element::from( + let mut tree = MenuTree::from(Element::from( menu_button(items).on_press(action.message()), - ))); + )); + + if let Some(width) = item_width { + tree = tree.width(width); + } + + trees.push(tree); } - MenuItem::Folder(label, children) => { + MenuItemKind::Folder(label, children) => { let l: Cow<'static, str> = label.into(); - trees.push(MenuTree::::with_children( + let mut tree = MenuTree::::with_children( RcElementWrapper::new(crate::Element::from( menu_button::<'static, _>(vec![ widget::text(l.clone()).into(), @@ -343,13 +399,25 @@ pub fn menu_items< ), )), menu_items(key_binds, children), - )); + ); + + if let Some(width) = item_width { + tree = tree.width(width); + } + + trees.push(tree); } - MenuItem::Divider => { + MenuItemKind::Divider => { if i != size - 1 { - trees.push(MenuTree::::from(Element::from( + let mut tree = MenuTree::::from(Element::from( widget::divider::horizontal::light(), - ))); + )); + + if let Some(width) = item_width { + tree = tree.width(width); + } + + trees.push(tree); } } } From 6158af1991965b31172e5d3425a5248b1eec404a Mon Sep 17 00:00:00 2001 From: Bryan Hyland Date: Mon, 19 Jan 2026 14:40:38 -0800 Subject: [PATCH 2/3] fixup! menu_bar: allow custom width on menu items --- src/widget/responsive_menu_bar.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/widget/responsive_menu_bar.rs b/src/widget/responsive_menu_bar.rs index 5f855260..af6c66d9 100644 --- a/src/widget/responsive_menu_bar.rs +++ b/src/widget/responsive_menu_bar.rs @@ -132,7 +132,7 @@ impl ResponsiveMenuBar { key_binds, trees .into_iter() - .map(|mt| menu::Item::Folder(mt.0, mt.1)) + .map(|mt| menu::Item::folder(mt.0, mt.1)) .collect(), ) .into_iter() From 7903ee12c0b08d390d6189b47438514bda4a2639 Mon Sep 17 00:00:00 2001 From: Bryan Hyland Date: Mon, 2 Feb 2026 09:49:03 -0800 Subject: [PATCH 3/3] menu_bar: add min_width/max_width constraints and update examples --- examples/application/src/main.rs | 60 +++++++------- examples/context-menu/src/main.rs | 16 ++-- examples/menu/src/main.rs | 17 ++-- examples/nav-context/src/main.rs | 6 +- examples/table-view/src/main.rs | 8 +- src/widget/menu/menu_inner.rs | 7 +- src/widget/menu/menu_tree.rs | 132 +++++++++++++++++++++++++++++- 7 files changed, 191 insertions(+), 55 deletions(-) diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs index 45805579..c9a54b87 100644 --- a/examples/application/src/main.rs +++ b/examples/application/src/main.rs @@ -236,26 +236,26 @@ impl cosmic::Application for App { ( "hi 1".into(), vec![ - menu::Item::Button("hi 12", None, Action::Hi), - menu::Item::Button("hi 13", None, Action::Hi2), + menu::Item::button("hi 12", None, Action::Hi), + menu::Item::button("hi 13", None, Action::Hi2), ], ), ( "hi 2".into(), vec![ - menu::Item::Button("hi 21", None, Action::Hi), - menu::Item::Button("hi 22", None, Action::Hi2), - menu::Item::Folder( + menu::Item::button("hi 21", None, Action::Hi), + menu::Item::button("hi 22", None, Action::Hi2), + menu::Item::folder( "nest 3 2 >".into(), vec![ - menu::Item::Button("21", None, Action::Hi), - menu::Item::Button("242", None, Action::Hi2), - menu::Item::Button("2443", None, Action::Hi3), - menu::Item::Folder( + menu::Item::button("21", None, Action::Hi), + menu::Item::button("242", None, Action::Hi2), + menu::Item::button("2443", None, Action::Hi3), + menu::Item::folder( "nest 4 2 >".into(), vec![ - menu::Item::Button("243", None, Action::Hi2), - menu::Item::Button("2444", None, Action::Hi), + menu::Item::button("243", None, Action::Hi2), + menu::Item::button("2444", None, Action::Hi), ], ), ], @@ -265,34 +265,34 @@ impl cosmic::Application for App { ( "hi 3".into(), vec![ - menu::Item::Button("hi 31", None, Action::Hi), - menu::Item::Button("hi 332", None, Action::Hi2), - menu::Item::Button("hi 3333", None, Action::Hi3), - menu::Item::Button("hi 33334", None, Action::Hi3), - menu::Item::Button("hi 333335", None, Action::Hi3), - menu::Item::Button("hi 3333336", None, Action::Hi3), + menu::Item::button("hi 31", None, Action::Hi), + menu::Item::button("hi 332", None, Action::Hi2), + menu::Item::button("hi 3333", None, Action::Hi3), + menu::Item::button("hi 33334", None, Action::Hi3), + menu::Item::button("hi 333335", None, Action::Hi3), + menu::Item::button("hi 3333336", None, Action::Hi3), ], ), ( "hiiiiiiiiiiiiiiiiiii 4".into(), vec![ - menu::Item::Button("hi 4", None, Action::Hi), - menu::Item::Button("hi 44", None, Action::Hi2), - menu::Item::Button("hi 444", None, Action::Hi3), - menu::Item::Folder( + menu::Item::button("hi 4", None, Action::Hi), + menu::Item::button("hi 44", None, Action::Hi2), + menu::Item::button("hi 444", None, Action::Hi3), + menu::Item::folder( "nest 4 >".into(), vec![ - menu::Item::Button("hi 41", None, Action::Hi), - menu::Item::Button("hi 442", None, Action::Hi2), - menu::Item::Folder( + menu::Item::button("hi 41", None, Action::Hi), + menu::Item::button("hi 442", None, Action::Hi2), + menu::Item::folder( "nest 3 4 >".into(), vec![ - menu::Item::Button("hi 443", None, Action::Hi2), - menu::Item::Button("hi 4444", None, Action::Hi), - menu::Item::Button("hi 44444", None, Action::Hi3), - menu::Item::Button("hi 444445", None, Action::Hi3), - menu::Item::Button("hi 4444446", None, Action::Hi3), - menu::Item::Button("hi 44444447", None, Action::Hi3), + menu::Item::button("hi 443", None, Action::Hi2), + menu::Item::button("hi 4444", None, Action::Hi), + menu::Item::button("hi 44444", None, Action::Hi3), + menu::Item::button("hi 444445", None, Action::Hi3), + menu::Item::button("hi 4444446", None, Action::Hi3), + menu::Item::button("hi 44444447", None, Action::Hi3), ], ), ], diff --git a/examples/context-menu/src/main.rs b/examples/context-menu/src/main.rs index db66ba1b..1950906d 100644 --- a/examples/context-menu/src/main.rs +++ b/examples/context-menu/src/main.rs @@ -122,19 +122,21 @@ impl App { Some(menu::items( &HashMap::new(), vec![ - menu::Item::Button("New window", None, ContextMenuAction::WindowNew), - menu::Item::Divider, - menu::Item::Folder( + menu::Item::button("New window", None, ContextMenuAction::WindowNew), + menu::Item::divider(), + menu::Item::folder( "View", - vec![menu::Item::CheckBox( + vec![menu::Item::checkbox( "Hide content", None, self.hide_content, ContextMenuAction::ToggleHideContent, )], - ), - menu::Item::Divider, - menu::Item::Button("Quit", None, ContextMenuAction::WindowClose), + ) + .width(200) + .min_width(180), + menu::Item::divider(), + menu::Item::button("Quit", None, ContextMenuAction::WindowClose), ], )) } diff --git a/examples/menu/src/main.rs b/examples/menu/src/main.rs index 8b5a1cb7..bae10c68 100644 --- a/examples/menu/src/main.rs +++ b/examples/menu/src/main.rs @@ -160,23 +160,26 @@ pub fn menu_bar<'a>(config: &Config, key_binds: &HashMap) -> El menu::items( key_binds, vec![ - menu::Item::Button( + menu::Item::button( "New window", Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()), Action::WindowNew, ), - menu::Item::Divider, - menu::Item::Folder( + menu::Item::divider(), + menu::Item::folder( "View", - vec![menu::Item::CheckBox( + vec![menu::Item::checkbox( "Hide content", Some(cosmic::widget::icon::from_name("view-conceal-symbolic").into()), config.hide_content, Action::ToggleHideContent, )], - ), - menu::Item::Divider, - menu::Item::Button( + ) + .width(280) + .min_width(200) + .max_width(300), + menu::Item::divider(), + menu::Item::button( "Quit", Some(cosmic::widget::icon::from_name("window-close-symbolic").into()), Action::WindowClose, diff --git a/examples/nav-context/src/main.rs b/examples/nav-context/src/main.rs index fdfb90f9..7a677cd2 100644 --- a/examples/nav-context/src/main.rs +++ b/examples/nav-context/src/main.rs @@ -135,9 +135,9 @@ impl cosmic::Application for App { Some(menu::items( &HashMap::new(), vec![ - menu::Item::Button("Move Up", None, NavMenuAction::MoveUp(id)), - menu::Item::Button("Move Down", None, NavMenuAction::MoveDown(id)), - menu::Item::Button("Delete", None, NavMenuAction::Delete(id)), + menu::Item::button("Move Up", None, NavMenuAction::MoveUp(id)), + menu::Item::button("Move Down", None, NavMenuAction::MoveDown(id)), + menu::Item::button("Delete", None, NavMenuAction::Delete(id)), ], )) } diff --git a/examples/table-view/src/main.rs b/examples/table-view/src/main.rs index bbd9cf5b..abb579c6 100644 --- a/examples/table-view/src/main.rs +++ b/examples/table-view/src/main.rs @@ -212,7 +212,7 @@ impl cosmic::Application for App { .item_context(move |item| { Some(widget::menu::items( &HashMap::new(), - vec![widget::menu::Item::Button( + vec![widget::menu::Item::button( format!("Action on {}", item.name.to_string()), None, Action::None, @@ -227,7 +227,7 @@ impl cosmic::Application for App { .item_context(|item| { Some(widget::menu::items( &HashMap::new(), - vec![widget::menu::Item::Button( + vec![widget::menu::Item::button( format!("Action on {}", item.name), None, Action::None, @@ -238,12 +238,12 @@ impl cosmic::Application for App { Some(widget::menu::items( &HashMap::new(), vec![ - widget::menu::Item::Button( + widget::menu::Item::button( format!("Action on {} category", category.to_string()), None, Action::None, ), - widget::menu::Item::Button( + widget::menu::Item::button( format!("Other action on {} category", category.to_string()), None, Action::None, diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index c455cd13..4b2a7498 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -1647,7 +1647,12 @@ fn get_children_layout( ) -> (Size, Vec, Vec) { let width = match item_width { ItemWidth::Uniform(u) => f32::from(u), - ItemWidth::Static(s) => f32::from(menu_tree.width.unwrap_or(s)), + ItemWidth::Static(s) => { + let base = f32::from(menu_tree.width.unwrap_or(s)); + let min = menu_tree.min_width.map(f32::from).unwrap_or(0.0); + let max = menu_tree.max_width.map(f32::from).unwrap_or(f32::MAX); + base.clamp(min, max) + } }; let child_sizes: Vec = match item_height { diff --git a/src/widget/menu/menu_tree.rs b/src/widget/menu/menu_tree.rs index 378e0939..b8bb4a9e 100644 --- a/src/widget/menu/menu_tree.rs +++ b/src/widget/menu/menu_tree.rs @@ -36,6 +36,10 @@ pub struct MenuTree { pub(crate) children: Vec>, /// The width of the menu tree pub(crate) width: Option, + /// The min width of the menu tree + pub(crate) min_width: Option, + /// The max width of the menu tree + pub(crate) max_width: Option, /// The height of the menu tree pub(crate) height: Option, } @@ -48,6 +52,8 @@ impl MenuTree { item: item.into(), children: Vec::new(), width: None, + min_width: None, + max_width: None, height: None, } } @@ -62,6 +68,8 @@ impl MenuTree { item: item.into(), children: children.into_iter().map(Into::into).collect(), width: None, + min_width: None, + max_width: None, height: None, } } @@ -76,6 +84,18 @@ impl MenuTree { self } + /// Sets the min width of the menu tree. + pub fn min_width(mut self, min: u16) -> Self { + self.min_width = Some(min); + self + } + + /// Sets the max width of the menu tree. + pub fn max_width(mut self, max: u16) -> Self { + self.max_width = Some(max); + self + } + /// Sets the height of the menu tree. /// See [`ItemHeight`] /// @@ -170,19 +190,66 @@ pub enum MenuItemKind>> { Divider, } +/// A menu item with optional width configuration. +/// +/// # Examples +/// +/// ```ignore +/// use cosmic::widget::menu; +/// +/// // Simple button +/// menu::Item::button("Save", None, Action::Save); +/// +/// // Button with icon +/// menu::Item::button( +/// "Open", +/// Some(cosmic::widget::icon::from_name("document-open-symbolic").into()), +/// Action::Open, +/// ); +/// +/// // Checkbox +/// menu::Item::checkbox("Show Hidden", None, true, Action::ToggleHidden); +/// +/// // Folder with custom width +/// menu::Item::folder("Recent", vec![ +/// menu::Item::button("file1.txt", None, Action::OpenRecent(0)), +/// ]).width(300); +/// +/// // Divider +/// menu::Item::divider(); +/// +/// // Folder with custom width constraints +/// menu::Item::folder("Recent", vec![ +/// menu::Item::button("file1.txt", None, Action::OpenRecent(0)), +/// ]).width(300).min_width(200).max_width(400); +/// +/// // Using min_width to ensure a minimum size +/// menu::Item::button("Short", None, Action::Short).min_width(150); +/// +/// // Using max_width to cap the size +/// menu::Item::button("Very Long Label Here", None, Action::Long).max_width(200); +/// ``` #[derive(Clone)] -/// A menu item with optional width configuration pub struct MenuItem>> { /// Kind of menu item. kind: MenuItemKind, - /// Optional width override for this item's submenu. + /// Optional width override for this item. width: Option, + /// Optional min width for this item. + min_width: Option, + /// Optional max width for this item. + max_width: Option, } impl>> MenuItem { /// Create from a kind with no width set pub fn new(kind: MenuItemKind) -> Self { - Self { kind, width: None } + Self { + kind, + width: None, + min_width: None, + max_width: None, + } } /// Builder method to set width @@ -191,22 +258,39 @@ impl>> MenuItem { self } + /// Builder method to set minimum width + pub fn min_width(mut self, min: u16) -> Self { + self.min_width = Some(min); + self + } + + /// Builder method to set max width + pub fn max_width(mut self, max: u16) -> Self { + self.max_width = Some(max); + self + } + + /// Create a button menu item. pub fn button(label: L, icon: Option, action: A) -> Self { Self::new(MenuItemKind::Button(label, icon, action)) } + /// Create a disabled button menu item. pub fn button_disabled(label: L, icon: Option, action: A) -> Self { Self::new(MenuItemKind::ButtonDisabled(label, icon, action)) } + /// Create a checkbox menu item. pub fn checkbox(label: L, icon: Option, checked: bool, action: A) -> Self { Self::new(MenuItemKind::CheckBox(label, icon, checked, action)) } + /// Create a folder (submenu) menu item. pub fn folder(label: L, children: Vec>) -> Self { Self::new(MenuItemKind::Folder(label, children)) } + /// Create a divider between menu items. pub fn divider() -> Self { Self::new(MenuItemKind::Divider) } @@ -283,6 +367,8 @@ pub fn menu_items< let mut trees = vec![]; let spacing = crate::theme::spacing(); let item_width = item.width; + let item_min_width = item.min_width; + let item_max_width = item.max_width; match item.kind { MenuItemKind::Button(label, icon, action) => { @@ -308,6 +394,14 @@ pub fn menu_items< tree = tree.width(width); } + if let Some(min_width) = item_min_width { + tree = tree.min_width(min_width); + } + + if let Some(max_width) = item_max_width { + tree = tree.max_width(max_width); + } + trees.push(tree); } MenuItemKind::ButtonDisabled(label, icon, action) => { @@ -334,6 +428,14 @@ pub fn menu_items< tree = tree.width(width); } + if let Some(min_width) = item_min_width { + tree = tree.min_width(min_width); + } + + if let Some(max_width) = item_max_width { + tree = tree.max_width(max_width); + } + trees.push(tree); } MenuItemKind::CheckBox(label, icon, value, action) => { @@ -372,6 +474,14 @@ pub fn menu_items< tree = tree.width(width); } + if let Some(min_width) = item_min_width { + tree = tree.min_width(min_width); + } + + if let Some(max_width) = item_max_width { + tree = tree.max_width(max_width); + } + trees.push(tree); } MenuItemKind::Folder(label, children) => { @@ -405,6 +515,14 @@ pub fn menu_items< tree = tree.width(width); } + if let Some(min_width) = item_min_width { + tree = tree.min_width(min_width); + } + + if let Some(max_width) = item_max_width { + tree = tree.max_width(max_width); + } + trees.push(tree); } MenuItemKind::Divider => { @@ -417,6 +535,14 @@ pub fn menu_items< tree = tree.width(width); } + if let Some(min_width) = item_min_width { + tree = tree.min_width(min_width); + } + + if let Some(max_width) = item_max_width { + tree = tree.max_width(max_width); + } + trees.push(tree); } }