feat(menu): add Item::Entry with a reservable icon slot
This commit is contained in:
parent
2fb052d134
commit
aac833d37d
2 changed files with 170 additions and 115 deletions
|
|
@ -71,8 +71,8 @@ pub mod menu_column;
|
||||||
mod menu_inner;
|
mod menu_inner;
|
||||||
mod menu_tree;
|
mod menu_tree;
|
||||||
pub use menu_tree::{
|
pub use menu_tree::{
|
||||||
MenuItem as Item, MenuTree as Tree, menu_button, menu_items as items, menu_root as root,
|
Entry, IconSlot, MenuItem as Item, MenuTree as Tree, menu_button, menu_items as items,
|
||||||
nav_context,
|
menu_root as root, nav_context,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use crate::style::menu_bar::{Appearance, StyleSheet};
|
pub use crate::style::menu_bar::{Appearance, StyleSheet};
|
||||||
|
|
|
||||||
|
|
@ -180,6 +180,84 @@ pub enum MenuItem<A: MenuAction, L: Into<Cow<'static, str>>> {
|
||||||
Folder(L, Vec<MenuItem<A, L>>),
|
Folder(L, Vec<MenuItem<A, L>>),
|
||||||
/// Represents a divider between menu items.
|
/// Represents a divider between menu items.
|
||||||
Divider,
|
Divider,
|
||||||
|
/// A menu entry with every option available; see [`Entry`].
|
||||||
|
Entry(Entry<A, L>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: MenuAction, L: Into<Cow<'static, str>>> MenuItem<A, L> {
|
||||||
|
/// Create an [`Entry`] menu item, configure it with the builder methods on [`Entry`].
|
||||||
|
pub fn entry(label: L, action: A) -> Self {
|
||||||
|
MenuItem::Entry(Entry::new(label, action))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The leading icon column of a menu entry.
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub enum IconSlot {
|
||||||
|
/// No icon and no space reserved for one.
|
||||||
|
#[default]
|
||||||
|
None,
|
||||||
|
/// No icon, but the space for an icon is reserved (indented entry)
|
||||||
|
Reserved,
|
||||||
|
/// An icon.
|
||||||
|
Icon(icon::Handle),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Option<icon::Handle>> for IconSlot {
|
||||||
|
fn from(icon: Option<icon::Handle>) -> Self {
|
||||||
|
icon.map_or(IconSlot::None, IconSlot::Icon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A menu entry: label, optional leading icon, optional check column, enabled state and action.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Entry<A, L> {
|
||||||
|
label: L,
|
||||||
|
icon: IconSlot,
|
||||||
|
/// `Some` draws the check column
|
||||||
|
checked: Option<bool>,
|
||||||
|
enabled: bool,
|
||||||
|
action: A,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A, L> Entry<A, L> {
|
||||||
|
pub fn new(label: L, action: A) -> Self {
|
||||||
|
Self {
|
||||||
|
label,
|
||||||
|
icon: IconSlot::None,
|
||||||
|
checked: None,
|
||||||
|
enabled: true,
|
||||||
|
action,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw a leading icon
|
||||||
|
#[must_use]
|
||||||
|
pub fn icon(mut self, icon: icon::Handle) -> Self {
|
||||||
|
self.icon = IconSlot::Icon(icon);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw no icon, but resever the space
|
||||||
|
#[must_use]
|
||||||
|
pub fn reserve_icon(mut self) -> Self {
|
||||||
|
self.icon = IconSlot::Reserved;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show a check column, ticked when `checked` is true, empty sapce when false
|
||||||
|
#[must_use]
|
||||||
|
pub fn checked(mut self, checked: bool) -> Self {
|
||||||
|
self.checked = Some(checked);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disabled entries are drawn dimmed and do not react to presses
|
||||||
|
#[must_use]
|
||||||
|
pub fn enabled(mut self, enabled: bool) -> Self {
|
||||||
|
self.enabled = enabled;
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a root menu item.
|
/// Create a root menu item.
|
||||||
|
|
@ -201,6 +279,84 @@ where
|
||||||
.class(theme::Button::MenuRoot)
|
.class(theme::Button::MenuRoot)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn entry_tree<
|
||||||
|
A: MenuAction<Message = Message>,
|
||||||
|
L: Into<Cow<'static, str>>,
|
||||||
|
Message: Clone + 'static,
|
||||||
|
>(
|
||||||
|
entry: Entry<A, L>,
|
||||||
|
key_binds: &HashMap<KeyBind, A>,
|
||||||
|
key_class: theme::Text,
|
||||||
|
) -> MenuTree<Message> {
|
||||||
|
let Entry {
|
||||||
|
label,
|
||||||
|
icon,
|
||||||
|
checked,
|
||||||
|
enabled,
|
||||||
|
action,
|
||||||
|
} = entry;
|
||||||
|
let spacing = crate::theme::spacing();
|
||||||
|
let key = key_binds
|
||||||
|
.iter()
|
||||||
|
.find(|(_, a)| **a == action)
|
||||||
|
.map_or_else(String::new, |(k, _)| k.to_string());
|
||||||
|
|
||||||
|
let mut items: Vec<crate::Element<'static, Message>> = Vec::with_capacity(7);
|
||||||
|
|
||||||
|
if let Some(checked) = checked {
|
||||||
|
items.push(if checked {
|
||||||
|
widget::icon::from_name("object-select-symbolic")
|
||||||
|
.size(16)
|
||||||
|
.icon()
|
||||||
|
.class(theme::Svg::Custom(Rc::new(|theme| {
|
||||||
|
iced_widget::svg::Style {
|
||||||
|
color: Some(theme.cosmic().accent_text_color().into()),
|
||||||
|
}
|
||||||
|
})))
|
||||||
|
.width(Length::Fixed(16.0))
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
widget::space::horizontal()
|
||||||
|
.width(Length::Fixed(16.0))
|
||||||
|
.into()
|
||||||
|
});
|
||||||
|
items.push(widget::space::horizontal().width(spacing.space_xxs).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
match icon {
|
||||||
|
IconSlot::Icon(icon) => {
|
||||||
|
items.push(widget::icon::icon(icon).size(14).into());
|
||||||
|
items.push(widget::space::horizontal().width(spacing.space_xxs).into());
|
||||||
|
}
|
||||||
|
IconSlot::Reserved => {
|
||||||
|
items.push(
|
||||||
|
widget::space::horizontal()
|
||||||
|
.width(Length::Fixed(14.0))
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
items.push(widget::space::horizontal().width(spacing.space_xxs).into());
|
||||||
|
}
|
||||||
|
IconSlot::None => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let ellipsize =
|
||||||
|
iced_core::text::Ellipsize::Middle(iced_core::text::EllipsizeHeightLimit::Lines(1));
|
||||||
|
items.push(widget::text(label.into()).ellipsize(ellipsize).into());
|
||||||
|
items.push(widget::space::horizontal().into());
|
||||||
|
items.push(
|
||||||
|
widget::text(key)
|
||||||
|
.class(key_class)
|
||||||
|
.ellipsize(ellipsize)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut button = menu_button(items);
|
||||||
|
if enabled {
|
||||||
|
button = button.on_press(action.message());
|
||||||
|
}
|
||||||
|
MenuTree::from(Element::from(button))
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a list of menu items from a vector of `MenuItem`.
|
/// Create a list of menu items from a vector of `MenuItem`.
|
||||||
///
|
///
|
||||||
/// The `MenuItem` can be either an action or a separator.
|
/// The `MenuItem` can be either an action or a separator.
|
||||||
|
|
@ -220,15 +376,6 @@ pub fn menu_items<
|
||||||
key_binds: &HashMap<KeyBind, A>,
|
key_binds: &HashMap<KeyBind, A>,
|
||||||
children: Vec<MenuItem<A, L>>,
|
children: Vec<MenuItem<A, L>>,
|
||||||
) -> Vec<MenuTree<Message>> {
|
) -> Vec<MenuTree<Message>> {
|
||||||
fn find_key<A: MenuAction>(action: &A, key_binds: &HashMap<KeyBind, A>) -> String {
|
|
||||||
for (key_bind, key_action) in key_binds {
|
|
||||||
if action == key_action {
|
|
||||||
return key_bind.to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
String::new()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn key_style(theme: &crate::Theme) -> TextStyle {
|
fn key_style(theme: &crate::Theme) -> TextStyle {
|
||||||
let mut color = theme.cosmic().background(theme.transparent).component.on;
|
let mut color = theme.cosmic().background(theme.transparent).component.on;
|
||||||
color.alpha *= 0.75;
|
color.alpha *= 0.75;
|
||||||
|
|
@ -246,117 +393,25 @@ pub fn menu_items<
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.flat_map(|(i, item)| {
|
.flat_map(|(i, item)| {
|
||||||
let mut trees = vec![];
|
let mut trees = vec![];
|
||||||
let spacing = crate::theme::spacing();
|
|
||||||
|
|
||||||
match item {
|
match item {
|
||||||
MenuItem::Button(label, icon, action) => {
|
MenuItem::Button(label, icon, action) => {
|
||||||
let l: Cow<'static, str> = label.into();
|
let mut entry = Entry::new(label, action);
|
||||||
let key = find_key(&action, key_binds);
|
entry.icon = icon.into();
|
||||||
let mut items = vec![
|
trees.push(entry_tree(entry, key_binds, key_class.clone()));
|
||||||
widget::text(l)
|
|
||||||
.ellipsize(iced_core::text::Ellipsize::Middle(
|
|
||||||
iced_core::text::EllipsizeHeightLimit::Lines(1),
|
|
||||||
))
|
|
||||||
.into(),
|
|
||||||
widget::space::horizontal().into(),
|
|
||||||
widget::text(key)
|
|
||||||
.class(key_class)
|
|
||||||
.ellipsize(iced_core::text::Ellipsize::Middle(
|
|
||||||
iced_core::text::EllipsizeHeightLimit::Lines(1),
|
|
||||||
))
|
|
||||||
.into(),
|
|
||||||
];
|
|
||||||
|
|
||||||
if let Some(icon) = icon {
|
|
||||||
items.insert(0, widget::icon::icon(icon).size(14).into());
|
|
||||||
items.insert(
|
|
||||||
1,
|
|
||||||
widget::space::horizontal().width(spacing.space_xxs).into(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let menu_button = menu_button(items).on_press(action.message());
|
|
||||||
|
|
||||||
trees.push(MenuTree::<Message>::from(Element::from(menu_button)));
|
|
||||||
}
|
}
|
||||||
MenuItem::ButtonDisabled(label, icon, action) => {
|
MenuItem::ButtonDisabled(label, icon, action) => {
|
||||||
let l: Cow<'static, str> = label.into();
|
let mut entry = Entry::new(label, action).enabled(false);
|
||||||
|
entry.icon = icon.into();
|
||||||
let key = find_key(&action, key_binds);
|
trees.push(entry_tree(entry, key_binds, key_class.clone()));
|
||||||
|
|
||||||
let mut items = vec![
|
|
||||||
widget::text(l)
|
|
||||||
.ellipsize(iced_core::text::Ellipsize::Middle(
|
|
||||||
iced_core::text::EllipsizeHeightLimit::Lines(1),
|
|
||||||
))
|
|
||||||
.into(),
|
|
||||||
widget::space::horizontal().into(),
|
|
||||||
widget::text(key)
|
|
||||||
.ellipsize(iced_core::text::Ellipsize::Middle(
|
|
||||||
iced_core::text::EllipsizeHeightLimit::Lines(1),
|
|
||||||
))
|
|
||||||
.class(key_class)
|
|
||||||
.into(),
|
|
||||||
];
|
|
||||||
|
|
||||||
if let Some(icon) = icon {
|
|
||||||
items.insert(0, widget::icon::icon(icon).size(14).into());
|
|
||||||
items.insert(
|
|
||||||
1,
|
|
||||||
widget::space::horizontal().width(spacing.space_xxs).into(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let menu_button = menu_button(items);
|
|
||||||
|
|
||||||
trees.push(MenuTree::<Message>::from(Element::from(menu_button)));
|
|
||||||
}
|
}
|
||||||
MenuItem::CheckBox(label, icon, value, action) => {
|
MenuItem::CheckBox(label, icon, value, action) => {
|
||||||
let key = find_key(&action, key_binds);
|
let mut entry = Entry::new(label, action).checked(value);
|
||||||
let mut items = vec![
|
entry.icon = icon.into();
|
||||||
if value {
|
trees.push(entry_tree(entry, key_binds, key_class.clone()));
|
||||||
widget::icon::from_name("object-select-symbolic")
|
}
|
||||||
.size(16)
|
MenuItem::Entry(entry) => {
|
||||||
.icon()
|
trees.push(entry_tree(entry, key_binds, key_class.clone()));
|
||||||
.class(theme::Svg::Custom(Rc::new(|theme| {
|
|
||||||
iced_widget::svg::Style {
|
|
||||||
color: Some(theme.cosmic().accent_text_color().into()),
|
|
||||||
}
|
|
||||||
})))
|
|
||||||
.width(Length::Fixed(16.0))
|
|
||||||
.into()
|
|
||||||
} else {
|
|
||||||
widget::space::horizontal()
|
|
||||||
.width(Length::Fixed(16.0))
|
|
||||||
.into()
|
|
||||||
},
|
|
||||||
widget::space::horizontal().width(spacing.space_xxs).into(),
|
|
||||||
widget::text(label)
|
|
||||||
.ellipsize(iced_core::text::Ellipsize::Middle(
|
|
||||||
iced_core::text::EllipsizeHeightLimit::Lines(1),
|
|
||||||
))
|
|
||||||
.align_x(iced::Alignment::Start)
|
|
||||||
.into(),
|
|
||||||
widget::space::horizontal().into(),
|
|
||||||
widget::text(key)
|
|
||||||
.class(key_class)
|
|
||||||
.ellipsize(iced_core::text::Ellipsize::Middle(
|
|
||||||
iced_core::text::EllipsizeHeightLimit::Lines(1),
|
|
||||||
))
|
|
||||||
.into(),
|
|
||||||
];
|
|
||||||
|
|
||||||
if let Some(icon) = icon {
|
|
||||||
items.insert(
|
|
||||||
1,
|
|
||||||
widget::space::horizontal().width(spacing.space_xxs).into(),
|
|
||||||
);
|
|
||||||
items.insert(2, widget::icon::icon(icon).size(14).into());
|
|
||||||
}
|
|
||||||
|
|
||||||
trees.push(MenuTree::from(Element::from(
|
|
||||||
menu_button(items).on_press(action.message()),
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
MenuItem::Folder(label, children) => {
|
MenuItem::Folder(label, children) => {
|
||||||
let l: Cow<'static, str> = label.into();
|
let l: Cow<'static, str> = label.into();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue