Skip to content

Commit

Permalink
feat(context-menu): add linux webview context menu (#237)
Browse files Browse the repository at this point in the history
* feat(context-menu): create webview on wayland

* dynamic generate menu items on html

* add context_menu.html

* add check mouse hit on context-menu webview

* fix: context menu prompt back to verso

* feat(context-menu): on linux, handle selection

* fix: disble right click on context menu

* organize code

* adding cfg target linux

* fix(linux): shift context menu to avoid overflow, best effort

* fix: not append context_menu to dialog webviews

* Update src/window.rs

Co-authored-by: Ngo Iok Ui (Wu Yu Wei) <[email protected]>
Signed-off-by: Jason Tsai <[email protected]>

* Update src/window.rs

Co-authored-by: Ngo Iok Ui (Wu Yu Wei) <[email protected]>
Signed-off-by: Jason Tsai <[email protected]>

* move webview id on position to compositor

* create Menu newtype

* chore: remove unused import

* fix: add target_os condition on context_menu

---------

Signed-off-by: Jason Tsai <[email protected]>
Co-authored-by: Ngo Iok Ui (Wu Yu Wei) <[email protected]>
  • Loading branch information
pewsheen and wusyong authored Nov 20, 2024
1 parent 636a7c7 commit 5356411
Show file tree
Hide file tree
Showing 8 changed files with 575 additions and 32 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ target
Cargo.lock
.DS_Store
.vscode/
resources/
.flatpak-builder/
libmozjs*
cargo-sources.json

resources/
!resources/panel.html
!resources/context-menu.html
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,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"

Expand Down
81 changes: 81 additions & 0 deletions resources/context_menu.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<html>
<head>
<style>
body {
font-family: Arial, Helvetica, sans-serif;
background: #dfdfdf;
width: 184px;
}
.menu {
display: flex;
flex-direction: column;
align-items: center;
justify-content: start;
}
.menu-item {
cursor: pointer;
display: inline-block;
height: 30px;
width: 100%;
line-height: 30px;
padding-left: 5px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.menu-item:hover {
background: #cecece;
border-radius: 5px;
}
.menu-item.disabled {
cursor: default;
background: #dfdfdf;
color: #505050;
cursor: pointer;
}
.menu-item:hover.disabled {
background: #dfdfdf;
}
</style>
</head>
<body>
<div id="menu" class="menu"></div>
</body>
<script>
const menuEl = document.getElementById('menu');

let url = URL.parse(window.location.href);
let params = url.searchParams;

const options = JSON.parse(params.get('items'));
for (option of options) {
createMenuItem(option.id, option.label, option.enabled);
}

function createMenuItem(id, label, enabled) {
const menuItem = document.createElement('div');
menuItem.classList.add('menu-item');
menuItem.id = id;
menuItem.innerText = label;

if (!enabled) {
menuItem.classList.add('disabled');
} else {
menuItem.onclick = (ev) => {
// accept left click only
if (ev.buttons !== 1) {
return;
}
const msg = JSON.stringify({
id,
close: true,
});
console.log(`CONTEXT_MENU:${msg}`);
window.prompt(`CONTEXT_MENU:${msg}`);
};
}

menuEl.appendChild(menuItem);
}
</script>
</html>
25 changes: 24 additions & 1 deletion src/compositor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1106,6 +1106,10 @@ impl IOCompositor {

if close_window {
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);
}

self.frame_tree_id.next();
Expand Down Expand Up @@ -1329,7 +1333,8 @@ impl IOCompositor {
}
}

fn hit_test_at_point(&self, point: DevicePoint) -> Option<CompositorHitTestResult> {
/// TODO: doc
pub fn hit_test_at_point(&self, point: DevicePoint) -> Option<CompositorHitTestResult> {
return self
.hit_test_at_point_with_flags_and_pipeline(point, HitTestFlags::empty(), None)
.first()
Expand Down Expand Up @@ -2158,6 +2163,24 @@ impl IOCompositor {
self.webrender_api
.send_transaction(self.webrender_document, transaction);
}

/// Get webview id on the position
pub fn webview_id_on_position(
&self,
position: DevicePoint,
) -> Option<TopLevelBrowsingContextId> {
let hit_result: Option<CompositorHitTestResult> = 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
}
}

#[derive(Debug, PartialEq)]
Expand Down
196 changes: 186 additions & 10 deletions src/context_menu.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,69 @@
/* 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};

/// Context Menu
#[cfg(any(target_os = "macos", target_os = "windows"))]
pub struct ContextMenu {
menu: Menu,
}

/// Context Menu
/* Wayland Implementation */
#[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)]
pub struct ContextMenu {}
use crossbeam_channel::Sender;
#[cfg(linux)]
use euclid::{Point2D, Size2D};
#[cfg(linux)]
use serde::{Deserialize, Serialize};
#[cfg(linux)]
use servo_url::ServoUrl;
#[cfg(linux)]
use webrender_api::units::DeviceIntRect;
#[cfg(linux)]
use winit::dpi::PhysicalPosition;

/// Context Menu inner menu
#[cfg(any(target_os = "macos", target_os = "windows"))]
pub struct Menu(pub MudaMenu);
/// Context Menu inner menu
#[cfg(linux)]
#[derive(Debug, Clone)]
pub struct Menu(pub Vec<MenuItem>);

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
Expand Down Expand Up @@ -48,3 +92,135 @@ impl ContextMenu {
}
}
}

/// Context Menu
#[cfg(linux)]
#[derive(Debug, Clone)]
pub struct ContextMenu {
menu_items: Vec<MenuItem>,
/// The webview that the context menu is attached to
webview: WebView,
}

#[cfg(linux)]
impl ContextMenu {
/// Show the context menu to current cursor position
pub fn show(
&mut self,
sender: &Sender<ConstellationMsg>,
window: &mut Window,
position: PhysicalPosition<f64>,
) {
let scale_factor = window.scale_factor();
self.set_position(window, position, scale_factor);

send_to_constellation(
sender,
ConstellationMsg::NewWebView(self.resource_url(), self.webview.webview_id),
);
}

/// 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()
}

/// Set the position of the context menu
fn set_position(
&mut self,
window: &Window,
position: PhysicalPosition<f64>,
scale_factor: f64,
) {
// Calculate menu size
// Each menu item is 30px height
// Menu has 10px padding top and bottom
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(DeviceIntRect::from_origin_and_size(origin, menu_size));
}

/// get item json
fn to_items_json(&self) -> String {
serde_json::to_string(&self.menu_items).unwrap()
}
}

/// Menu Item
#[cfg(linux)]
#[derive(Debug, Clone, Serialize)]
pub struct MenuItem {
id: String,
/// label of the menu item
pub label: String,
/// Whether the menu item is enabled
pub enabled: bool,
}

#[cfg(linux)]
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
}
}

/// Context Menu Click Result
#[cfg(linux)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContextMenuClickResult {
/// The id of the menu ite /// Get the label of the menu item
pub id: String,
/// Close the context menu
pub close: bool,
}
Loading

0 comments on commit 5356411

Please sign in to comment.