Skip to main content

cbf_chrome/data/
context_menu.rs

1//! Chrome-specific context menu item structures, including types, icons, accelerators, and submenus.
2
3#[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
130/// Chromium-derived command ids mirrored from `chrome/app/chrome_command_ids.h`.
131/// Keep these values aligned with the Chromium revision used by CBF because the
132/// backend reports and accepts the same command id space.
133///
134/// Current upstream reference:
135/// - `IDC_BACK`, `IDC_FORWARD`, `IDC_RELOAD`
136/// - `IDC_PRINT`
137/// - `IDC_CONTENT_CONTEXT_*`
138///   in `chromium/src/chrome/app/chrome_command_ids.h`
139///
140/// Command id for navigating back.
141pub const CMD_BACK: i32 = 33000;
142/// Command id for navigating forward.
143pub const CMD_FORWARD: i32 = 33001;
144/// Command id for reloading the page.
145pub const CMD_RELOAD: i32 = 33002;
146/// Command id for printing the page.
147pub const CMD_PRINT: i32 = 35003;
148/// Command id for cutting selection.
149pub const CMD_CUT: i32 = 36000;
150/// Command id for copying selection.
151pub const CMD_COPY: i32 = 36001;
152/// Command id for pasting clipboard contents.
153pub const CMD_PASTE: i32 = 36003;
154/// Command id for opening a link in a new tab.
155pub const CMD_CONTENT_OPEN_LINK_NEW_TAB: i32 = 50100;
156/// Command id for opening a link in a new window.
157pub const CMD_CONTENT_OPEN_LINK_NEW_WINDOW: i32 = 50101;
158/// Command id for copying a link location.
159pub const CMD_CONTENT_COPY_LINK_LOCATION: i32 = 50104;
160/// Command id for saving an image as a file.
161pub const CMD_CONTENT_SAVE_IMAGE_AS: i32 = 50120;
162/// Command id for copying an image location.
163pub const CMD_CONTENT_COPY_IMAGE_LOCATION: i32 = 50121;
164/// Command id for copying an image.
165pub const CMD_CONTENT_COPY_IMAGE: i32 = 50122;
166/// Command id for copying selected content.
167pub const CMD_CONTENT_COPY: i32 = 50150;
168/// Command id for cutting selected content.
169pub const CMD_CONTENT_CUT: i32 = 50151;
170/// Command id for pasting into content.
171pub const CMD_CONTENT_PASTE: i32 = 50152;
172/// Command id for undo in editable content.
173pub const CMD_CONTENT_UNDO: i32 = 50154;
174/// Command id for redo in editable content.
175pub const CMD_CONTENT_REDO: i32 = 50155;
176/// Command id for selecting all content.
177pub const CMD_CONTENT_SELECT_ALL: i32 = 50156;
178/// Command id for pasting while matching style.
179pub const CMD_CONTENT_PASTE_AND_MATCH_STYLE: i32 = 50157;
180/// Command id for inspecting element via DevTools.
181pub 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
212/// Check whether a command id represents "open link in new tab".
213pub fn is_open_link_new_tab(command_id: i32) -> bool {
214    command_id == CMD_CONTENT_OPEN_LINK_NEW_TAB
215}
216
217/// Check whether a command id represents "open link in new window".
218pub 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}