Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(context-menu): add linux webview context menu #237

Merged
merged 22 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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