use std::collections::HashMap;
use std::fmt;
use std::sync::{Arc, Mutex};
use serde::Serialize;
use zbus::zvariant::{OwnedValue, Str, Type, Value};
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Type, Serialize)]
#[zvariant(signature = "s")]
pub enum TextDirection {
#[serde(rename = "ltr")]
LeftToRight,
#[serde(rename = "rtl")]
RightToLeft,
}
impl From<TextDirection> for Value<'_> {
fn from(value: TextDirection) -> Self {
value.to_string().into()
}
}
impl fmt::Display for TextDirection {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
self.serialize(f)
}
}
#[derive(Type, Serialize)]
#[zvariant(signature = "s")]
#[serde(rename_all = "lowercase")]
pub(crate) enum Status {
Normal,
Notice,
}
impl From<Status> for Value<'_> {
fn from(value: Status) -> Self {
value.to_string().into()
}
}
impl fmt::Display for Status {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
self.serialize(f)
}
}
pub enum MenuItem<T> {
Standard(StandardItem<T>),
Separator,
Checkmark(CheckmarkItem<T>),
SubMenu(SubMenu<T>),
RadioGroup(RadioGroup<T>),
}
pub struct StandardItem<T> {
pub label: String,
pub enabled: bool,
pub visible: bool,
pub icon_name: String,
pub icon_data: Vec<u8>,
pub shortcut: Vec<Vec<String>>,
pub disposition: Disposition,
pub activate: Box<dyn Fn(&mut T) + Send>,
}
impl<T> Default for StandardItem<T> {
fn default() -> Self {
StandardItem {
label: String::default(),
enabled: true,
visible: true,
icon_name: String::default(),
icon_data: Vec::default(),
shortcut: Vec::default(),
disposition: Disposition::Normal,
activate: Box::new(|_this| {}),
}
}
}
impl<T> From<StandardItem<T>> for MenuItem<T> {
fn from(item: StandardItem<T>) -> Self {
MenuItem::Standard(item)
}
}
impl<T: 'static> From<StandardItem<T>> for RawMenuItem<T> {
fn from(item: StandardItem<T>) -> Self {
let activate = item.activate;
Self {
r#type: ItemType::Standard,
label: item.label,
enabled: item.enabled,
visible: item.visible,
icon_name: item.icon_name,
icon_data: item.icon_data,
shortcut: item.shortcut,
disposition: item.disposition,
on_clicked: Box::new(move |this: &mut T, _id| {
(activate)(this);
}),
..Default::default()
}
}
}
pub struct SubMenu<T> {
pub label: String,
pub enabled: bool,
pub visible: bool,
pub icon_name: String,
pub icon_data: Vec<u8>,
pub shortcut: Vec<Vec<String>>,
pub disposition: Disposition,
pub submenu: Vec<MenuItem<T>>,
}
impl<T> Default for SubMenu<T> {
fn default() -> Self {
Self {
label: String::default(),
enabled: true,
visible: true,
icon_name: String::default(),
icon_data: Vec::default(),
shortcut: Vec::default(),
disposition: Disposition::Normal,
submenu: Vec::default(),
}
}
}
impl<T> From<SubMenu<T>> for MenuItem<T> {
fn from(item: SubMenu<T>) -> Self {
MenuItem::SubMenu(item)
}
}
impl<T> From<SubMenu<T>> for RawMenuItem<T> {
fn from(item: SubMenu<T>) -> Self {
Self {
r#type: ItemType::Standard,
label: item.label,
enabled: item.enabled,
visible: item.visible,
icon_name: item.icon_name,
icon_data: item.icon_data,
shortcut: item.shortcut,
disposition: item.disposition,
on_clicked: Box::new(move |_this: &mut T, _id| Default::default()),
..Default::default()
}
}
}
pub struct CheckmarkItem<T> {
pub label: String,
pub enabled: bool,
pub visible: bool,
pub checked: bool,
pub icon_name: String,
pub icon_data: Vec<u8>,
pub shortcut: Vec<Vec<String>>,
pub disposition: Disposition,
pub activate: Box<dyn Fn(&mut T) + Send>,
}
impl<T> Default for CheckmarkItem<T> {
fn default() -> Self {
CheckmarkItem {
label: String::default(),
enabled: true,
visible: true,
checked: false,
icon_name: String::default(),
icon_data: Vec::default(),
shortcut: Vec::default(),
disposition: Disposition::Normal,
activate: Box::new(|_this| {}),
}
}
}
impl<T> From<CheckmarkItem<T>> for MenuItem<T> {
fn from(item: CheckmarkItem<T>) -> Self {
MenuItem::Checkmark(item)
}
}
impl<T: 'static> From<CheckmarkItem<T>> for RawMenuItem<T> {
fn from(item: CheckmarkItem<T>) -> Self {
let activate = item.activate;
Self {
r#type: ItemType::Standard,
label: item.label,
enabled: item.enabled,
visible: item.visible,
icon_name: item.icon_name,
icon_data: item.icon_data,
shortcut: item.shortcut,
toggle_type: ToggleType::Checkmark,
toggle_state: if item.checked {
ToggleState::On
} else {
ToggleState::Off
},
disposition: item.disposition,
on_clicked: Box::new(move |this: &mut T, _id| {
(activate)(this);
}),
..Default::default()
}
}
}
pub struct RadioGroup<T> {
pub selected: usize,
pub select: Box<dyn Fn(&mut T, usize) + Send>,
pub options: Vec<RadioItem>,
}
impl<T> Default for RadioGroup<T> {
fn default() -> Self {
Self {
selected: 0,
select: Box::new(|_, _| {}),
options: Default::default(),
}
}
}
impl<T> From<RadioGroup<T>> for MenuItem<T> {
fn from(item: RadioGroup<T>) -> Self {
MenuItem::RadioGroup(item)
}
}
pub struct RadioItem {
pub label: String,
pub enabled: bool,
pub visible: bool,
pub icon_name: String,
pub icon_data: Vec<u8>,
pub shortcut: Vec<Vec<String>>,
pub disposition: Disposition,
}
impl Default for RadioItem {
fn default() -> Self {
Self {
label: String::default(),
enabled: true,
visible: true,
icon_name: String::default(),
icon_data: Vec::default(),
shortcut: Vec::default(),
disposition: Disposition::Normal,
}
}
}
pub(crate) struct RawMenuItem<T> {
r#type: ItemType,
label: String,
enabled: bool,
visible: bool,
icon_name: String,
icon_data: Vec<u8>,
shortcut: Vec<Vec<String>>,
toggle_type: ToggleType,
toggle_state: ToggleState,
disposition: Disposition,
pub on_clicked: Box<dyn Fn(&mut T, usize) + Send>,
}
macro_rules! if_not_default_then_insert {
($map: ident, $item: ident, $default: ident, $filter: ident, $property: ident) => {
if_not_default_then_insert!($map, $item, $default, $filter, $property, (|r| r));
};
($map: ident, $item: ident, $default: ident, $filter: ident, $property: ident, $to_refarg: tt) => {{
let name = stringify!($property).replace('_', "-");
if_not_default_then_insert!($map, $item, $default, $filter, $property, name, $to_refarg);
}};
($map: ident, $item: ident, $default: ident, $filter: ident, $property: ident, $property_name: tt, $to_refarg: tt) => {
if ($filter.is_empty() || $filter.contains(&$property_name.to_string()))
&& $item.$property != $default.$property
{
$map.insert(
$property_name.to_string(),
OwnedValue::from($to_refarg($item.$property.clone())),
);
}
};
}
impl<T> fmt::Debug for RawMenuItem<T> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Item {}", self.label)
}
}
impl<T> RawMenuItem<T> {
pub(crate) fn to_dbus_map(&self, property_filter: &[String]) -> HashMap<String, OwnedValue> {
let mut properties: HashMap<String, OwnedValue> = HashMap::with_capacity(11);
let default: RawMenuItem<T> = RawMenuItem::default();
if_not_default_then_insert!(
properties,
self,
default,
property_filter,
r#type,
"type",
(|r: ItemType| r)
);
if_not_default_then_insert!(
properties,
self,
default,
property_filter,
label,
(|r: String| -> Str { r.into() })
);
if_not_default_then_insert!(properties, self, default, property_filter, enabled);
if_not_default_then_insert!(properties, self, default, property_filter, visible);
if_not_default_then_insert!(
properties,
self,
default,
property_filter,
icon_name,
(|r: String| -> Str { r.into() })
);
if_not_default_then_insert!(
properties,
self,
default,
property_filter,
icon_data,
(|r: Vec<u8>| -> OwnedValue {
Value::from(r)
.try_into()
.expect("unreachable: Vec<u8> to OwnedValue")
})
);
if_not_default_then_insert!(
properties,
self,
default,
property_filter,
shortcut,
(|r: Vec<Vec<String>>| -> OwnedValue {
Value::from(r)
.try_into()
.expect("unreachable: Vec<Vec<String>> to OwnedValue")
})
);
if_not_default_then_insert!(properties, self, default, property_filter, toggle_type);
if_not_default_then_insert!(properties, self, default, property_filter, toggle_state);
if_not_default_then_insert!(properties, self, default, property_filter, disposition);
properties
}
pub(crate) fn diff(&self, other: &Self) -> Option<(HashMap<String, OwnedValue>, Vec<String>)> {
let default = Self::default();
let mut updated_props: HashMap<String, OwnedValue> = HashMap::new();
let mut removed_props = Vec::new();
if self.r#type != other.r#type {
if other.r#type == default.r#type {
removed_props.push("type".into());
} else {
updated_props.insert(
"type".into(),
<OwnedValue as From<Str>>::from(other.r#type.to_string().into()),
);
}
}
if self.label != other.label {
if other.label == default.label {
removed_props.push("label".into());
} else {
updated_props.insert(
"label".into(),
<OwnedValue as From<Str>>::from(other.label.clone().into()),
);
}
}
if self.enabled != other.enabled {
if other.enabled == default.enabled {
removed_props.push("enabled".into());
} else {
updated_props.insert("enabled".into(), OwnedValue::from(other.enabled));
}
}
if self.visible != other.visible {
if other.visible == default.visible {
removed_props.push("visible".into());
} else {
updated_props.insert("visible".into(), OwnedValue::from(other.visible));
}
}
if self.icon_name != other.icon_name {
if other.icon_name == default.icon_name {
removed_props.push("icon-name".into());
} else {
updated_props.insert(
"icon-name".into(),
<OwnedValue as From<Str>>::from(other.icon_name.clone().into()),
);
}
}
if self.icon_data != other.icon_data {
if other.icon_data == default.icon_data {
removed_props.push("icon-data".into());
} else {
updated_props.insert(
"icon-data".into(),
<OwnedValue as TryFrom<Value>>::try_from(other.icon_data.clone().into())
.expect("unreachable: Vec<u8> to OwnedValue"),
);
}
}
if self.shortcut != other.shortcut {
if other.shortcut == default.shortcut {
removed_props.push("shortcut".into());
} else {
updated_props.insert(
"shortcut".into(),
<OwnedValue as TryFrom<Value>>::try_from(other.shortcut.clone().into())
.expect("unreachable: Vec<Vec<u8>> to OwnedValue"),
);
}
}
if self.toggle_type != other.toggle_type {
if other.toggle_type == default.toggle_type {
removed_props.push("toggle-type".into());
} else {
updated_props.insert(
"toggle-type".into(),
<OwnedValue as From<Str>>::from(other.toggle_type.to_string().into()),
);
}
}
if self.toggle_state != other.toggle_state {
if other.toggle_state == default.toggle_state {
removed_props.push("toggle-state".into());
} else {
updated_props.insert(
"toggle-state".into(),
OwnedValue::from(other.toggle_state as i32),
);
}
}
if self.disposition != other.disposition {
if other.disposition == default.disposition {
removed_props.push("disposition".into());
} else {
updated_props.insert(
"disposition".into(),
<OwnedValue as From<Str>>::from(other.disposition.to_string().into()),
);
}
}
if updated_props.is_empty() && removed_props.is_empty() {
None
} else {
Some((updated_props, removed_props))
}
}
}
impl<T> Default for RawMenuItem<T> {
fn default() -> Self {
RawMenuItem {
r#type: ItemType::Standard,
label: String::default(),
enabled: true,
visible: true,
icon_name: String::default(),
icon_data: Vec::default(),
shortcut: Vec::default(),
toggle_type: ToggleType::Null,
toggle_state: ToggleState::Indeterminate,
disposition: Disposition::Normal,
on_clicked: Box::new(|_this: &mut T, _id| Default::default()),
}
}
}
#[allow(dead_code)]
#[derive(Debug, Eq, PartialEq, Clone)]
enum ItemType {
Standard,
Separator,
}
impl fmt::Display for ItemType {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
use ItemType::*;
match self {
Standard => f.write_str("standard"),
Separator => f.write_str("separator"),
}
}
}
impl From<ItemType> for OwnedValue {
fn from(value: ItemType) -> Self {
use ItemType::*;
let s = match value {
Standard => "standard",
Separator => "separator",
};
Str::from_static(s).into()
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
enum ToggleType {
Checkmark,
Radio,
Null,
}
impl fmt::Display for ToggleType {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
use ToggleType::*;
let r = match self {
Checkmark => "checkmark",
Radio => "radio",
Null => "",
};
f.write_str(r)
}
}
impl From<ToggleType> for OwnedValue {
fn from(value: ToggleType) -> Self {
use ToggleType::*;
let s = match value {
Checkmark => "checkmark",
Radio => "radio",
Null => "",
};
Str::from_static(s).into()
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
enum ToggleState {
Off = 0,
On = 1,
Indeterminate = -1,
}
impl From<ToggleState> for OwnedValue {
fn from(value: ToggleState) -> Self {
(value as i32).into()
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Disposition {
Normal,
Informative,
Warning,
Alert,
}
impl fmt::Display for Disposition {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
use Disposition::*;
let r = match self {
Normal => "normal",
Informative => "informative",
Warning => "warning",
Alert => "alert",
};
f.write_str(r)
}
}
impl From<Disposition> for OwnedValue {
fn from(value: Disposition) -> Self {
use Disposition::*;
let s = match value {
Normal => "normal",
Informative => "informative",
Warning => "warning",
Alert => "alert",
};
Str::from_static(s).into()
}
}
pub(crate) fn menu_flatten<T: 'static>(
items: Vec<MenuItem<T>>,
) -> Vec<(RawMenuItem<T>, Vec<usize>)> {
let mut list: Vec<(RawMenuItem<T>, Vec<usize>)> =
vec![(RawMenuItem::default(), Vec::with_capacity(items.len()))];
let mut stack = vec![(items, 0)];
while let Some((mut current_menu, parent_index)) = stack.pop() {
while !current_menu.is_empty() {
match current_menu.remove(0) {
MenuItem::Standard(item) => {
let index = list.len();
list.push((item.into(), Vec::new()));
list[parent_index].1.push(index);
}
MenuItem::Separator => {
let item = RawMenuItem {
r#type: ItemType::Separator,
..Default::default()
};
let index = list.len();
list.push((item, Vec::new()));
list[parent_index].1.push(index);
}
MenuItem::Checkmark(item) => {
let index = list.len();
list.push((item.into(), Vec::new()));
list[parent_index].1.push(index);
}
MenuItem::SubMenu(mut item) => {
let submenu = std::mem::replace(&mut item.submenu, Default::default());
let index = list.len();
list.push((item.into(), Vec::with_capacity(submenu.len())));
list[parent_index].1.push(index);
if !submenu.is_empty() {
stack.push((current_menu, parent_index));
stack.push((submenu, index));
break;
}
}
MenuItem::RadioGroup(group) => {
let offset = list.len();
let on_selected = Arc::new(Mutex::new(group.select));
for (idx, option) in group.options.into_iter().enumerate() {
let on_selected = on_selected.clone();
let item = RawMenuItem {
r#type: ItemType::Standard,
label: option.label,
enabled: option.enabled,
visible: option.visible,
icon_name: option.icon_name,
icon_data: option.icon_data,
shortcut: option.shortcut,
toggle_type: ToggleType::Radio,
toggle_state: if idx == group.selected {
ToggleState::On
} else {
ToggleState::Off
},
disposition: option.disposition,
on_clicked: Box::new(move |this: &mut T, id| {
(on_selected.lock().unwrap())(this, id - offset);
}),
..Default::default()
};
let index = list.len();
list.push((item, Vec::new()));
list[parent_index].1.push(index);
}
}
}
}
}
list
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_enums() {
assert_eq!(TextDirection::LeftToRight.to_string(), "ltr");
assert_eq!(TextDirection::RightToLeft.to_string(), "rtl");
}
#[test]
fn test_menu_flatten() {
let x: Vec<MenuItem<()>> = vec![
SubMenu {
label: "a".into(),
submenu: vec![
SubMenu {
label: "a1".into(),
submenu: vec![StandardItem {
label: "a1.1".into(),
..Default::default()
}
.into()],
..Default::default()
}
.into(),
StandardItem {
label: "a2".into(),
..Default::default()
}
.into(),
],
..Default::default()
}
.into(),
StandardItem {
label: "b".into(),
..Default::default()
}
.into(),
SubMenu {
label: "c".into(),
submenu: vec![
StandardItem {
label: "c1".into(),
..Default::default()
}
.into(),
SubMenu {
label: "c2".into(),
submenu: vec![StandardItem {
label: "c2.1".into(),
..Default::default()
}
.into()],
..Default::default()
}
.into(),
],
..Default::default()
}
.into(),
];
let r = menu_flatten(x);
let expect: Vec<(RawMenuItem<()>, Vec<usize>)> = vec![
(
RawMenuItem {
label: "".into(),
..Default::default()
},
vec![1, 5, 6],
),
(
RawMenuItem {
label: "a".into(),
..Default::default()
},
vec![2, 4],
),
(
RawMenuItem {
label: "a1".into(),
..Default::default()
},
vec![3],
),
(
RawMenuItem {
label: "a1.1".into(),
..Default::default()
},
vec![],
),
(
RawMenuItem {
label: "a2".into(),
..Default::default()
},
vec![],
),
(
RawMenuItem {
label: "b".into(),
..Default::default()
},
vec![],
),
(
RawMenuItem {
label: "c".into(),
..Default::default()
},
vec![7, 8],
),
(
RawMenuItem {
label: "c1".into(),
..Default::default()
},
vec![],
),
(
RawMenuItem {
label: "c2".into(),
..Default::default()
},
vec![9],
),
(
RawMenuItem {
label: "c2.1".into(),
..Default::default()
},
vec![],
),
];
assert_eq!(r.len(), 10);
assert_eq!(r[0].1, expect[0].1);
assert_eq!(r[1].1, expect[1].1);
assert_eq!(r[2].1, expect[2].1);
assert_eq!(r[3].1, expect[3].1);
assert_eq!(r[4].1, expect[4].1);
assert_eq!(r[5].1, expect[5].1);
assert_eq!(r[6].1, expect[6].1);
assert_eq!(r[7].1, expect[7].1);
assert_eq!(r[8].1, expect[8].1);
assert_eq!(r[9].1, expect[9].1);
assert_eq!(r[0].0.label, expect[0].0.label);
assert_eq!(r[1].0.label, expect[1].0.label);
assert_eq!(r[2].0.label, expect[2].0.label);
assert_eq!(r[3].0.label, expect[3].0.label);
assert_eq!(r[4].0.label, expect[4].0.label);
assert_eq!(r[5].0.label, expect[5].0.label);
assert_eq!(r[6].0.label, expect[6].0.label);
assert_eq!(r[7].0.label, expect[7].0.label);
assert_eq!(r[8].0.label, expect[8].0.label);
assert_eq!(r[9].0.label, expect[9].0.label);
}
}