ltk_webkit/wpe/
context_menu.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

//! What a secondary press in the page lands on: WebKit's `context-menu` signal, whose hit test the host turns into a menu of its own.

use std::ffi::{ c_char, c_int, CStr };
use std::sync::{ Mutex, Weak };

use glib_sys::{ gboolean, gpointer };
use gobject_sys::{ g_signal_connect_data, GObject };
use wpe_platform_sys::{ WPE_BUTTON_SECONDARY, WPE_EVENT_POINTER_DOWN, WPE_EVENT_POINTER_UP };
use wpe_webkit_sys::*;

use super::{ popups, Inner, WebView };
use crate::types::PageTarget;

type ContextMenuFn = unsafe extern "C" fn( *mut WebKitWebView, *mut WebKitContextMenu, *mut WebKitHitTestResult, gpointer ) -> gboolean;

impl WebView
{
	/// Ask what is at `(x, y)` with a secondary click there; the page sees it as one, and the answer comes through [`WebView::take_page_target`].
	pub fn request_context_menu( &self, x: f64, y: f64 )
	{
		self.send_button( WPE_EVENT_POINTER_DOWN, WPE_BUTTON_SECONDARY, x, y );
		self.send_button( WPE_EVENT_POINTER_UP, WPE_BUTTON_SECONDARY, x, y );
	}

	/// The target of the last secondary press, once. `None` as well when the page handled the press itself.
	pub fn take_page_target( &self ) -> Option<PageTarget>
	{
		self.inner.lock().ok().and_then( |mut g| g.page_target.take() )
	}
}

pub( crate ) unsafe fn connect( webview: *mut WebKitWebView, inner: Weak<Mutex<Inner>> )
{
	// SAFETY: the handler matches the signal's signature; the closure owns its boxed `Weak`.
	unsafe
	{
		let handler = std::mem::transmute::<ContextMenuFn, unsafe extern "C" fn()>( on_context_menu );
		let data    = Box::into_raw( Box::new( inner ) ) as gpointer;
		g_signal_connect_data( webview as *mut GObject, b"context-menu\0".as_ptr() as *const c_char, Some( handler ), data, Some( popups::drop_weak ), 0 );
	}
}

/// Returns `TRUE`: the host draws the menu, WebKit shows none.
unsafe extern "C" fn on_context_menu( _webview: *mut WebKitWebView, menu: *mut WebKitContextMenu, hit: *mut WebKitHitTestResult, user_data: gpointer ) -> gboolean
{
	// SAFETY: `user_data` is the boxed `Weak` owned by the closure.
	let Some( inner ) = unsafe { &*( user_data as *const Weak<Mutex<Inner>> ) }.upgrade() else { return 1 };
	if menu.is_null() || hit.is_null() { return 1; }
	let ( mut x, mut y ): ( c_int, c_int ) = ( 0, 0 );
	// SAFETY: `menu` and `hit` are alive for the signal; the strings are owned by the hit test result and copied here.
	let mut target = unsafe
	{
		webkit_context_menu_get_position( menu, &mut x, &mut y );
		PageTarget
		{
			x:          0.0,
			y:          0.0,
			link_uri:   text( webkit_hit_test_result_get_link_uri( hit ) ),
			link_label: text( webkit_hit_test_result_get_link_label( hit ) ),
			image_uri:  text( webkit_hit_test_result_get_image_uri( hit ) ),
			editable:   webkit_hit_test_result_context_is_editable( hit ) != 0,
			selection:  webkit_hit_test_result_context_is_selection( hit ) != 0,
		}
	};
	if let Ok( mut g ) = inner.lock()
	{
		let ( hx, hy ) = g.to_host( x as f64, y as f64 );
		target.x = hx as f32;
		target.y = hy as f32;
		g.page_target = Some( target );
	}
	1
}

pub( super ) unsafe fn text( s: *const c_char ) -> Option<String>
{
	if s.is_null() { return None; }
	// SAFETY: the caller passes a NUL-terminated string that outlives the call.
	let owned = unsafe { CStr::from_ptr( s ) }.to_string_lossy().into_owned();
	( !owned.is_empty() ).then_some( owned )
}