Skip to main content

argui_platform/
tray.rs

1use std::collections::HashSet;
2
3use crate::{IconSet, WindowKey};
4
5#[derive(Clone, Debug, Eq, Hash, PartialEq)]
6pub struct TrayItemId(String);
7
8impl TrayItemId {
9    #[must_use]
10    pub fn new(value: impl Into<String>) -> Self {
11        Self(value.into())
12    }
13
14    #[must_use]
15    pub fn as_str(&self) -> &str {
16        &self.0
17    }
18}
19
20#[derive(Clone, Debug, Eq, PartialEq)]
21pub enum TrayAction {
22    Custom(String),
23    ShowWindow(WindowKey),
24    HideWindow(WindowKey),
25    ToggleWindow(WindowKey),
26    FocusWindow(WindowKey),
27    CloseWindow(WindowKey),
28    Quit,
29}
30
31#[derive(Clone, Debug, PartialEq)]
32pub enum TrayMenuItem {
33    Action {
34        id: TrayItemId,
35        label: String,
36        enabled: bool,
37        action: TrayAction,
38    },
39    Check {
40        id: TrayItemId,
41        label: String,
42        enabled: bool,
43        checked: bool,
44        action: TrayAction,
45    },
46    Separator,
47    Submenu {
48        id: TrayItemId,
49        label: String,
50        enabled: bool,
51        items: Vec<TrayMenuItem>,
52    },
53}
54
55impl TrayMenuItem {
56    #[must_use]
57    pub fn id(&self) -> Option<&TrayItemId> {
58        match self {
59            Self::Action { id, .. } | Self::Check { id, .. } | Self::Submenu { id, .. } => Some(id),
60            Self::Separator => None,
61        }
62    }
63}
64
65#[derive(Clone, Debug, PartialEq)]
66pub struct TrayConfig {
67    pub tooltip: Option<String>,
68    pub title: Option<String>,
69    pub icon: Option<IconSet>,
70    pub menu: Vec<TrayMenuItem>,
71    pub visible: bool,
72    pub icon_is_template: bool,
73    pub show_menu_on_left_click: bool,
74}
75
76impl Default for TrayConfig {
77    fn default() -> Self {
78        Self {
79            tooltip: None,
80            title: None,
81            icon: None,
82            menu: Vec::new(),
83            visible: true,
84            icon_is_template: false,
85            show_menu_on_left_click: true,
86        }
87    }
88}
89
90impl TrayConfig {
91    pub fn validate(&self) -> Result<(), TrayConfigError> {
92        let mut ids = HashSet::new();
93        validate_items(&self.menu, &mut ids)
94    }
95}
96
97fn validate_items(
98    items: &[TrayMenuItem],
99    ids: &mut HashSet<TrayItemId>,
100) -> Result<(), TrayConfigError> {
101    for item in items {
102        let Some(id) = item.id() else {
103            continue;
104        };
105        if id.as_str().is_empty() {
106            return Err(TrayConfigError::EmptyItemId);
107        }
108        if !ids.insert(id.clone()) {
109            return Err(TrayConfigError::DuplicateItemId(id.clone()));
110        }
111        if let TrayMenuItem::Submenu { items, .. } = item {
112            validate_items(items, ids)?;
113        }
114    }
115    Ok(())
116}
117
118#[derive(Clone, Debug, Eq, PartialEq)]
119pub enum TrayConfigError {
120    EmptyItemId,
121    DuplicateItemId(TrayItemId),
122}
123
124impl std::fmt::Display for TrayConfigError {
125    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126        match self {
127            Self::EmptyItemId => formatter.write_str("tray item ids cannot be empty"),
128            Self::DuplicateItemId(id) => {
129                write!(formatter, "duplicate tray item id: {}", id.as_str())
130            }
131        }
132    }
133}
134
135impl std::error::Error for TrayConfigError {}
136
137#[derive(Clone, Copy, Debug, Eq, PartialEq)]
138pub enum TrayPointerButton {
139    Primary,
140    Secondary,
141    Middle,
142    Unknown,
143}
144
145#[derive(Clone, Debug, PartialEq)]
146pub enum TrayEvent {
147    Action {
148        id: TrayItemId,
149        action: TrayAction,
150    },
151    Click {
152        button: TrayPointerButton,
153        double: bool,
154    },
155}