From cf804dd70925118ddeb147cfb626db1b180ca0b3 Mon Sep 17 00:00:00 2001 From: Jason Tsai Date: Tue, 12 Nov 2024 21:05:24 +0800 Subject: [PATCH 01/17] feat(context-menu): create webview on wayland --- src/compositor.rs | 3 ++ src/context_menu.rs | 103 ++++++++++++++++++++++++++++++++++++++-- src/window.rs | 113 ++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 207 insertions(+), 12 deletions(-) diff --git a/src/compositor.rs b/src/compositor.rs index 2fc78bba..62a244f3 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -1112,6 +1112,9 @@ impl IOCompositor { if close_window { window_id = Some(window.id()); + } else { + // if the window is not closed, we need to update the display list + self.send_root_pipeline_display_list(window); } self.frame_tree_id.next(); diff --git a/src/context_menu.rs b/src/context_menu.rs index 968aa93b..3bb6947a 100644 --- a/src/context_menu.rs +++ b/src/context_menu.rs @@ -1,18 +1,28 @@ +use base::id::WebViewId; +use compositing_traits::ConstellationMsg; +use crossbeam_channel::Sender; +use euclid::{Point2D, Size2D}; +/* macOS, Windows Native Implementation */ #[cfg(any(target_os = "macos", target_os = "windows"))] use muda::{ContextMenu as MudaContextMenu, Menu}; #[cfg(any(target_os = "macos", target_os = "windows"))] use raw_window_handle::{HasWindowHandle, RawWindowHandle}; +use servo_url::ServoUrl; +/* Wayland Implementation */ +#[cfg(linux)] +use webrender_api::units::DeviceIntPoint; +use webrender_api::units::DeviceIntRect; +use winit::dpi::PhysicalPosition; + +use crate::{verso::send_to_constellation, webview::WebView, window::Window}; + /// Context Menu #[cfg(any(target_os = "macos", target_os = "windows"))] pub struct ContextMenu { menu: Menu, } -/// Context Menu -#[cfg(linux)] -pub struct ContextMenu {} - #[cfg(any(target_os = "macos", target_os = "windows"))] impl ContextMenu { /// Create context menu with custom items @@ -48,3 +58,88 @@ impl ContextMenu { } } } + +/// Context Menu +#[cfg(linux)] +#[derive(Debug, Clone)] +pub struct ContextMenu { + menu_items: Vec, + /// The webview that the context menu is attached to + pub webview: Option, +} + +#[cfg(linux)] +impl ContextMenu { + /// Create a dialog in the window. + /// + /// Often used by calling window.alert() or window.confirm() in the web page. + pub fn new_with_menu(menu_items: Vec) -> Self { + Self { + menu_items, + webview: None, + } + } + /// Set the context menu's options + pub fn set_menu_items(&mut self, menu_items: Vec) { + self.menu_items = menu_items; + } + /// Show the context menu on position + pub fn create_webview(&mut self, position: DeviceIntPoint) -> WebView { + // Translate position to origin + let origin = Point2D::new(position.x, position.y); + + // Calculate menu size + // Each menu item is 30px height + // Menu has 10px padding top and bottom + let menu_height = self.menu_items.len() as i32 * 30 + 20; + let menu_width = 200; + let size = Size2D::new(menu_width, menu_height); + let rect = DeviceIntRect::from_origin_and_size(origin, size); + + let webview_id = WebViewId::new(); + let webview = WebView::new(webview_id, rect); + // let url = ServoUrl::parse("https://example.com").unwrap(); + + self.webview = Some(webview.clone()); + + webview + } +} + +/// Menu Item +#[derive(Debug, Clone)] +pub struct MenuItem { + id: String, + label: String, + enabled: bool, +} + +impl MenuItem { + /// Create a new menu item + pub fn new(id: Option<&str>, label: &str, enabled: bool) -> Self { + let id = id.unwrap_or(label); + Self { + id: id.to_string(), + label: label.to_string(), + enabled, + } + } + /// Get the id of the menu item + pub fn id(&self) -> &str { + &self.id + } + /// Get the label of the menu item + pub fn label(&self) -> &str { + &self.label + } + /// Set the label of the menu item + pub fn set_label(&mut self, label: &str) -> &Self { + self.label = label.to_string(); + self + } + /// Enable or disable menu item + pub fn set_enabled(&mut self, enabled: bool) -> &Self { + self.enabled = enabled; + self + } +} diff --git a/src/window.rs b/src/window.rs index 20a5898d..1a4771b9 100644 --- a/src/window.rs +++ b/src/window.rs @@ -32,10 +32,9 @@ use winit::{ window::{CursorIcon, Window as WinitWindow, WindowAttributes, WindowId}, }; -#[cfg(any(target_os = "macos", target_os = "windows"))] -use crate::context_menu::ContextMenu; use crate::{ compositor::{IOCompositor, MouseWindowEvent}, + context_menu::{self, ContextMenu}, keyboard::keyboard_event_from_winit, rendering::{gl_config_picker, RenderingContext}, verso::send_to_constellation, @@ -58,11 +57,19 @@ pub struct Window { mouse_position: Cell>>, /// Modifiers state of the keyboard. modifiers_state: Cell, + /// Browser history of the window. history: Vec, + /// Current history index. current_history_index: usize, /// State to indicate if the window is resizing. pub(crate) resizing: bool, + /// dialog webviews + dialog_webviews: Vec, + + /// context_menu + context_menu: Option, + /// Global menu evnet receiver for muda crate #[cfg(any(target_os = "macos", target_os = "windows"))] menu_event_receiver: MenuEventReceiver, @@ -116,6 +123,8 @@ impl Window { history: vec![], current_history_index: 0, resizing: false, + dialog_webviews: vec![], + context_menu: None, #[cfg(any(target_os = "macos", target_os = "windows"))] menu_event_receiver: MenuEvent::receiver().clone(), }, @@ -157,6 +166,8 @@ impl Window { history: vec![], current_history_index: 0, resizing: false, + dialog_webviews: vec![], + context_menu: None, #[cfg(any(target_os = "macos", target_os = "windows"))] menu_event_receiver: MenuEvent::receiver().clone(), }; @@ -274,12 +285,45 @@ impl Window { WindowEvent::MouseInput { state, button, .. } => { // handle context menu // TODO: should create on ShowContextMenu event - #[cfg(any(target_os = "macos", target_os = "windows"))] + #[cfg(linux)] + if *button == winit::event::MouseButton::Left && *state == ElementState::Pressed { + if let Some(context_menu) = self.context_menu.take() { + dbg!("close context menu"); + if let Some(webview) = context_menu.webview { + dbg!("remove context menu"); + send_to_constellation( + sender, + ConstellationMsg::CloseWebView(webview.webview_id), + ); + return; + } + } + } if *button == winit::event::MouseButton::Right && *state == ElementState::Pressed { - self.show_context_menu(); - // FIXME: there's chance to lose the event since the channel is async. - if let Ok(event) = self.menu_event_receiver.try_recv() { - self.handle_context_menu_event(sender, event); + #[cfg(any(target_os = "macos", target_os = "windows"))] + { + self.show_context_menu(); + // FIXME: there's chance to lose the event since the channel is async. + if let Ok(event) = self.menu_event_receiver.try_recv() { + self.handle_context_menu_event(sender, event); + } + } + #[cfg(linux)] + { + if let Some(context_menu) = self.context_menu.take() { + dbg!("closing old context menu"); + if let Some(webview) = context_menu.webview { + dbg!("remove old context menu"); + send_to_constellation( + sender, + ConstellationMsg::CloseWebView(webview.webview_id), + ); + } + } + dbg!("open context menu"); + self.context_menu = Some(self.create_context_menu()); + self.show_context_menu(sender); + return; } } @@ -435,6 +479,16 @@ impl Window { self.window.scale_factor() } + /// Append a dialog webview to the window. + pub fn append_dialog_webview(&mut self, webview: &WebView) { + self.dialog_webviews.push(webview.clone()); + } + + /// Remove a dialog webview from the window. + pub fn remove_dialog_webview(&mut self, id: WebViewId) { + self.dialog_webviews.retain(|w| w.webview_id != id); + } + /// Check if the window has such webview. pub fn has_webview(&self, id: WebViewId) -> bool { self.panel @@ -450,6 +504,7 @@ impl Window { id: WebViewId, compositor: &mut IOCompositor, ) -> (Option, bool) { + dbg!("REMOVE WEBVIEW"); if self .panel .as_ref() @@ -470,6 +525,9 @@ impl Window { .is_some() { (self.webview.take(), self.panel.is_none()) + } else if let Some(index) = self.dialog_webviews.iter().position(|w| w.webview_id == id) { + let webview = self.dialog_webviews.remove(index); + (Some(webview), false) } else { (None, false) } @@ -477,6 +535,7 @@ impl Window { /// Get the painting order of this window. pub fn painting_order(&self) -> Vec<&WebView> { + dbg!(&self.dialog_webviews); let mut order = vec![]; if let Some(panel) = &self.panel { order.push(&panel.webview); @@ -484,6 +543,7 @@ impl Window { if let Some(webview) = &self.webview { order.push(webview); } + self.dialog_webviews.iter().for_each(|w| order.push(w)); order } @@ -537,8 +597,8 @@ impl Window { } // Dummy Context Menu -#[cfg(any(target_os = "macos", target_os = "windows"))] impl Window { + #[cfg(any(target_os = "macos", target_os = "windows"))] pub(crate) fn show_context_menu(&self) { let history_len = self.history.len(); @@ -558,6 +618,43 @@ impl Window { let context_menu = ContextMenu::new_with_menu(menu); context_menu.show(self.window.window_handle().unwrap()); } + + #[cfg(linux)] + pub(crate) fn create_context_menu(&mut self) -> ContextMenu { + use crate::context_menu::MenuItem; + let history_len = self.history.len(); + + // items + let back = MenuItem::new(Some("back"), "Back", self.current_history_index > 0); + let forward = MenuItem::new( + Some("forward"), + "Forward", + self.current_history_index + 1 < history_len, + ); + let reload = MenuItem::new(Some("reload"), "Reload", true); + + ContextMenu::new_with_menu([back, forward, reload].to_vec()) + } + + #[cfg(linux)] + pub(crate) fn show_context_menu(&mut self, sender: &Sender) { + if let Some(ref mut context_menu) = self.context_menu { + let mouse_position = self.mouse_position.get().unwrap(); + let position = Point2D::new(mouse_position.x as i32, mouse_position.y as i32); + + let url = ServoUrl::parse("https://example.com").unwrap(); + let menu_webview = context_menu.create_webview(position); + + send_to_constellation( + sender, + ConstellationMsg::NewWebView(url, menu_webview.webview_id), + ); + + self.append_dialog_webview(&menu_webview); + } + } + + #[cfg(any(target_os = "macos", target_os = "windows"))] fn handle_context_menu_event(&self, sender: &Sender, event: MenuEvent) { // TODO: should be more flexible to handle different menu items match event.id().0.as_str() { From ecf72f905afd36a1035c7143af38edf8c61b88de Mon Sep 17 00:00:00 2001 From: Jason Tsai Date: Wed, 13 Nov 2024 18:34:52 +0800 Subject: [PATCH 02/17] dynamic generate menu items on html --- Cargo.lock | 2 ++ Cargo.toml | 4 ++++ src/context_menu.rs | 27 ++++++++++++++++----------- src/window.rs | 11 ++++++----- 4 files changed, 28 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e2af6147..faa75f0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6701,6 +6701,8 @@ dependencies = [ "raw-window-handle", "script", "script_traits", + "serde", + "serde_json", "servo-media", "servo-media-dummy", "servo_config", diff --git a/Cargo.toml b/Cargo.toml index 50faf204..cc706ec5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -109,6 +109,10 @@ url = { workspace = true } headers = "0.3" versoview_messages = { path = "./versoview_messages" } +[target.'cfg(all(unix, not(apple), not(android)))'.dependencies] +serde_json = "1.0.132" +serde = { workspace = true } + [target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies] muda = "0.15" diff --git a/src/context_menu.rs b/src/context_menu.rs index 3bb6947a..097731ee 100644 --- a/src/context_menu.rs +++ b/src/context_menu.rs @@ -1,21 +1,21 @@ use base::id::WebViewId; -use compositing_traits::ConstellationMsg; -use crossbeam_channel::Sender; use euclid::{Point2D, Size2D}; + /* macOS, Windows Native Implementation */ #[cfg(any(target_os = "macos", target_os = "windows"))] use muda::{ContextMenu as MudaContextMenu, Menu}; #[cfg(any(target_os = "macos", target_os = "windows"))] use raw_window_handle::{HasWindowHandle, RawWindowHandle}; -use servo_url::ServoUrl; /* Wayland Implementation */ #[cfg(linux)] +use crate::webview::WebView; +#[cfg(linux)] +use serde::Serialize; +#[cfg(linux)] use webrender_api::units::DeviceIntPoint; +#[cfg(linux)] use webrender_api::units::DeviceIntRect; -use winit::dpi::PhysicalPosition; - -use crate::{verso::send_to_constellation, webview::WebView, window::Window}; /// Context Menu #[cfg(any(target_os = "macos", target_os = "windows"))] @@ -84,16 +84,16 @@ impl ContextMenu { self.menu_items = menu_items; } /// Show the context menu on position - pub fn create_webview(&mut self, position: DeviceIntPoint) -> WebView { + pub fn create_webview(&mut self, position: DeviceIntPoint, scale_factor: f64) -> WebView { // Translate position to origin let origin = Point2D::new(position.x, position.y); // Calculate menu size // Each menu item is 30px height // Menu has 10px padding top and bottom - let menu_height = self.menu_items.len() as i32 * 30 + 20; - let menu_width = 200; - let size = Size2D::new(menu_width, menu_height); + let menu_height = (self.menu_items.len() * 30 + 20) as f64 * scale_factor; + let menu_width = 200.0 * scale_factor; + let size = Size2D::new(menu_width as i32, menu_height as i32); let rect = DeviceIntRect::from_origin_and_size(origin, size); let webview_id = WebViewId::new(); @@ -104,10 +104,15 @@ impl ContextMenu { webview } + + /// get item json + pub fn get_items_json(&self) -> String { + serde_json::to_string(&self.menu_items).unwrap() + } } /// Menu Item -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct MenuItem { id: String, label: String, diff --git a/src/window.rs b/src/window.rs index 1a4771b9..c964c2a6 100644 --- a/src/window.rs +++ b/src/window.rs @@ -535,7 +535,6 @@ impl Window { /// Get the painting order of this window. pub fn painting_order(&self) -> Vec<&WebView> { - dbg!(&self.dialog_webviews); let mut order = vec![]; if let Some(panel) = &self.panel { order.push(&panel.webview); @@ -638,13 +637,15 @@ impl Window { #[cfg(linux)] pub(crate) fn show_context_menu(&mut self, sender: &Sender) { + let scale_factor = self.scale_factor(); + if let Some(ref mut context_menu) = self.context_menu { let mouse_position = self.mouse_position.get().unwrap(); let position = Point2D::new(mouse_position.x as i32, mouse_position.y as i32); - - let url = ServoUrl::parse("https://example.com").unwrap(); - let menu_webview = context_menu.create_webview(position); - + let items_json = context_menu.get_items_json(); + let url_str = format!("verso://context_menu.html?items={}", items_json); + let url = ServoUrl::parse(&url_str).unwrap(); + let menu_webview = context_menu.create_webview(position, scale_factor); send_to_constellation( sender, ConstellationMsg::NewWebView(url, menu_webview.webview_id), From 15b7cb6a27c07db2995d522730eb4f52ef020291 Mon Sep 17 00:00:00 2001 From: Jason Tsai Date: Wed, 13 Nov 2024 18:38:47 +0800 Subject: [PATCH 03/17] add context_menu.html --- resources/context_menu.html | 68 +++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 resources/context_menu.html diff --git a/resources/context_menu.html b/resources/context_menu.html new file mode 100644 index 00000000..41a3478e --- /dev/null +++ b/resources/context_menu.html @@ -0,0 +1,68 @@ + + + + + + + + + From 5213d8c5a051693cd9d86fc135d09d3c3e2a7e5d Mon Sep 17 00:00:00 2001 From: Jason Tsai Date: Wed, 13 Nov 2024 23:08:03 +0800 Subject: [PATCH 04/17] add check mouse hit on context-menu webview --- resources/context_menu.html | 8 ++++- src/compositor.rs | 16 ++++++++- src/context_menu.rs | 12 ++++++- src/webview.rs | 15 ++++++-- src/window.rs | 69 ++++++++++++++++++++++++++++--------- 5 files changed, 98 insertions(+), 22 deletions(-) diff --git a/resources/context_menu.html b/resources/context_menu.html index 41a3478e..32afd1f3 100644 --- a/resources/context_menu.html +++ b/resources/context_menu.html @@ -58,7 +58,13 @@ menuItem.classList.add('disabled'); } else { menuItem.onclick = () => { - console.log(`Clicked on ${id}:${label}`); + const msg = JSON.stringify({ + id, + close: true, + }); + console.log(`CONTEXT_MENU:${msg}`); + // FIXME: current using propmt will crash on verso's servo backend, IPCChannel seems disconnected before recv + // window.prompt(`CONTEXT_MENU:${msg}`); }; } diff --git a/src/compositor.rs b/src/compositor.rs index 62a244f3..2f05e268 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -1338,7 +1338,8 @@ impl IOCompositor { } } - fn hit_test_at_point(&self, point: DevicePoint) -> Option { + /// TODO: doc + pub fn hit_test_at_point(&self, point: DevicePoint) -> Option { return self .hit_test_at_point_with_flags_and_pipeline(point, HitTestFlags::empty(), None) .first() @@ -2175,6 +2176,19 @@ impl IOCompositor { self.webrender_api .send_transaction(self.webrender_document, transaction); } + + /// Get webview id by pipeline id. + pub fn webview_id_by_pipeline_id( + &self, + pipeline_id: PipelineId, + ) -> Option { + for (w_id, p_id) in &self.webviews { + if *p_id == pipeline_id { + return Some(*w_id); + } + } + None + } } #[derive(Debug, PartialEq)] diff --git a/src/context_menu.rs b/src/context_menu.rs index 097731ee..890a563a 100644 --- a/src/context_menu.rs +++ b/src/context_menu.rs @@ -11,7 +11,7 @@ use raw_window_handle::{HasWindowHandle, RawWindowHandle}; #[cfg(linux)] use crate::webview::WebView; #[cfg(linux)] -use serde::Serialize; +use serde::{Deserialize, Serialize}; #[cfg(linux)] use webrender_api::units::DeviceIntPoint; #[cfg(linux)] @@ -148,3 +148,13 @@ impl MenuItem { self } } + +/// Context Menu Click Result +#[cfg(linux)] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ContextMenuClickResult { + /// The id of the menu item + pub id: String, + /// Close the context menu + pub close: bool, +} diff --git a/src/webview.rs b/src/webview.rs index 096e112e..6adce193 100644 --- a/src/webview.rs +++ b/src/webview.rs @@ -12,7 +12,10 @@ use servo_url::ServoUrl; use url::Url; use webrender_api::units::DeviceIntRect; -use crate::{compositor::IOCompositor, verso::send_to_constellation, window::Window}; +use crate::{ + compositor::IOCompositor, context_menu::ContextMenuClickResult, verso::send_to_constellation, + window::Window, +}; /// A web view is an area to display web browsing context. It's what user will treat as a "web page". #[derive(Debug, Clone)] @@ -193,7 +196,15 @@ impl Window { if let Some(webview) = &self.webview { let id = webview.webview_id; - if msg.starts_with("NAVIGATE_TO:") { + if msg.starts_with("CONTEXT_MENU:") { + // FIXME: we can't get message from menu dialog now. App crashes before we get this message and it's because prompt_sender's channel is already closed. + let json_str_msg = msg.strip_prefix("CONTEXT_MENU:").unwrap(); + dbg!(json_str_msg); + let json = + serde_json::from_str::(json_str_msg) + .unwrap(); + dbg!(json); + } else if msg.starts_with("NAVIGATE_TO:") { let unparsed_url = msg.strip_prefix("NAVIGATE_TO:").unwrap(); let url = match Url::parse(unparsed_url) { Ok(url_parsed) => url_parsed, diff --git a/src/window.rs b/src/window.rs index c964c2a6..d91eb178 100644 --- a/src/window.rs +++ b/src/window.rs @@ -34,7 +34,7 @@ use winit::{ use crate::{ compositor::{IOCompositor, MouseWindowEvent}, - context_menu::{self, ContextMenu}, + context_menu::ContextMenu, keyboard::keyboard_event_from_winit, rendering::{gl_config_picker, RenderingContext}, verso::send_to_constellation, @@ -283,14 +283,29 @@ impl Window { } } WindowEvent::MouseInput { state, button, .. } => { - // handle context menu - // TODO: should create on ShowContextMenu event + let position = match self.mouse_position.get() { + Some(position) => Point2D::new(position.x as f32, position.y as f32), + None => { + log::trace!("Mouse position is None, skipping MouseInput event."); + return; + } + }; + + /* handle context menu */ + + // TODO(context-menu): should create on ShowContextMenu event + #[cfg(linux)] + let is_click_on_context_menu = + self.is_position_on_context_menu(compositor, position); + + // Close context menu when clicking outside of it #[cfg(linux)] - if *button == winit::event::MouseButton::Left && *state == ElementState::Pressed { + if !is_click_on_context_menu + && *button == winit::event::MouseButton::Left + && *state == ElementState::Pressed + { if let Some(context_menu) = self.context_menu.take() { - dbg!("close context menu"); if let Some(webview) = context_menu.webview { - dbg!("remove context menu"); send_to_constellation( sender, ConstellationMsg::CloseWebView(webview.webview_id), @@ -299,6 +314,7 @@ impl Window { } } } + if *button == winit::event::MouseButton::Right && *state == ElementState::Pressed { #[cfg(any(target_os = "macos", target_os = "windows"))] { @@ -310,30 +326,23 @@ impl Window { } #[cfg(linux)] { + // Close old context menu if let Some(context_menu) = self.context_menu.take() { - dbg!("closing old context menu"); if let Some(webview) = context_menu.webview { - dbg!("remove old context menu"); send_to_constellation( sender, ConstellationMsg::CloseWebView(webview.webview_id), ); } } - dbg!("open context menu"); + // Create new context menu self.context_menu = Some(self.create_context_menu()); self.show_context_menu(sender); return; } } - let position = match self.mouse_position.get() { - Some(position) => Point2D::new(position.x as f32, position.y as f32), - None => { - log::trace!("Mouse position is None, skipping MouseInput event."); - return; - } - }; + // TODO(context-menu): ignore first release event after context menu open or close to prevent click on backgound element // handle Windows and Linux non-decoration window resize #[cfg(any(linux, target_os = "windows"))] @@ -504,7 +513,6 @@ impl Window { id: WebViewId, compositor: &mut IOCompositor, ) -> (Option, bool) { - dbg!("REMOVE WEBVIEW"); if self .panel .as_ref() @@ -655,6 +663,33 @@ impl Window { } } + #[cfg(linux)] + fn is_position_on_context_menu( + &self, + compositor: &mut IOCompositor, + position: DevicePoint, + ) -> bool { + let result = compositor.hit_test_at_point(position); + + if let Some(result) = result { + let pipeline_id = result.pipeline_id; + if let Some(webview_id) = compositor.webview_id_by_pipeline_id(pipeline_id) { + return self + .context_menu + .as_ref() + .and_then(|context_menu| context_menu.webview.as_ref()) + .and_then(|webview| { + if webview.webview_id == webview_id { + return Some(true); + } + None + }) + .unwrap_or(false); + } + } + false + } + #[cfg(any(target_os = "macos", target_os = "windows"))] fn handle_context_menu_event(&self, sender: &Sender, event: MenuEvent) { // TODO: should be more flexible to handle different menu items From 4b9814fefaafd7c2b1e9a1886cf78ea23b5809b8 Mon Sep 17 00:00:00 2001 From: Jason Tsai Date: Wed, 13 Nov 2024 23:58:33 +0800 Subject: [PATCH 05/17] fix: context menu prompt back to verso --- resources/context_menu.html | 5 ++--- src/webview.rs | 41 +++++++++++++++++++++++++++++-------- src/window.rs | 18 ++++++++++++++++ 3 files changed, 52 insertions(+), 12 deletions(-) diff --git a/resources/context_menu.html b/resources/context_menu.html index 32afd1f3..9691130c 100644 --- a/resources/context_menu.html +++ b/resources/context_menu.html @@ -4,7 +4,7 @@ body { font-family: Arial, Helvetica, sans-serif; background: #dfdfdf; - width: 185px; + width: 184px; } .menu { display: flex; @@ -63,8 +63,7 @@ close: true, }); console.log(`CONTEXT_MENU:${msg}`); - // FIXME: current using propmt will crash on verso's servo backend, IPCChannel seems disconnected before recv - // window.prompt(`CONTEXT_MENU:${msg}`); + window.prompt(`CONTEXT_MENU:${msg}`); }; } diff --git a/src/webview.rs b/src/webview.rs index 6adce193..06e9294a 100644 --- a/src/webview.rs +++ b/src/webview.rs @@ -196,15 +196,7 @@ impl Window { if let Some(webview) = &self.webview { let id = webview.webview_id; - if msg.starts_with("CONTEXT_MENU:") { - // FIXME: we can't get message from menu dialog now. App crashes before we get this message and it's because prompt_sender's channel is already closed. - let json_str_msg = msg.strip_prefix("CONTEXT_MENU:").unwrap(); - dbg!(json_str_msg); - let json = - serde_json::from_str::(json_str_msg) - .unwrap(); - dbg!(json); - } else if msg.starts_with("NAVIGATE_TO:") { + if msg.starts_with("NAVIGATE_TO:") { let unparsed_url = msg.strip_prefix("NAVIGATE_TO:").unwrap(); let url = match Url::parse(unparsed_url) { Ok(url_parsed) => url_parsed, @@ -301,4 +293,35 @@ impl Window { } false } + + /// Handle servo messages with main panel. Return true it requests a new window. + pub fn handle_servo_messages_with_context_menu( + &mut self, + webview_id: WebViewId, + message: EmbedderMsg, + _sender: &Sender, + _clipboard: Option<&mut Clipboard>, + _compositor: &mut IOCompositor, + ) -> bool { + log::trace!("Verso Context Menu {webview_id:?} is handling Embedder message: {message:?}",); + match message { + EmbedderMsg::Prompt(definition, _origin) => match definition { + PromptDefinition::Input(msg, _, prompt_sender) => { + let _ = prompt_sender.send(None); + if msg.starts_with("CONTEXT_MENU:") { + let json_str_msg = msg.strip_prefix("CONTEXT_MENU:").unwrap(); + let result = + serde_json::from_str::(json_str_msg).unwrap(); + + dbg!(result); + } + } + _ => log::trace!("Verso Panel isn't supporting this prompt yet"), + }, + e => { + log::trace!("Verso Panel isn't supporting this message yet: {e:?}") + } + } + false + } } diff --git a/src/window.rs b/src/window.rs index d91eb178..cf7910ed 100644 --- a/src/window.rs +++ b/src/window.rs @@ -462,6 +462,15 @@ impl Window { ); } } + if let Some(context_menu) = &self.context_menu { + if let Some(webview) = &context_menu.webview { + if webview.webview_id == webview_id { + return self.handle_servo_messages_with_context_menu( + webview_id, message, sender, clipboard, compositor, + ); + } + } + } // Handle message in Verso WebView self.handle_servo_messages_with_webview(webview_id, message, sender, clipboard, compositor); false @@ -498,12 +507,21 @@ impl Window { self.dialog_webviews.retain(|w| w.webview_id != id); } + /// Check has dialog webview in the window. + pub fn has_dialog_webview(&self, id: WebViewId) -> bool { + self.dialog_webviews + .iter() + .find(|w| w.webview_id == id) + .is_some() + } + /// Check if the window has such webview. pub fn has_webview(&self, id: WebViewId) -> bool { self.panel .as_ref() .map_or(false, |w| w.webview.webview_id == id) || self.webview.as_ref().map_or(false, |w| w.webview_id == id) + || self.has_dialog_webview(id) } /// Remove the webview in this window by provided webview ID. If this is the panel, it will From 487d6ba84fa776c71cfefb7917eacfd49a474635 Mon Sep 17 00:00:00 2001 From: Jason Tsai Date: Thu, 14 Nov 2024 10:28:05 +0800 Subject: [PATCH 06/17] feat(context-menu): on linux, handle selection --- .gitignore | 1 - resources/context_menu.html | 6 ++- src/compositor.rs | 1 + src/context_menu.rs | 2 +- src/webview.rs | 4 +- src/window.rs | 86 ++++++++++++++++++++++++++++--------- 6 files changed, 74 insertions(+), 26 deletions(-) diff --git a/.gitignore b/.gitignore index 72de5f50..0dd95b87 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ target Cargo.lock .DS_Store .vscode/ -resources/ .flatpak-builder/ libmozjs* cargo-sources.json diff --git a/resources/context_menu.html b/resources/context_menu.html index 9691130c..825c2054 100644 --- a/resources/context_menu.html +++ b/resources/context_menu.html @@ -13,12 +13,14 @@ justify-content: start; } .menu-item { - display: flex; + display: inline-block; height: 30px; width: 100%; line-height: 30px; - align-items: center; padding-left: 5px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; } .menu-item:hover { background: #cecece; diff --git a/src/compositor.rs b/src/compositor.rs index 2f05e268..d2e8b309 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -1114,6 +1114,7 @@ impl IOCompositor { window_id = Some(window.id()); } else { // if the window is not closed, we need to update the display list + // to remove the webview from viewport self.send_root_pipeline_display_list(window); } diff --git a/src/context_menu.rs b/src/context_menu.rs index 890a563a..bd3c5aeb 100644 --- a/src/context_menu.rs +++ b/src/context_menu.rs @@ -106,7 +106,7 @@ impl ContextMenu { } /// get item json - pub fn get_items_json(&self) -> String { + pub fn to_items_json(&self) -> String { serde_json::to_string(&self.menu_items).unwrap() } } diff --git a/src/webview.rs b/src/webview.rs index 06e9294a..276528ea 100644 --- a/src/webview.rs +++ b/src/webview.rs @@ -299,7 +299,7 @@ impl Window { &mut self, webview_id: WebViewId, message: EmbedderMsg, - _sender: &Sender, + sender: &Sender, _clipboard: Option<&mut Clipboard>, _compositor: &mut IOCompositor, ) -> bool { @@ -313,7 +313,7 @@ impl Window { let result = serde_json::from_str::(json_str_msg).unwrap(); - dbg!(result); + self.handle_context_menu_event(sender, result); } } _ => log::trace!("Verso Panel isn't supporting this prompt yet"), diff --git a/src/window.rs b/src/window.rs index cf7910ed..8fa2d4ee 100644 --- a/src/window.rs +++ b/src/window.rs @@ -14,7 +14,7 @@ use glutin_winit::DisplayBuilder; use muda::{Menu, MenuEvent, MenuEventReceiver, MenuItem}; #[cfg(any(target_os = "macos", target_os = "windows"))] use raw_window_handle::HasWindowHandle; -#[cfg(any(target_os = "macos", target_os = "windows"))] +// #[cfg(any(target_os = "macos", target_os = "windows"))] use script_traits::TraversalDirection; use script_traits::{TouchEventType, WheelDelta, WheelMode}; use servo_url::ServoUrl; @@ -68,7 +68,7 @@ pub struct Window { dialog_webviews: Vec, /// context_menu - context_menu: Option, + pub(crate) context_menu: Option, /// Global menu evnet receiver for muda crate #[cfg(any(target_os = "macos", target_os = "windows"))] @@ -304,14 +304,9 @@ impl Window { && *button == winit::event::MouseButton::Left && *state == ElementState::Pressed { - if let Some(context_menu) = self.context_menu.take() { - if let Some(webview) = context_menu.webview { - send_to_constellation( - sender, - ConstellationMsg::CloseWebView(webview.webview_id), - ); - return; - } + if self.close_context_menu(sender) { + // do not prcoess event for underlying elements + return; } } @@ -327,14 +322,7 @@ impl Window { #[cfg(linux)] { // Close old context menu - if let Some(context_menu) = self.context_menu.take() { - if let Some(webview) = context_menu.webview { - send_to_constellation( - sender, - ConstellationMsg::CloseWebView(webview.webview_id), - ); - } - } + self.close_context_menu(sender); // Create new context menu self.context_menu = Some(self.create_context_menu()); self.show_context_menu(sender); @@ -445,6 +433,19 @@ impl Window { } } + /// Close the context menu + /// + /// If context menu exists, return true. + pub(crate) fn close_context_menu(&mut self, sender: &Sender) -> bool { + if let Some(context_menu) = self.context_menu.take() { + if let Some(webview) = context_menu.webview { + send_to_constellation(sender, ConstellationMsg::CloseWebView(webview.webview_id)); + return true; + } + } + false + } + /// Handle servo messages. Return true if it requests a new window pub fn handle_servo_message( &mut self, @@ -454,6 +455,7 @@ impl Window { clipboard: Option<&mut Clipboard>, compositor: &mut IOCompositor, ) -> bool { + println!("{:?} handle_servo_message: {:?}", webview_id, message); // Handle message in Verso Panel if let Some(panel) = &self.panel { if panel.webview.webview_id == webview_id { @@ -465,9 +467,10 @@ impl Window { if let Some(context_menu) = &self.context_menu { if let Some(webview) = &context_menu.webview { if webview.webview_id == webview_id { - return self.handle_servo_messages_with_context_menu( + self.handle_servo_messages_with_context_menu( webview_id, message, sender, clipboard, compositor, ); + return false; } } } @@ -668,7 +671,7 @@ impl Window { if let Some(ref mut context_menu) = self.context_menu { let mouse_position = self.mouse_position.get().unwrap(); let position = Point2D::new(mouse_position.x as i32, mouse_position.y as i32); - let items_json = context_menu.get_items_json(); + let items_json = context_menu.to_items_json(); let url_str = format!("verso://context_menu.html?items={}", items_json); let url = ServoUrl::parse(&url_str).unwrap(); let menu_webview = context_menu.create_webview(position, scale_factor); @@ -708,6 +711,49 @@ impl Window { false } + /// Handle linux context menu event + // TODO(context-menu): should make the call in synchronous way after calling show_context_menu, otherwise + // we'll have to deal with constellation sender and other parameter's lifetime, also we lose the context that why this context menu popup + #[cfg(linux)] + pub(crate) fn handle_context_menu_event( + &mut self, + sender: &Sender, + event: crate::context_menu::ContextMenuClickResult, + ) { + // FIXME: (context-menu) find the reason that close after doing action (traverse history) will hang the window + // Close context menu somehow must put before other actions, or it will hang the window + if event.close { + self.close_context_menu(sender); + } + match event.id.as_str() { + "back" => { + send_to_constellation( + sender, + ConstellationMsg::TraverseHistory( + self.webview.as_ref().unwrap().webview_id, + TraversalDirection::Back(1), + ), + ); + } + "forward" => { + send_to_constellation( + sender, + ConstellationMsg::TraverseHistory( + self.webview.as_ref().unwrap().webview_id, + TraversalDirection::Forward(1), + ), + ); + } + "reload" => { + send_to_constellation( + sender, + ConstellationMsg::Reload(self.webview.as_ref().unwrap().webview_id), + ); + } + _ => {} + } + } + #[cfg(any(target_os = "macos", target_os = "windows"))] fn handle_context_menu_event(&self, sender: &Sender, event: MenuEvent) { // TODO: should be more flexible to handle different menu items From 5b0a26aaa6fc3cf5401962bdd318677cdea82b82 Mon Sep 17 00:00:00 2001 From: Jason Tsai Date: Thu, 14 Nov 2024 10:47:56 +0800 Subject: [PATCH 07/17] fix: disble right click on context menu --- resources/context_menu.html | 6 +++- src/window.rs | 61 ++++++++++++++++++++++--------------- 2 files changed, 42 insertions(+), 25 deletions(-) diff --git a/resources/context_menu.html b/resources/context_menu.html index 825c2054..702d1709 100644 --- a/resources/context_menu.html +++ b/resources/context_menu.html @@ -59,7 +59,11 @@ if (!enabled) { menuItem.classList.add('disabled'); } else { - menuItem.onclick = () => { + menuItem.onclick = (ev) => { + // accept left click only + if (ev.buttons != 1) { + return; + } const msg = JSON.stringify({ id, close: true, diff --git a/src/window.rs b/src/window.rs index 8fa2d4ee..e068d7f8 100644 --- a/src/window.rs +++ b/src/window.rs @@ -292,26 +292,48 @@ impl Window { }; /* handle context menu */ - // TODO(context-menu): should create on ShowContextMenu event - #[cfg(linux)] - let is_click_on_context_menu = - self.is_position_on_context_menu(compositor, position); - // Close context menu when clicking outside of it #[cfg(linux)] - if !is_click_on_context_menu - && *button == winit::event::MouseButton::Left - && *state == ElementState::Pressed { - if self.close_context_menu(sender) { - // do not prcoess event for underlying elements - return; + let is_click_on_context_menu = + self.is_position_on_context_menu(compositor, position); + + if !is_click_on_context_menu { + if *state == ElementState::Pressed { + match *button { + winit::event::MouseButton::Left => { + if self.close_context_menu(sender) { + // return here to bypass following mouse event for underlying element + return; + } + } + winit::event::MouseButton::Right => { + // Close old context menu + self.close_context_menu(sender); + // Create new context menu + self.context_menu = Some(self.create_context_menu()); + self.show_context_menu(sender); + return; + } + _ => {} + } + } else if *state == ElementState::Released { + match *button { + winit::event::MouseButton::Right => { + if self.context_menu.is_some() { + return; + } + } + _ => {} + } + } } + // TODO(context-menu): ignore first release event after context menu open or close to prevent click on backgound element } + #[cfg(any(target_os = "macos", target_os = "windows"))] if *button == winit::event::MouseButton::Right && *state == ElementState::Pressed { - #[cfg(any(target_os = "macos", target_os = "windows"))] { self.show_context_menu(); // FIXME: there's chance to lose the event since the channel is async. @@ -319,20 +341,10 @@ impl Window { self.handle_context_menu_event(sender, event); } } - #[cfg(linux)] - { - // Close old context menu - self.close_context_menu(sender); - // Create new context menu - self.context_menu = Some(self.create_context_menu()); - self.show_context_menu(sender); - return; - } } - // TODO(context-menu): ignore first release event after context menu open or close to prevent click on backgound element + /* handle Windows and Linux non-decoration window resize */ - // handle Windows and Linux non-decoration window resize #[cfg(any(linux, target_os = "windows"))] { if *state == ElementState::Pressed && *button == winit::event::MouseButton::Left @@ -343,6 +355,8 @@ impl Window { } } + /* handle mouse events */ + let button: script_traits::MouseButton = match button { winit::event::MouseButton::Left => script_traits::MouseButton::Left, winit::event::MouseButton::Right => script_traits::MouseButton::Right, @@ -455,7 +469,6 @@ impl Window { clipboard: Option<&mut Clipboard>, compositor: &mut IOCompositor, ) -> bool { - println!("{:?} handle_servo_message: {:?}", webview_id, message); // Handle message in Verso Panel if let Some(panel) = &self.panel { if panel.webview.webview_id == webview_id { From 0c63a6d60bf31962b967e5a6e13de95b09ba9fc6 Mon Sep 17 00:00:00 2001 From: Jason Tsai Date: Thu, 14 Nov 2024 18:19:04 +0800 Subject: [PATCH 08/17] organize code --- resources/context_menu.html | 4 +- src/context_menu.rs | 87 ++++++++++++++---------- src/window.rs | 132 ++++++++++++++++-------------------- 3 files changed, 113 insertions(+), 110 deletions(-) diff --git a/resources/context_menu.html b/resources/context_menu.html index 702d1709..765b5f92 100644 --- a/resources/context_menu.html +++ b/resources/context_menu.html @@ -13,6 +13,7 @@ justify-content: start; } .menu-item { + cursor: pointer; display: inline-block; height: 30px; width: 100%; @@ -27,6 +28,7 @@ border-radius: 5px; } .menu-item.disabled { + cursor: default; background: #dfdfdf; color: #505050; cursor: pointer; @@ -61,7 +63,7 @@ } else { menuItem.onclick = (ev) => { // accept left click only - if (ev.buttons != 1) { + if (ev.buttons !== 1) { return; } const msg = JSON.stringify({ diff --git a/src/context_menu.rs b/src/context_menu.rs index bd3c5aeb..869915c9 100644 --- a/src/context_menu.rs +++ b/src/context_menu.rs @@ -9,13 +9,19 @@ use raw_window_handle::{HasWindowHandle, RawWindowHandle}; /* Wayland Implementation */ #[cfg(linux)] -use crate::webview::WebView; +use crate::{verso::send_to_constellation, webview::WebView, window::Window}; +#[cfg(linux)] +use compositing_traits::ConstellationMsg; +#[cfg(linux)] +use crossbeam_channel::Sender; #[cfg(linux)] use serde::{Deserialize, Serialize}; #[cfg(linux)] -use webrender_api::units::DeviceIntPoint; +use servo_url::ServoUrl; #[cfg(linux)] use webrender_api::units::DeviceIntRect; +#[cfg(linux)] +use winit::dpi::PhysicalPosition; /// Context Menu #[cfg(any(target_os = "macos", target_os = "windows"))] @@ -65,7 +71,7 @@ impl ContextMenu { pub struct ContextMenu { menu_items: Vec, /// The webview that the context menu is attached to - pub webview: Option, + webview: WebView, } #[cfg(linux)] @@ -74,19 +80,48 @@ impl ContextMenu { /// /// Often used by calling window.alert() or window.confirm() in the web page. pub fn new_with_menu(menu_items: Vec) -> Self { + let webview_id = WebViewId::new(); + let webview = WebView::new(webview_id, DeviceIntRect::zero()); + Self { menu_items, - webview: None, + webview, } } - /// Set the context menu's options - pub fn set_menu_items(&mut self, menu_items: Vec) { - self.menu_items = menu_items; + + /// Show the context menu to current cursor position + pub fn show( + &mut self, + sender: &Sender, + window: &mut Window, + position: PhysicalPosition, + ) { + let scale_factor = window.scale_factor(); + self.set_position(position, scale_factor); + + send_to_constellation( + sender, + ConstellationMsg::NewWebView(self.resource_url(), self.webview.webview_id), + ); + window.append_dialog_webview(self.webview.clone()); + } + + /// Get webview of the context menu + pub fn webview(&self) -> &WebView { + &self.webview + } + + /// Get resource URL of the context menu + fn resource_url(&self) -> ServoUrl { + let items_json: String = self.to_items_json(); + let url_str = format!("verso://context_menu.html?items={}", items_json); + ServoUrl::parse(&url_str).unwrap() } - /// Show the context menu on position - pub fn create_webview(&mut self, position: DeviceIntPoint, scale_factor: f64) -> WebView { + + /// Set the position of the context menu + fn set_position(&mut self, position: PhysicalPosition, scale_factor: f64) { // Translate position to origin - let origin = Point2D::new(position.x, position.y); + let origin = Point2D::new(position.x as i32, position.y as i32); // Calculate menu size // Each menu item is 30px height @@ -96,17 +131,11 @@ impl ContextMenu { let size = Size2D::new(menu_width as i32, menu_height as i32); let rect = DeviceIntRect::from_origin_and_size(origin, size); - let webview_id = WebViewId::new(); - let webview = WebView::new(webview_id, rect); - // let url = ServoUrl::parse("https://example.com").unwrap(); - - self.webview = Some(webview.clone()); - - webview + self.webview.set_size(rect); } /// get item json - pub fn to_items_json(&self) -> String { + fn to_items_json(&self) -> String { serde_json::to_string(&self.menu_items).unwrap() } } @@ -115,8 +144,10 @@ impl ContextMenu { #[derive(Debug, Clone, Serialize)] pub struct MenuItem { id: String, - label: String, - enabled: bool, + /// label of the menu item + pub label: String, + /// Whether the menu item is enabled + pub enabled: bool, } impl MenuItem { @@ -133,27 +164,13 @@ impl MenuItem { pub fn id(&self) -> &str { &self.id } - /// Get the label of the menu item - pub fn label(&self) -> &str { - &self.label - } - /// Set the label of the menu item - pub fn set_label(&mut self, label: &str) -> &Self { - self.label = label.to_string(); - self - } - /// Enable or disable menu item - pub fn set_enabled(&mut self, enabled: bool) -> &Self { - self.enabled = enabled; - self - } } /// Context Menu Click Result #[cfg(linux)] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ContextMenuClickResult { - /// The id of the menu item + /// The id of the menu ite /// Get the label of the menu item pub id: String, /// Close the context menu pub close: bool, diff --git a/src/window.rs b/src/window.rs index e068d7f8..a83cf8e0 100644 --- a/src/window.rs +++ b/src/window.rs @@ -312,8 +312,7 @@ impl Window { // Close old context menu self.close_context_menu(sender); // Create new context menu - self.context_menu = Some(self.create_context_menu()); - self.show_context_menu(sender); + self.context_menu = Some(self.show_context_menu(sender)); return; } _ => {} @@ -447,19 +446,6 @@ impl Window { } } - /// Close the context menu - /// - /// If context menu exists, return true. - pub(crate) fn close_context_menu(&mut self, sender: &Sender) -> bool { - if let Some(context_menu) = self.context_menu.take() { - if let Some(webview) = context_menu.webview { - send_to_constellation(sender, ConstellationMsg::CloseWebView(webview.webview_id)); - return true; - } - } - false - } - /// Handle servo messages. Return true if it requests a new window pub fn handle_servo_message( &mut self, @@ -478,13 +464,11 @@ impl Window { } } if let Some(context_menu) = &self.context_menu { - if let Some(webview) = &context_menu.webview { - if webview.webview_id == webview_id { - self.handle_servo_messages_with_context_menu( - webview_id, message, sender, clipboard, compositor, - ); - return false; - } + if context_menu.webview().webview_id == webview_id { + self.handle_servo_messages_with_context_menu( + webview_id, message, sender, clipboard, compositor, + ); + return false; } } // Handle message in Verso WebView @@ -514,8 +498,8 @@ impl Window { } /// Append a dialog webview to the window. - pub fn append_dialog_webview(&mut self, webview: &WebView) { - self.dialog_webviews.push(webview.clone()); + pub fn append_dialog_webview(&mut self, webview: WebView) { + self.dialog_webviews.push(webview); } /// Remove a dialog webview from the window. @@ -660,9 +644,42 @@ impl Window { context_menu.show(self.window.window_handle().unwrap()); } + #[cfg(any(target_os = "macos", target_os = "windows"))] + fn handle_context_menu_event(&self, sender: &Sender, event: MenuEvent) { + // TODO: should be more flexible to handle different menu items + match event.id().0.as_str() { + "back" => { + send_to_constellation( + sender, + ConstellationMsg::TraverseHistory( + self.webview.as_ref().unwrap().webview_id, + TraversalDirection::Back(1), + ), + ); + } + "forward" => { + send_to_constellation( + sender, + ConstellationMsg::TraverseHistory( + self.webview.as_ref().unwrap().webview_id, + TraversalDirection::Forward(1), + ), + ); + } + "reload" => { + send_to_constellation( + sender, + ConstellationMsg::Reload(self.webview.as_ref().unwrap().webview_id), + ); + } + _ => {} + } + } + #[cfg(linux)] - pub(crate) fn create_context_menu(&mut self) -> ContextMenu { + pub(crate) fn show_context_menu(&mut self, sender: &Sender) -> ContextMenu { use crate::context_menu::MenuItem; + let history_len = self.history.len(); // items @@ -674,27 +691,27 @@ impl Window { ); let reload = MenuItem::new(Some("reload"), "Reload", true); - ContextMenu::new_with_menu([back, forward, reload].to_vec()) + let mut context_menu = ContextMenu::new_with_menu([back, forward, reload].to_vec()); + + let position = self.mouse_position.get().unwrap(); + context_menu.show(sender, self, position); + + context_menu } + /// Close the context menu + /// + /// If context menu exists, return true. #[cfg(linux)] - pub(crate) fn show_context_menu(&mut self, sender: &Sender) { - let scale_factor = self.scale_factor(); - - if let Some(ref mut context_menu) = self.context_menu { - let mouse_position = self.mouse_position.get().unwrap(); - let position = Point2D::new(mouse_position.x as i32, mouse_position.y as i32); - let items_json = context_menu.to_items_json(); - let url_str = format!("verso://context_menu.html?items={}", items_json); - let url = ServoUrl::parse(&url_str).unwrap(); - let menu_webview = context_menu.create_webview(position, scale_factor); + pub(crate) fn close_context_menu(&mut self, sender: &Sender) -> bool { + if let Some(context_menu) = self.context_menu.take() { send_to_constellation( sender, - ConstellationMsg::NewWebView(url, menu_webview.webview_id), + ConstellationMsg::CloseWebView(context_menu.webview().webview_id), ); - - self.append_dialog_webview(&menu_webview); + return true; } + false } #[cfg(linux)] @@ -711,9 +728,8 @@ impl Window { return self .context_menu .as_ref() - .and_then(|context_menu| context_menu.webview.as_ref()) - .and_then(|webview| { - if webview.webview_id == webview_id { + .and_then(|context_menu| { + if context_menu.webview().webview_id == webview_id { return Some(true); } None @@ -766,38 +782,6 @@ impl Window { _ => {} } } - - #[cfg(any(target_os = "macos", target_os = "windows"))] - fn handle_context_menu_event(&self, sender: &Sender, event: MenuEvent) { - // TODO: should be more flexible to handle different menu items - match event.id().0.as_str() { - "back" => { - send_to_constellation( - sender, - ConstellationMsg::TraverseHistory( - self.webview.as_ref().unwrap().webview_id, - TraversalDirection::Back(1), - ), - ); - } - "forward" => { - send_to_constellation( - sender, - ConstellationMsg::TraverseHistory( - self.webview.as_ref().unwrap().webview_id, - TraversalDirection::Forward(1), - ), - ); - } - "reload" => { - send_to_constellation( - sender, - ConstellationMsg::Reload(self.webview.as_ref().unwrap().webview_id), - ); - } - _ => {} - } - } } // Non-decorated window resizing for Windows and Linux. From d7d97e3e69793fe2e4dcaf63540475e1a0132fdb Mon Sep 17 00:00:00 2001 From: Jason Tsai Date: Thu, 14 Nov 2024 18:23:43 +0800 Subject: [PATCH 09/17] adding cfg target linux --- .gitignore | 4 ++++ src/context_menu.rs | 9 ++++++--- src/webview.rs | 9 +++++---- src/window.rs | 7 +++++-- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 0dd95b87..519b7e3b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,7 @@ Cargo.lock .flatpak-builder/ libmozjs* cargo-sources.json + +resources/ +!resources/panel.html +!resources/context-menu.html \ No newline at end of file diff --git a/src/context_menu.rs b/src/context_menu.rs index 869915c9..85f49044 100644 --- a/src/context_menu.rs +++ b/src/context_menu.rs @@ -1,6 +1,3 @@ -use base::id::WebViewId; -use euclid::{Point2D, Size2D}; - /* macOS, Windows Native Implementation */ #[cfg(any(target_os = "macos", target_os = "windows"))] use muda::{ContextMenu as MudaContextMenu, Menu}; @@ -11,10 +8,14 @@ use raw_window_handle::{HasWindowHandle, RawWindowHandle}; #[cfg(linux)] use crate::{verso::send_to_constellation, webview::WebView, window::Window}; #[cfg(linux)] +use base::id::WebViewId; +#[cfg(linux)] use compositing_traits::ConstellationMsg; #[cfg(linux)] use crossbeam_channel::Sender; #[cfg(linux)] +use euclid::{Point2D, Size2D}; +#[cfg(linux)] use serde::{Deserialize, Serialize}; #[cfg(linux)] use servo_url::ServoUrl; @@ -141,6 +142,7 @@ impl ContextMenu { } /// Menu Item +#[cfg(linux)] #[derive(Debug, Clone, Serialize)] pub struct MenuItem { id: String, @@ -150,6 +152,7 @@ pub struct MenuItem { pub enabled: bool, } +#[cfg(linux)] impl MenuItem { /// Create a new menu item pub fn new(id: Option<&str>, label: &str, enabled: bool) -> Self { diff --git a/src/webview.rs b/src/webview.rs index 276528ea..68513a09 100644 --- a/src/webview.rs +++ b/src/webview.rs @@ -12,10 +12,10 @@ use servo_url::ServoUrl; use url::Url; use webrender_api::units::DeviceIntRect; -use crate::{ - compositor::IOCompositor, context_menu::ContextMenuClickResult, verso::send_to_constellation, - window::Window, -}; +use crate::{compositor::IOCompositor, verso::send_to_constellation, window::Window}; + +#[cfg(linux)] +use crate::context_menu::ContextMenuClickResult; /// A web view is an area to display web browsing context. It's what user will treat as a "web page". #[derive(Debug, Clone)] @@ -295,6 +295,7 @@ impl Window { } /// Handle servo messages with main panel. Return true it requests a new window. + #[cfg(linux)] pub fn handle_servo_messages_with_context_menu( &mut self, webview_id: WebViewId, diff --git a/src/window.rs b/src/window.rs index a83cf8e0..713e8600 100644 --- a/src/window.rs +++ b/src/window.rs @@ -14,7 +14,6 @@ use glutin_winit::DisplayBuilder; use muda::{Menu, MenuEvent, MenuEventReceiver, MenuItem}; #[cfg(any(target_os = "macos", target_os = "windows"))] use raw_window_handle::HasWindowHandle; -// #[cfg(any(target_os = "macos", target_os = "windows"))] use script_traits::TraversalDirection; use script_traits::{TouchEventType, WheelDelta, WheelMode}; use servo_url::ServoUrl; @@ -67,7 +66,8 @@ pub struct Window { /// dialog webviews dialog_webviews: Vec, - /// context_menu + /// Linux context_menu + #[cfg(linux)] pub(crate) context_menu: Option, /// Global menu evnet receiver for muda crate @@ -124,6 +124,7 @@ impl Window { current_history_index: 0, resizing: false, dialog_webviews: vec![], + #[cfg(linux)] context_menu: None, #[cfg(any(target_os = "macos", target_os = "windows"))] menu_event_receiver: MenuEvent::receiver().clone(), @@ -167,6 +168,7 @@ impl Window { current_history_index: 0, resizing: false, dialog_webviews: vec![], + #[cfg(linux)] context_menu: None, #[cfg(any(target_os = "macos", target_os = "windows"))] menu_event_receiver: MenuEvent::receiver().clone(), @@ -463,6 +465,7 @@ impl Window { ); } } + #[cfg(linux)] if let Some(context_menu) = &self.context_menu { if context_menu.webview().webview_id == webview_id { self.handle_servo_messages_with_context_menu( From 73f63e27433bba7ba7375121187c719ce6918d1c Mon Sep 17 00:00:00 2001 From: Jason Tsai Date: Fri, 15 Nov 2024 16:24:40 +0800 Subject: [PATCH 10/17] fix(linux): shift context menu to avoid overflow, best effort --- src/context_menu.rs | 53 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/src/context_menu.rs b/src/context_menu.rs index 85f49044..d0eea815 100644 --- a/src/context_menu.rs +++ b/src/context_menu.rs @@ -98,7 +98,7 @@ impl ContextMenu { position: PhysicalPosition, ) { let scale_factor = window.scale_factor(); - self.set_position(position, scale_factor); + self.set_position(window, position, scale_factor); send_to_constellation( sender, @@ -120,19 +120,52 @@ impl ContextMenu { } /// Set the position of the context menu - fn set_position(&mut self, position: PhysicalPosition, scale_factor: f64) { - // Translate position to origin - let origin = Point2D::new(position.x as i32, position.y as i32); - + fn set_position( + &mut self, + window: &Window, + position: PhysicalPosition, + scale_factor: f64, + ) { // Calculate menu size // Each menu item is 30px height // Menu has 10px padding top and bottom - let menu_height = (self.menu_items.len() * 30 + 20) as f64 * scale_factor; - let menu_width = 200.0 * scale_factor; - let size = Size2D::new(menu_width as i32, menu_height as i32); - let rect = DeviceIntRect::from_origin_and_size(origin, size); + let height = (self.menu_items.len() * 30 + 20) as f64 * scale_factor; + let width = 200.0 * scale_factor; + let menu_size = Size2D::new(width as i32, height as i32); + + // Translate position to origin + let mut origin = Point2D::new(position.x as i32, position.y as i32); + + // Avoid overflow to the window, adjust position if necessary + let window_size = window.size(); + let x_overflow: i32 = origin.x + menu_size.width - window_size.width; + let y_overflow: i32 = origin.y + menu_size.height - window_size.height; + + if x_overflow >= 0 { + // check if the menu can be shown on left side of the cursor + if (origin.x - menu_size.width) >= 0 { + origin.x = i32::max(0, origin.x - menu_size.width); + } else { + // if menu can't fit to left side of the cursor, + // shift left the menu, but not less than zero. + // TODO: if still smaller than screen, should show scroller + origin.x = i32::max(0, origin.x - x_overflow); + } + } + if y_overflow >= 0 { + // check if the menu can be shown above the cursor + if (origin.y - menu_size.height) >= 0 { + origin.y = i32::max(0, origin.y - menu_size.height); + } else { + // if menu can't fit to top of the cursor + // shift up the menu, but not less than zero. + // TODO: if still smaller than screen, should show scroller + origin.y = i32::max(0, origin.y - y_overflow); + } + } - self.webview.set_size(rect); + self.webview + .set_size(DeviceIntRect::from_origin_and_size(origin, menu_size)); } /// get item json From 44d7e5c0e0616f71e1dc16e1438e40334d51981b Mon Sep 17 00:00:00 2001 From: Jason Tsai Date: Mon, 18 Nov 2024 13:34:26 +0800 Subject: [PATCH 11/17] fix: not append context_menu to dialog webviews --- src/context_menu.rs | 1 - src/window.rs | 23 ++++++++++++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/context_menu.rs b/src/context_menu.rs index d0eea815..03c0e9f0 100644 --- a/src/context_menu.rs +++ b/src/context_menu.rs @@ -104,7 +104,6 @@ impl ContextMenu { sender, ConstellationMsg::NewWebView(self.resource_url(), self.webview.webview_id), ); - window.append_dialog_webview(self.webview.clone()); } /// Get webview of the context menu diff --git a/src/window.rs b/src/window.rs index 713e8600..07eab5b1 100644 --- a/src/window.rs +++ b/src/window.rs @@ -511,7 +511,7 @@ impl Window { } /// Check has dialog webview in the window. - pub fn has_dialog_webview(&self, id: WebViewId) -> bool { + fn has_dialog_webview(&self, id: WebViewId) -> bool { self.dialog_webviews .iter() .find(|w| w.webview_id == id) @@ -525,6 +525,10 @@ impl Window { .map_or(false, |w| w.webview.webview_id == id) || self.webview.as_ref().map_or(false, |w| w.webview_id == id) || self.has_dialog_webview(id) + || self + .context_menu + .as_ref() + .map_or(false, |w| w.webview().webview_id == id) } /// Remove the webview in this window by provided webview ID. If this is the panel, it will @@ -557,6 +561,14 @@ impl Window { } else if let Some(index) = self.dialog_webviews.iter().position(|w| w.webview_id == id) { let webview = self.dialog_webviews.remove(index); (Some(webview), false) + } else if self + .context_menu + .as_ref() + .filter(|menu| menu.webview().webview_id == id) + .is_some() + { + let context_menu = self.context_menu.take().expect("Context menu should exist"); + (Some(context_menu.webview().clone()), false) } else { (None, false) } @@ -571,7 +583,12 @@ impl Window { if let Some(webview) = &self.webview { order.push(webview); } + self.dialog_webviews.iter().for_each(|w| order.push(w)); + + if let Some(context_menu) = &self.context_menu { + order.push(context_menu.webview()); + } order } @@ -706,8 +723,8 @@ impl Window { /// /// If context menu exists, return true. #[cfg(linux)] - pub(crate) fn close_context_menu(&mut self, sender: &Sender) -> bool { - if let Some(context_menu) = self.context_menu.take() { + pub(crate) fn close_context_menu(&self, sender: &Sender) -> bool { + if let Some(context_menu) = &self.context_menu { send_to_constellation( sender, ConstellationMsg::CloseWebView(context_menu.webview().webview_id), From 918a902598612ff2de74aa122c77c1e8444337a9 Mon Sep 17 00:00:00 2001 From: Jason Tsai Date: Mon, 18 Nov 2024 13:35:51 +0800 Subject: [PATCH 12/17] Update src/window.rs Co-authored-by: Ngo Iok Ui (Wu Yu Wei) Signed-off-by: Jason Tsai --- src/window.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/window.rs b/src/window.rs index 07eab5b1..40254672 100644 --- a/src/window.rs +++ b/src/window.rs @@ -70,7 +70,7 @@ pub struct Window { #[cfg(linux)] pub(crate) context_menu: Option, - /// Global menu evnet receiver for muda crate + /// Global menu event receiver for muda crate #[cfg(any(target_os = "macos", target_os = "windows"))] menu_event_receiver: MenuEventReceiver, } From ac290de032f336f10f18b66b1c1dde935f572277 Mon Sep 17 00:00:00 2001 From: Jason Tsai Date: Mon, 18 Nov 2024 13:35:59 +0800 Subject: [PATCH 13/17] Update src/window.rs Co-authored-by: Ngo Iok Ui (Wu Yu Wei) Signed-off-by: Jason Tsai --- src/window.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/window.rs b/src/window.rs index 40254672..dbdfce07 100644 --- a/src/window.rs +++ b/src/window.rs @@ -330,7 +330,7 @@ impl Window { } } } - // TODO(context-menu): ignore first release event after context menu open or close to prevent click on backgound element + // TODO(context-menu): ignore first release event after context menu open or close to prevent click on background element } #[cfg(any(target_os = "macos", target_os = "windows"))] From 4ea912279ec049a8eae5bb0061d68a4f8af6b4b0 Mon Sep 17 00:00:00 2001 From: Jason Tsai Date: Mon, 18 Nov 2024 14:43:16 +0800 Subject: [PATCH 14/17] move webview id on position to compositor --- src/compositor.rs | 18 ++++++++++++------ src/window.rs | 27 +++++++++++---------------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/compositor.rs b/src/compositor.rs index 85c6ec25..9556fe0d 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -25,6 +25,7 @@ use script_traits::{ ScrollState, TouchEventType, TouchId, WheelDelta, WindowSizeData, WindowSizeType, }; use servo_geometry::{DeviceIndependentIntSize, DeviceIndependentPixel}; +use style::computed_values::position; use style_traits::{CSSPixel, DevicePixel, PinchZoomFactor}; use webrender::{RenderApi, Transaction}; use webrender_api::units::{ @@ -2164,16 +2165,21 @@ impl IOCompositor { .send_transaction(self.webrender_document, transaction); } - /// Get webview id by pipeline id. - pub fn webview_id_by_pipeline_id( + /// Get webview id on the position + pub fn webview_id_on_position( &self, - pipeline_id: PipelineId, + position: DevicePoint, ) -> Option { - for (w_id, p_id) in &self.webviews { - if *p_id == pipeline_id { - return Some(*w_id); + let hit_result: Option = self.hit_test_at_point(position); + if let Some(result) = hit_result { + let pipeline_id = result.pipeline_id; + for (w_id, p_id) in &self.webviews { + if *p_id == pipeline_id { + return Some(*w_id); + } } } + None } } diff --git a/src/window.rs b/src/window.rs index dbdfce07..d7bd6ed3 100644 --- a/src/window.rs +++ b/src/window.rs @@ -740,22 +740,17 @@ impl Window { compositor: &mut IOCompositor, position: DevicePoint, ) -> bool { - let result = compositor.hit_test_at_point(position); - - if let Some(result) = result { - let pipeline_id = result.pipeline_id; - if let Some(webview_id) = compositor.webview_id_by_pipeline_id(pipeline_id) { - return self - .context_menu - .as_ref() - .and_then(|context_menu| { - if context_menu.webview().webview_id == webview_id { - return Some(true); - } - None - }) - .unwrap_or(false); - } + if let Some(webview_id) = compositor.webview_id_on_position(position) { + return self + .context_menu + .as_ref() + .and_then(|context_menu| { + if context_menu.webview().webview_id == webview_id { + return Some(true); + } + None + }) + .unwrap_or(false); } false } From 3edd3994d03f98b2dca2743de430b0c8528bcf46 Mon Sep 17 00:00:00 2001 From: Jason Tsai Date: Mon, 18 Nov 2024 15:05:33 +0800 Subject: [PATCH 15/17] create Menu newtype --- src/context_menu.rs | 54 ++++++++++++++++++++++++++++----------------- src/window.rs | 6 ++--- 2 files changed, 37 insertions(+), 23 deletions(-) diff --git a/src/context_menu.rs b/src/context_menu.rs index 03c0e9f0..b1feb91a 100644 --- a/src/context_menu.rs +++ b/src/context_menu.rs @@ -1,6 +1,6 @@ /* macOS, Windows Native Implementation */ #[cfg(any(target_os = "macos", target_os = "windows"))] -use muda::{ContextMenu as MudaContextMenu, Menu}; +use muda::{ContextMenu as MudaContextMenu, Menu as MudaMenu}; #[cfg(any(target_os = "macos", target_os = "windows"))] use raw_window_handle::{HasWindowHandle, RawWindowHandle}; @@ -24,19 +24,46 @@ use webrender_api::units::DeviceIntRect; #[cfg(linux)] use winit::dpi::PhysicalPosition; -/// Context Menu +/// Context Menu inner menu #[cfg(any(target_os = "macos", target_os = "windows"))] -pub struct ContextMenu { - menu: Menu, -} +pub struct Menu(pub MudaMenu); +/// Context Menu inner menu +#[cfg(linux)] +#[derive(Debug, Clone)] +pub struct Menu(pub Vec); -#[cfg(any(target_os = "macos", target_os = "windows"))] impl ContextMenu { /// Create context menu with custom items + /// + /// **Platform Specific** + /// - macOS / Windows: Creates a context menu by muda crate with natvie OS support + /// - Linux: Creates a context menu with webview implementation pub fn new_with_menu(menu: Menu) -> Self { - Self { menu } + #[cfg(any(target_os = "macos", target_os = "windows"))] + { + Self { menu: menu.0 } + } + #[cfg(linux)] + { + let webview_id = WebViewId::new(); + let webview = WebView::new(webview_id, DeviceIntRect::zero()); + + Self { + menu_items: menu.0, + webview, + } + } } +} + +/// Context Menu +#[cfg(any(target_os = "macos", target_os = "windows"))] +pub struct ContextMenu { + menu: MudaMenu, +} +#[cfg(any(target_os = "macos", target_os = "windows"))] +impl ContextMenu { /// Show the context menu on current cursor position /// /// This function returns when the context menu is dismissed @@ -77,19 +104,6 @@ pub struct ContextMenu { #[cfg(linux)] impl ContextMenu { - /// Create a dialog in the window. - /// - /// Often used by calling window.alert() or window.confirm() in the web page. - pub fn new_with_menu(menu_items: Vec) -> Self { - let webview_id = WebViewId::new(); - let webview = WebView::new(webview_id, DeviceIntRect::zero()); - - Self { - menu_items, - webview, - } - } - /// Show the context menu to current cursor position pub fn show( &mut self, diff --git a/src/window.rs b/src/window.rs index d7bd6ed3..5768807f 100644 --- a/src/window.rs +++ b/src/window.rs @@ -33,7 +33,7 @@ use winit::{ use crate::{ compositor::{IOCompositor, MouseWindowEvent}, - context_menu::ContextMenu, + context_menu::{ContextMenu, Menu}, keyboard::keyboard_event_from_winit, rendering::{gl_config_picker, RenderingContext}, verso::send_to_constellation, @@ -660,7 +660,7 @@ impl Window { let menu = Menu::new(); let _ = menu.append_items(&[&back, &forward, &reload]); - let context_menu = ContextMenu::new_with_menu(menu); + let context_menu = ContextMenu::new_with_menu(Menu(menu)); context_menu.show(self.window.window_handle().unwrap()); } @@ -711,7 +711,7 @@ impl Window { ); let reload = MenuItem::new(Some("reload"), "Reload", true); - let mut context_menu = ContextMenu::new_with_menu([back, forward, reload].to_vec()); + let mut context_menu = ContextMenu::new_with_menu(Menu([back, forward, reload].to_vec())); let position = self.mouse_position.get().unwrap(); context_menu.show(sender, self, position); From 58298f674d59aca85b2661d82f128a7f66205fee Mon Sep 17 00:00:00 2001 From: Jason Tsai Date: Mon, 18 Nov 2024 15:06:58 +0800 Subject: [PATCH 16/17] chore: remove unused import --- src/compositor.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/compositor.rs b/src/compositor.rs index 9556fe0d..d387dcbd 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -25,7 +25,6 @@ use script_traits::{ ScrollState, TouchEventType, TouchId, WheelDelta, WindowSizeData, WindowSizeType, }; use servo_geometry::{DeviceIndependentIntSize, DeviceIndependentPixel}; -use style::computed_values::position; use style_traits::{CSSPixel, DevicePixel, PinchZoomFactor}; use webrender::{RenderApi, Transaction}; use webrender_api::units::{ From 782682615f1958fb35c91146330fd923df902a71 Mon Sep 17 00:00:00 2001 From: Jason Tsai Date: Mon, 18 Nov 2024 15:24:58 +0800 Subject: [PATCH 17/17] fix: add target_os condition on context_menu --- src/window.rs | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/src/window.rs b/src/window.rs index 5768807f..35eeb74b 100644 --- a/src/window.rs +++ b/src/window.rs @@ -11,7 +11,7 @@ use glutin::{ }; use glutin_winit::DisplayBuilder; #[cfg(any(target_os = "macos", target_os = "windows"))] -use muda::{Menu, MenuEvent, MenuEventReceiver, MenuItem}; +use muda::{Menu as MudaMenu, MenuEvent, MenuEventReceiver, MenuItem}; #[cfg(any(target_os = "macos", target_os = "windows"))] use raw_window_handle::HasWindowHandle; use script_traits::TraversalDirection; @@ -520,15 +520,20 @@ impl Window { /// Check if the window has such webview. pub fn has_webview(&self, id: WebViewId) -> bool { + #[cfg(linux)] + if self + .context_menu + .as_ref() + .map_or(false, |w| w.webview().webview_id == id) + { + return true; + } + self.panel .as_ref() .map_or(false, |w| w.webview.webview_id == id) || self.webview.as_ref().map_or(false, |w| w.webview_id == id) || self.has_dialog_webview(id) - || self - .context_menu - .as_ref() - .map_or(false, |w| w.webview().webview_id == id) } /// Remove the webview in this window by provided webview ID. If this is the panel, it will @@ -538,6 +543,17 @@ impl Window { id: WebViewId, compositor: &mut IOCompositor, ) -> (Option, bool) { + #[cfg(linux)] + if self + .context_menu + .as_ref() + .filter(|menu| menu.webview().webview_id == id) + .is_some() + { + let context_menu = self.context_menu.take().expect("Context menu should exist"); + return (Some(context_menu.webview().clone()), false); + } + if self .panel .as_ref() @@ -561,14 +577,6 @@ impl Window { } else if let Some(index) = self.dialog_webviews.iter().position(|w| w.webview_id == id) { let webview = self.dialog_webviews.remove(index); (Some(webview), false) - } else if self - .context_menu - .as_ref() - .filter(|menu| menu.webview().webview_id == id) - .is_some() - { - let context_menu = self.context_menu.take().expect("Context menu should exist"); - (Some(context_menu.webview().clone()), false) } else { (None, false) } @@ -586,9 +594,11 @@ impl Window { self.dialog_webviews.iter().for_each(|w| order.push(w)); + #[cfg(linux)] if let Some(context_menu) = &self.context_menu { order.push(context_menu.webview()); } + order } @@ -657,7 +667,7 @@ impl Window { ); let reload = MenuItem::with_id("reload", "Reload", true, None); - let menu = Menu::new(); + let menu = MudaMenu::new(); let _ = menu.append_items(&[&back, &forward, &reload]); let context_menu = ContextMenu::new_with_menu(Menu(menu));