use std::ops::Deref;
use std::sync::atomic::{self, AtomicI32};
use serde::{Deserialize, Serialize};
use zbus::zvariant::{self, OwnedValue, Type, Value, as_value::optional};
use zbus::{interface, object_server::SignalEmitter};
pub mod event_types;
pub use event_types::*;
#[derive(Type, Debug, Serialize, Deserialize, Default, OwnedValue, Value, Clone)]
#[zvariant(signature = "dict")]
pub struct MenuProperty {
#[serde(with = "optional", skip_serializing_if = "Option::is_none", default)]
pub label: Option<String>,
#[serde(with = "optional", skip_serializing_if = "Option::is_none", default)]
#[zvariant(rename = "icon-name")]
pub icon_name: Option<String>,
#[serde(with = "optional", skip_serializing_if = "Option::is_none", default)]
pub enabled: Option<bool>,
#[serde(with = "optional", skip_serializing_if = "Option::is_none", default)]
#[zvariant(rename = "toggle-type")]
pub toggle_type: Option<ToggleType>,
#[serde(with = "optional", skip_serializing_if = "Option::is_none", default)]
#[zvariant(rename = "toggle-state")]
pub toggle_state: Option<ToggleState>,
#[serde(with = "optional", skip_serializing_if = "Option::is_none", default)]
#[zvariant(rename = "children-display")]
pub children_display: Option<String>,
}
impl MenuProperty {
pub const LABEL: &str = "label";
pub const ICON_NAME: &str = "icon-name";
pub const ENABLED: &str = "enabled";
pub const TOGGLE_TYPE: &str = "toggle-type";
pub const TOGGLE_STATE: &str = "toggle-state";
pub const CHILDREN_DISPLAY: &str = "children-display";
}
impl MenuProperty {
pub fn root() -> Self {
MenuProperty {
label: Some("root".to_owned()),
children_display: Some("submenu".to_owned()),
..Default::default()
}
}
pub fn submenu(label: String) -> Self {
MenuProperty {
label: Some(label),
children_display: Some("submenu".to_owned()),
..Default::default()
}
}
}
#[derive(
Type, Debug, Serialize, Deserialize, OwnedValue, Value, Clone, Copy, PartialEq, PartialOrd,
)]
pub struct Id(i32);
static COUNT: AtomicI32 = AtomicI32::new(1);
impl Id {
pub const MAIN: Self = Id(0);
pub fn unique() -> Id {
Id(COUNT.fetch_add(1, atomic::Ordering::Relaxed))
}
}
impl Deref for Id {
type Target = i32;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[allow(unused)]
#[derive(Debug, Clone)]
pub enum MenuType {
Root,
SubMenu,
Button,
RadioGroup,
}
#[derive(Debug, Clone)]
pub enum MenuUnit<Message: Clone> {
StandardButton {
id: Id,
options: ButtonOptions,
message: Message,
},
Root {
sub_menus: Vec<MenuUnit<Message>>,
},
SubMenu {
id: Id,
label: String,
sub_menus: Vec<MenuUnit<Message>>,
},
RadioGroup {
selections: Vec<MenuUnit<Message>>,
},
RadioButton {
id: Id,
options: RadioOptions,
message: Message,
},
}
#[derive(Debug, Clone)]
pub struct MenuTree<Message: Clone>(MenuUnit<Message>);
impl<Message: Clone> Default for MenuTree<Message> {
fn default() -> Self {
Self::new()
}
}
impl<Message: Clone> MenuTree<Message> {
pub fn new() -> Self {
Self(MenuUnit::root())
}
pub fn push_sub_menu(mut self, menu: MenuUnit<Message>) -> Self {
self.0 = self.0.push_sub_menu(menu);
self
}
fn get_unit_mut(&mut self) -> &mut MenuUnit<Message> {
&mut self.0
}
pub fn get_unit(&self) -> &MenuUnit<Message> {
&self.0
}
}
impl<Message: Clone> From<&MenuTree<Message>> for MenuItem {
fn from(value: &MenuTree<Message>) -> Self {
(&value.0).into()
}
}
#[derive(Debug, Clone, Default)]
pub struct ButtonOptions {
pub label: String,
pub enabled: bool,
pub icon_name: String,
}
#[derive(Debug, Clone, Default)]
pub struct RadioOptions {
pub label: String,
pub enabled: bool,
pub icon_name: String,
pub toggle_type: ToggleType,
pub toggle_state: ToggleState,
}
#[derive(Debug, Clone)]
pub struct RadioInitOption<Message: Clone> {
pub options: RadioOptions,
pub message: Message,
}
impl<Message: Clone> From<&MenuUnit<Message>> for MenuItem {
fn from(value: &MenuUnit<Message>) -> Self {
let IdOrGroup::Id(id) = value.id_or_ids() else {
panic!("RadioGroup should not be here");
};
let mut output = MenuItem {
id,
property: value.get_property().expect("RadioGroup should not be here"),
sub_menus: vec![],
};
let Some(sub_menus) = value.sub_menus() else {
return output;
};
for sub_menu in sub_menus {
if let MenuUnit::RadioGroup { selections, .. } = sub_menu {
for selection in selections {
output = output.push_sub_menu(selection.into());
}
} else {
output = output.push_sub_menu(sub_menu.into());
}
}
output
}
}
enum IdOrGroup {
Id(Id),
Groups(Vec<Id>),
}
impl IdOrGroup {
fn contains_id(&self, id: i32) -> bool {
let id = Id(id);
match self {
IdOrGroup::Id(oid) => *oid == id,
IdOrGroup::Groups(ids) => ids.contains(&id),
}
}
}
impl<Message: Clone> MenuUnit<Message> {
pub fn try_change_label(&mut self, new_label: String) {
match self {
Self::RadioButton {
options: RadioOptions { label, .. },
..
}
| Self::SubMenu { label, .. }
| Self::StandardButton {
options: ButtonOptions { label, .. },
..
} => {
*label = new_label;
}
_ => {}
}
}
fn message(&self, id: i32) -> Option<Message> {
let fid = Id(id);
match self {
Self::RadioGroup { selections } => {
for selection in selections {
let IdOrGroup::Id(oid) = selection.id_or_ids() else {
continue;
};
if oid == fid {
return selection.message(id);
}
}
}
Self::RadioButton { id, message, .. } | Self::StandardButton { id, message, .. } => {
if fid == *id {
return Some(message.clone());
}
}
_ => {}
}
None
}
fn get_property(&self) -> Option<MenuProperty> {
match self {
Self::Root { .. } => Some(MenuProperty::root()),
Self::SubMenu { label, .. } => Some(MenuProperty::submenu(label.clone())),
Self::StandardButton {
options:
ButtonOptions {
label,
enabled,
icon_name,
},
..
} => Some(MenuProperty {
label: Some(label.clone()),
icon_name: Some(icon_name.clone()),
enabled: Some(*enabled),
..Default::default()
}),
Self::RadioButton {
options:
RadioOptions {
label,
enabled,
icon_name,
toggle_state,
toggle_type,
},
..
} => Some(MenuProperty {
label: Some(label.clone()),
icon_name: Some(icon_name.clone()),
enabled: Some(*enabled),
toggle_type: Some(*toggle_type),
toggle_state: Some(*toggle_state),
..Default::default()
}),
Self::RadioGroup { .. } => None,
}
}
pub fn sub_menus(&self) -> Option<&Vec<Self>> {
match self {
Self::Root { sub_menus } | Self::SubMenu { sub_menus, .. } => Some(sub_menus),
_ => None,
}
}
pub fn sub_menus_mut(&mut self) -> Option<&mut Vec<Self>> {
match self {
Self::Root { sub_menus } | Self::SubMenu { sub_menus, .. } => Some(sub_menus),
_ => None,
}
}
fn id_or_ids(&self) -> IdOrGroup {
match self {
Self::Root { .. } => IdOrGroup::Id(Id::MAIN),
Self::SubMenu { id, .. }
| Self::StandardButton { id, .. }
| Self::RadioButton { id, .. } => IdOrGroup::Id(*id),
Self::RadioGroup { selections, .. } => {
let mut groups = vec![];
for selection in selections {
match selection.id_or_ids() {
IdOrGroup::Id(id) => groups.push(id),
IdOrGroup::Groups(mut ids) => {
groups.append(&mut ids);
}
}
}
IdOrGroup::Groups(groups)
}
}
}
pub fn button(options: ButtonOptions, message: Message) -> Self {
Self::StandardButton {
id: Id::unique(),
options,
message,
}
}
pub fn root() -> Self {
Self::Root { sub_menus: vec![] }
}
pub fn sub_menu(label: String) -> Self {
Self::SubMenu {
id: Id::unique(),
label,
sub_menus: vec![],
}
}
pub fn toggle_group(init_options: Vec<RadioInitOption<Message>>) -> Self {
let selections = init_options
.into_iter()
.map(
|RadioInitOption { options, message }| MenuUnit::RadioButton {
id: Id::unique(),
options,
message,
},
)
.collect();
Self::RadioGroup { selections }
}
pub fn push_sub_menu(mut self, menu: Self) -> Self {
let Some(sub_menus) = self.sub_menus_mut() else {
return self;
};
sub_menus.push(menu);
self
}
pub fn unit_type(&self) -> MenuType {
match self {
MenuUnit::Root { .. } => MenuType::Root,
MenuUnit::SubMenu { .. } => MenuType::SubMenu,
MenuUnit::RadioGroup { .. } => MenuType::RadioGroup,
MenuUnit::StandardButton { .. } | MenuUnit::RadioButton { .. } => MenuType::Button,
}
}
pub fn find_menu_by_id(&self, id: i32) -> Option<&Self> {
if self.id_or_ids().contains_id(id) {
return Some(self);
}
let sub_menus = self.sub_menus()?;
for menu in sub_menus {
if let Some(menu) = menu.find_menu_by_id(id) {
return Some(menu);
}
}
None
}
pub fn find_menu_by_id_mut(&mut self, id: i32) -> Option<&mut Self> {
if self.id_or_ids().contains_id(id) {
return Some(self);
}
let sub_menus = self.sub_menus_mut()?;
for menu in sub_menus {
if let Some(menu) = menu.find_menu_by_id_mut(id) {
return Some(menu);
}
}
None
}
fn find_menu_and_message_by_id_mut(&mut self, id: i32) -> Option<(&mut Self, Message)> {
if self.id_or_ids().contains_id(id) {
let message = self.message(id)?;
return Some((self, message));
}
let sub_menus = self.sub_menus_mut()?;
for menu in sub_menus {
if let Some(menu) = menu.find_menu_and_message_by_id_mut(id) {
return Some(menu);
}
}
None
}
}
#[derive(Type, Debug, Serialize, Deserialize, OwnedValue, Value, Clone)]
#[zvariant(signature = "(ia{sv}av)")]
pub struct MenuItem {
pub id: Id,
pub property: MenuProperty,
pub sub_menus: Vec<zvariant::OwnedValue>,
}
#[derive(Clone, PartialEq, Type, Serialize, Deserialize, OwnedValue, Value, Debug, Default)]
#[zvariant(signature = "s", rename_all = "lowercase")]
pub enum MenuStatus {
#[default]
Normal,
Notice,
Disabled,
}
impl Default for MenuItem {
fn default() -> Self {
MenuItem {
id: Id::unique(),
property: MenuProperty::root(),
sub_menus: vec![],
}
}
}
impl MenuItem {
pub fn new(property: MenuProperty) -> Self {
MenuItem {
id: Id::unique(),
property,
sub_menus: vec![],
}
}
#[allow(unused)]
#[allow(clippy::only_used_in_recursion)]
pub fn get_filiter(
&self,
parent_id: i32,
recursion_depth: i32,
property_names: &[&str],
) -> Option<MenuItem> {
if *self.id == parent_id {
let mut new_menu = MenuItem {
id: self.id,
property: self.property.clone(),
sub_menus: vec![],
};
let next_reversion_depth = recursion_depth - 1;
if next_reversion_depth != 0 {
for menu in self.sub_menus.as_slice() {
let menu: MenuItem = menu.clone().try_into().unwrap();
let next_menu = menu.filiter(next_reversion_depth, property_names);
new_menu = new_menu.push_sub_menu(next_menu);
}
}
return Some(new_menu);
}
None
}
#[allow(unused)]
#[allow(clippy::only_used_in_recursion)]
fn filiter(&self, recursion_depth: i32, property_names: &[&str]) -> MenuItem {
let mut new_menu = MenuItem {
id: self.id,
property: self.property.clone(),
sub_menus: vec![],
};
let next_reversion_depth = recursion_depth - 1;
for menu in self.sub_menus.as_slice() {
let menu: MenuItem = menu.clone().try_into().unwrap();
let next_menu = menu.filiter(next_reversion_depth, property_names);
new_menu = new_menu.push_sub_menu(next_menu);
}
new_menu
}
pub fn push_sub_menu(mut self, menu: MenuItem) -> Self {
self.sub_menus.push(OwnedValue::try_from(menu).unwrap());
self
}
pub fn get_property(&self, id: i32, name: String) -> Option<PropertyItem> {
if *self.id == id {
return Some(PropertyItem {
id,
item: self.property.clone(),
});
}
let sub_menus: Vec<MenuItem> = self
.sub_menus
.iter()
.map(|submenu| submenu.clone().try_into().unwrap())
.collect();
for sub_menu in sub_menus {
let property = sub_menu.get_property(id, name.clone());
if property.is_some() {
return property;
}
}
None
}
pub fn get_property_groups(
&self,
ids: Vec<i32>,
_property_names: Vec<String>,
) -> Vec<PropertyItem> {
let mut output = vec![];
for id in ids {
if let Some(property) = self.get_property(id, "".to_string()) {
output.push(property);
}
}
output
}
}
#[derive(Type, Debug, Default, Serialize, Deserialize)]
pub struct PropertyItem {
pub id: i32,
pub item: MenuProperty,
}
pub trait DBusMenuItem {
type State;
type Message: Clone;
fn boot(&self) -> Self::State;
fn menu(&self) -> MenuTree<Self::Message>;
fn revision(&self, state: &Self::State) -> u32;
#[allow(unused)]
fn about_to_show(&self, state: &mut Self::State, id: i32) -> zbus::fdo::Result<bool> {
Err(zbus::fdo::Error::Failed("Unimplemented".to_string()))
}
#[allow(unused)]
fn about_to_show_group(
&self,
state: &mut Self::State,
ids: Vec<i32>,
) -> zbus::fdo::Result<(Vec<i32>, Vec<i32>)> {
Err(zbus::fdo::Error::Failed("Unimplemented".to_string()))
}
fn status(&self, _state: &Self::State) -> zbus::fdo::Result<MenuStatus> {
Ok(MenuStatus::Normal)
}
#[allow(unused)]
fn on_clicked(
&self,
state: &mut Self::State,
button: &mut MenuUnit<Self::Message>,
message: Self::Message,
timestamp: u32,
) -> EventUpdate {
EventUpdate::None
}
#[allow(unused)]
fn text_direction(&self, state: &Self::State) -> TextDirection {
TextDirection::Inherit
}
#[allow(unused)]
fn icon_theme_path(&self, state: &Self::State) -> Vec<String> {
vec![]
}
}
pub struct DBusMenuInstance<State, Message>
where
Message: Clone,
{
pub(crate) program: Box<dyn DBusMenuItem<State = State, Message = Message> + Send + Sync>,
pub(crate) state: State,
pub(crate) menu_tree: MenuTree<Message>,
}
pub trait DBusMenuBootFn<State> {
fn boot(&self) -> State;
}
impl<T, State> DBusMenuBootFn<State> for T
where
T: Fn() -> State,
{
fn boot(&self) -> State {
self()
}
}
pub trait MenuBootFn<Message: Clone> {
fn menu(&self) -> MenuTree<Message>;
}
impl<Message> MenuBootFn<Message> for MenuTree<Message>
where
Message: Clone,
{
fn menu(&self) -> MenuTree<Message> {
self.clone()
}
}
impl<T, Message> MenuBootFn<Message> for T
where
Message: Clone,
T: Fn() -> MenuTree<Message>,
{
fn menu(&self) -> MenuTree<Message> {
self()
}
}
pub trait MenuStatusFn<State> {
fn status(&self, state: &State) -> MenuStatus;
}
impl<T, State> MenuStatusFn<State> for T
where
T: Fn(&State) -> MenuStatus,
{
fn status(&self, state: &State) -> MenuStatus {
self(state)
}
}
pub trait AboutToShowFn<State> {
fn about_to_show(&self, state: &mut State, id: i32) -> bool;
}
impl<T, State> AboutToShowFn<State> for T
where
T: Fn(&mut State, i32) -> bool,
{
fn about_to_show(&self, state: &mut State, id: i32) -> bool {
self(state, id)
}
}
#[derive(Debug)]
pub enum EventUpdate {
None,
UpdateCurrent,
UpdateAll,
}
pub trait RevisionFn<State> {
fn revision(&self, state: &State) -> u32;
}
impl<State, F> RevisionFn<State> for F
where
F: Fn(&State) -> u32,
{
fn revision(&self, state: &State) -> u32 {
self(state)
}
}
impl<State> RevisionFn<State> for u32 {
fn revision(&self, _state: &State) -> u32 {
*self
}
}
pub trait OnClickedFn<State, Message: Clone> {
fn on_clicked(
&self,
state: &mut State,
button: &mut MenuUnit<Message>,
message: Message,
timestamp: u32,
) -> EventUpdate;
}
impl<T, State, Message> OnClickedFn<State, Message> for T
where
T: Fn(&mut State, &mut MenuUnit<Message>, Message, u32) -> EventUpdate,
Message: Clone,
{
fn on_clicked(
&self,
state: &mut State,
button: &mut MenuUnit<Message>,
message: Message,
timestamp: u32,
) -> EventUpdate {
self(state, button, message, timestamp)
}
}
pub trait TextDirectionFn<State> {
fn text_direction(&self, state: &State) -> TextDirection;
}
impl<State> TextDirectionFn<State> for TextDirection {
fn text_direction(&self, _state: &State) -> TextDirection {
*self
}
}
impl<T, State> TextDirectionFn<State> for T
where
T: Fn(&State) -> TextDirection,
{
fn text_direction(&self, state: &State) -> TextDirection {
self(state)
}
}
pub trait IconThemePathFn<State> {
fn icon_theme_path(&self, state: &State) -> Vec<String>;
}
impl<State> IconThemePathFn<State> for Vec<String> {
fn icon_theme_path(&self, _state: &State) -> Vec<String> {
self.clone()
}
}
impl<T, State> IconThemePathFn<State> for T
where
T: Fn(&State) -> Vec<String>,
{
fn icon_theme_path(&self, state: &State) -> Vec<String> {
self(state)
}
}
pub trait AboutToShowGroupFn<State> {
fn about_to_show_group(
&self,
state: &mut State,
ids: Vec<i32>,
) -> zbus::fdo::Result<(Vec<i32>, Vec<i32>)>;
}
impl<T, State> AboutToShowGroupFn<State> for T
where
T: Fn(&mut State, Vec<i32>) -> zbus::fdo::Result<(Vec<i32>, Vec<i32>)>,
{
fn about_to_show_group(
&self,
state: &mut State,
ids: Vec<i32>,
) -> zbus::fdo::Result<(Vec<i32>, Vec<i32>)> {
self(state, ids)
}
}
#[interface(name = "com.canonical.dbusmenu")]
impl<State, Message> DBusMenuInstance<State, Message>
where
State: 'static + Send + Sync,
Message: 'static + Send + Sync + Clone,
{
fn about_to_show(&mut self, id: i32) -> zbus::fdo::Result<bool> {
self.program.about_to_show(&mut self.state, id)
}
fn about_to_show_group(&mut self, ids: Vec<i32>) -> zbus::fdo::Result<(Vec<i32>, Vec<i32>)> {
self.program.about_to_show_group(&mut self.state, ids)
}
fn get_layout(
&mut self,
parent_id: i32,
recursion_depth: i32,
property_names: Vec<String>,
) -> zbus::fdo::Result<(u32, MenuItem)> {
let property_names: Vec<&str> = property_names.iter().map(|name| name.as_str()).collect();
let menuitem: MenuItem = (&self.menu_tree).into();
Ok((
self.program.revision(&self.state),
menuitem
.get_filiter(parent_id, recursion_depth, &property_names)
.ok_or(zbus::fdo::Error::Failed("UnFounded".to_string()))?,
))
}
fn get_group_properties(
&mut self,
ids: Vec<i32>,
property_names: Vec<String>,
) -> zbus::fdo::Result<Vec<PropertyItem>> {
let menuitem: MenuItem = (&self.menu_tree).into();
Ok(menuitem.get_property_groups(ids, property_names))
}
fn get_property(&mut self, id: i32, name: String) -> zbus::fdo::Result<PropertyItem> {
let menuitem: MenuItem = (&self.menu_tree).into();
menuitem
.get_property(id, name)
.ok_or(zbus::fdo::Error::Failed("Unfounded".to_string()))
}
#[zbus(property)]
fn version(&self) -> u32 {
2
}
#[zbus(property)]
fn status(&self) -> zbus::fdo::Result<MenuStatus> {
self.program.status(&self.state)
}
async fn event(
&mut self,
id: i32,
event_id: String,
_data: zbus::zvariant::OwnedValue,
timestamp: u32,
#[zbus(signal_emitter)] cxts: SignalEmitter<'_>,
) -> zbus::fdo::Result<()> {
let menu = self.menu_tree.get_unit_mut();
let Some((button, message)) = menu.find_menu_and_message_by_id_mut(id) else {
return Ok(());
};
let need_update = match event_id.as_str() {
"clicked" => {
if !matches!(button.unit_type(), MenuType::Button | MenuType::RadioGroup) {
return Ok(());
}
self.program
.on_clicked(&mut self.state, button, message, timestamp)
}
_ => EventUpdate::None,
};
let revision = self.program.revision(&self.state);
match need_update {
EventUpdate::UpdateCurrent => {
let _ =
DBusMenuInstance::<State, Message>::layout_updated(&cxts, revision, id).await;
}
EventUpdate::UpdateAll => {
let _ =
DBusMenuInstance::<State, Message>::layout_updated(&cxts, revision, *Id::MAIN)
.await;
}
_ => {}
}
Ok(())
}
async fn event_group(
&mut self,
events: Vec<(i32, String, zbus::zvariant::OwnedValue, u32)>,
#[zbus(signal_emitter)] cxts: SignalEmitter<'_>,
) -> zbus::fdo::Result<Vec<i32>> {
let mut output = vec![];
let mut update_all = false;
let mut update_parents: Vec<i32> = vec![];
for (id, event_id, _data, timestamp) in events {
let menu = self.menu_tree.get_unit_mut();
let Some((button, message)) = menu.find_menu_and_message_by_id_mut(id) else {
continue;
};
let need_update = match event_id.as_str() {
"clicked" => {
if !matches!(button.unit_type(), MenuType::Button | MenuType::RadioGroup) {
continue;
}
self.program
.on_clicked(&mut self.state, button, message, timestamp)
}
_ => {
continue;
}
};
match need_update {
EventUpdate::None => {
continue;
}
EventUpdate::UpdateAll => {
update_all = true;
}
EventUpdate::UpdateCurrent => {
if let IdOrGroup::Id(id) = menu.id_or_ids() {
update_parents.push(*id);
}
}
};
output.push(id);
}
let revision = self.program.revision(&self.state);
if update_all {
let _ = DBusMenuInstance::<State, Message>::layout_updated(&cxts, revision, *Id::MAIN)
.await;
} else {
for id in update_parents {
let _ =
DBusMenuInstance::<State, Message>::layout_updated(&cxts, revision, id).await;
}
}
Ok(output)
}
#[zbus(property)]
fn text_direction(&self) -> TextDirection {
self.program.text_direction(&self.state)
}
#[zbus(property)]
fn icon_theme_path(&self) -> Vec<String> {
self.program.icon_theme_path(&self.state)
}
#[zbus(signal)]
pub async fn item_activation_requested(
ctxt: &SignalEmitter<'_>,
id: i32,
timestamp: u32,
) -> zbus::Result<()>;
#[zbus(signal)]
pub async fn items_properties_updated(
ctxt: &SignalEmitter<'_>,
updated_props: Vec<(i32, MenuProperty)>,
removed_props: Vec<(i32, Vec<&str>)>,
) -> zbus::Result<()>;
#[zbus(signal)]
pub async fn layout_updated(
ctxt: &SignalEmitter<'_>,
revision: u32,
parent: i32,
) -> zbus::Result<()>;
}