1#[derive(Debug, Clone, PartialEq, Eq)]
4pub enum ChromeContextMenuItemType {
5 Command,
6 Check,
7 Radio,
8 Separator,
9 ButtonItem,
10 Submenu,
11 ActionableSubmenu,
12 Highlighted,
13 Title,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct ChromeContextMenuIcon {
18 pub png_bytes: Vec<u8>,
19 pub width: u32,
20 pub height: u32,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct ChromeContextMenuAccelerator {
25 pub key_equivalent: String,
26 pub modifier_mask: u32,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct ChromeContextMenuItem {
31 pub r#type: ChromeContextMenuItemType,
32 pub command_id: i32,
33 pub label: String,
34 pub secondary_label: String,
35 pub minor_text: String,
36 pub accessible_name: String,
37 pub enabled: bool,
38 pub visible: bool,
39 pub checked: bool,
40 pub group_id: i32,
41 pub is_new_feature: bool,
42 pub is_alerted: bool,
43 pub may_have_mnemonics: bool,
44 pub accelerator: Option<ChromeContextMenuAccelerator>,
45 pub icon: Option<ChromeContextMenuIcon>,
46 pub minor_icon: Option<ChromeContextMenuIcon>,
47 pub submenu: Vec<ChromeContextMenuItem>,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct ChromeContextMenu {
52 pub menu_id: u64,
53 pub x: i32,
54 pub y: i32,
55 pub source_type: u32,
56 pub items: Vec<ChromeContextMenuItem>,
57}
58
59impl From<ChromeContextMenuItemType> for cbf::data::context_menu::ContextMenuItemType {
60 fn from(value: ChromeContextMenuItemType) -> Self {
61 match value {
62 ChromeContextMenuItemType::Command => Self::Command,
63 ChromeContextMenuItemType::Check => Self::Check,
64 ChromeContextMenuItemType::Radio => Self::Radio,
65 ChromeContextMenuItemType::Separator => Self::Separator,
66 ChromeContextMenuItemType::ButtonItem => Self::ButtonItem,
67 ChromeContextMenuItemType::Submenu => Self::Submenu,
68 ChromeContextMenuItemType::ActionableSubmenu => Self::ActionableSubmenu,
69 ChromeContextMenuItemType::Highlighted => Self::Highlighted,
70 ChromeContextMenuItemType::Title => Self::Title,
71 }
72 }
73}
74
75impl From<ChromeContextMenuIcon> for cbf::data::context_menu::ContextMenuIcon {
76 fn from(value: ChromeContextMenuIcon) -> Self {
77 Self {
78 png_bytes: value.png_bytes,
79 width: value.width,
80 height: value.height,
81 }
82 }
83}
84
85impl From<ChromeContextMenuAccelerator> for cbf::data::context_menu::ContextMenuAccelerator {
86 fn from(value: ChromeContextMenuAccelerator) -> Self {
87 Self {
88 key_equivalent: value.key_equivalent,
89 modifier_mask: value.modifier_mask,
90 }
91 }
92}
93
94impl From<ChromeContextMenuItem> for cbf::data::context_menu::ContextMenuItem {
95 fn from(value: ChromeContextMenuItem) -> Self {
96 Self {
97 r#type: value.r#type.into(),
98 command_id: value.command_id,
99 label: value.label,
100 secondary_label: value.secondary_label,
101 minor_text: value.minor_text,
102 accessible_name: value.accessible_name,
103 enabled: value.enabled,
104 visible: value.visible,
105 checked: value.checked,
106 group_id: value.group_id,
107 is_new_feature: value.is_new_feature,
108 is_alerted: value.is_alerted,
109 may_have_mnemonics: value.may_have_mnemonics,
110 accelerator: value.accelerator.map(Into::into),
111 icon: value.icon.map(Into::into),
112 minor_icon: value.minor_icon.map(Into::into),
113 submenu: value.submenu.into_iter().map(Into::into).collect(),
114 }
115 }
116}
117
118impl From<ChromeContextMenu> for cbf::data::context_menu::ContextMenu {
119 fn from(value: ChromeContextMenu) -> Self {
120 Self {
121 menu_id: value.menu_id,
122 x: value.x,
123 y: value.y,
124 source_type: value.source_type,
125 items: value.items.into_iter().map(Into::into).collect(),
126 }
127 }
128}
129
130pub const CMD_BACK: i32 = 33000;
142pub const CMD_FORWARD: i32 = 33001;
144pub const CMD_RELOAD: i32 = 33002;
146pub const CMD_PRINT: i32 = 35003;
148pub const CMD_CUT: i32 = 36000;
150pub const CMD_COPY: i32 = 36001;
152pub const CMD_PASTE: i32 = 36003;
154pub const CMD_CONTENT_OPEN_LINK_NEW_TAB: i32 = 50100;
156pub const CMD_CONTENT_OPEN_LINK_NEW_WINDOW: i32 = 50101;
158pub const CMD_CONTENT_COPY_LINK_LOCATION: i32 = 50104;
160pub const CMD_CONTENT_SAVE_IMAGE_AS: i32 = 50120;
162pub const CMD_CONTENT_COPY_IMAGE_LOCATION: i32 = 50121;
164pub const CMD_CONTENT_COPY_IMAGE: i32 = 50122;
166pub const CMD_CONTENT_COPY: i32 = 50150;
168pub const CMD_CONTENT_CUT: i32 = 50151;
170pub const CMD_CONTENT_PASTE: i32 = 50152;
172pub const CMD_CONTENT_UNDO: i32 = 50154;
174pub const CMD_CONTENT_REDO: i32 = 50155;
176pub const CMD_CONTENT_SELECT_ALL: i32 = 50156;
178pub const CMD_CONTENT_PASTE_AND_MATCH_STYLE: i32 = 50157;
180pub const CMD_CONTENT_INSPECT_ELEMENT: i32 = 50162;
182
183const CONTEXT_MENU_ALLOWLIST: &[i32] = &[
184 CMD_BACK,
185 CMD_FORWARD,
186 CMD_RELOAD,
187 CMD_PRINT,
188 CMD_CUT,
189 CMD_COPY,
190 CMD_PASTE,
191 CMD_CONTENT_OPEN_LINK_NEW_TAB,
192 CMD_CONTENT_OPEN_LINK_NEW_WINDOW,
193 CMD_CONTENT_COPY_LINK_LOCATION,
194 CMD_CONTENT_SAVE_IMAGE_AS,
195 CMD_CONTENT_COPY_IMAGE_LOCATION,
196 CMD_CONTENT_COPY_IMAGE,
197 CMD_CONTENT_COPY,
198 CMD_CONTENT_CUT,
199 CMD_CONTENT_PASTE,
200 CMD_CONTENT_UNDO,
201 CMD_CONTENT_REDO,
202 CMD_CONTENT_SELECT_ALL,
203 CMD_CONTENT_PASTE_AND_MATCH_STYLE,
204 CMD_CONTENT_INSPECT_ELEMENT,
205];
206
207pub fn filter_supported(menu: ChromeContextMenu) -> ChromeContextMenu {
208 let items = filter_items(menu.items);
209 ChromeContextMenu { items, ..menu }
210}
211
212pub fn is_open_link_new_tab(command_id: i32) -> bool {
214 command_id == CMD_CONTENT_OPEN_LINK_NEW_TAB
215}
216
217pub fn is_open_link_new_window(command_id: i32) -> bool {
219 command_id == CMD_CONTENT_OPEN_LINK_NEW_WINDOW
220}
221
222fn filter_items(items: Vec<ChromeContextMenuItem>) -> Vec<ChromeContextMenuItem> {
223 let mut filtered = Vec::new();
224
225 for mut item in items {
226 match item.r#type {
227 ChromeContextMenuItemType::Separator => filtered.push(item),
228 ChromeContextMenuItemType::Submenu | ChromeContextMenuItemType::ActionableSubmenu => {
229 let submenu = filter_items(item.submenu);
230 if submenu.is_empty() {
231 continue;
232 }
233 item.submenu = submenu;
234 filtered.push(item);
235 }
236 _ => {
237 if CONTEXT_MENU_ALLOWLIST.contains(&item.command_id) {
238 filtered.push(item);
239 }
240 }
241 }
242 }
243
244 trim_menu_separators(filtered)
245}
246
247fn trim_menu_separators(items: Vec<ChromeContextMenuItem>) -> Vec<ChromeContextMenuItem> {
248 let mut trimmed = Vec::new();
249 let mut last_was_separator = true;
250
251 for item in items {
252 if item.r#type == ChromeContextMenuItemType::Separator {
253 if last_was_separator {
254 continue;
255 }
256 last_was_separator = true;
257 trimmed.push(item);
258 } else {
259 last_was_separator = false;
260 trimmed.push(item);
261 }
262 }
263
264 if matches!(trimmed.last(), Some(item) if item.r#type == ChromeContextMenuItemType::Separator) {
265 trimmed.pop();
266 }
267
268 trimmed
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn filter_supported_keeps_save_image_as_item() {
277 let menu = ChromeContextMenu {
278 menu_id: 1,
279 x: 0,
280 y: 0,
281 source_type: 0,
282 items: vec![ChromeContextMenuItem {
283 r#type: ChromeContextMenuItemType::Command,
284 command_id: CMD_CONTENT_SAVE_IMAGE_AS,
285 label: String::new(),
286 secondary_label: String::new(),
287 minor_text: String::new(),
288 accessible_name: String::new(),
289 enabled: true,
290 visible: true,
291 checked: false,
292 group_id: 0,
293 is_new_feature: false,
294 is_alerted: false,
295 may_have_mnemonics: false,
296 accelerator: None,
297 icon: None,
298 minor_icon: None,
299 submenu: Vec::new(),
300 }],
301 };
302
303 let filtered = filter_supported(menu);
304
305 assert_eq!(filtered.items.len(), 1);
306 assert_eq!(filtered.items[0].command_id, CMD_CONTENT_SAVE_IMAGE_AS);
307 }
308}