ltk_webkit/wpe/
clipboard.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
85
86
87
88
89
90
91
92
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

//! The page's clipboard and selection, for the host to bridge to the system's: WPE keeps a clipboard per display, which no other application sees.

use std::ffi::{ c_char, CString };

use glib_sys::g_free;
use wpe_platform_sys::*;

use super::WebView;
use crate::page_scripts::{ paste_script, selection_of, SELECTION_SCRIPT };

const TEXT_FORMATS: [ &[ u8 ]; 2 ] = [ b"text/plain;charset=utf-8\0", b"text/plain\0" ];

impl WebView
{
	/// Text the page put in its clipboard, by Copy, Cut or a script, since the previous call: for the host to make it the system's.
	pub fn take_copied_text( &self ) -> Option<String>
	{
		let clipboard =
		{
			let mut g     = self.inner.lock().ok()?;
			let clipboard = clipboard_of( g.display )?;
			// SAFETY: the display, and so its clipboard, lives as long as the view.
			let count = unsafe { wpe_clipboard_get_change_count( clipboard ) };
			if count == g.clipboard_seen { return None; }
			g.clipboard_seen = count;
			clipboard
		};
		read_text( clipboard )
	}

	/// What the page pastes next, which the host takes from the system's clipboard before it passes Ctrl+V on.
	pub fn set_clipboard_text( &self, text: &str )
	{
		let Ok( text )  = CString::new( text ) else { return };
		let Ok( mut g ) = self.inner.lock() else { return };
		let Some( clipboard ) = clipboard_of( g.display ) else { return };
		// SAFETY: the clipboard lives as long as the view; it takes its own reference on the content and copies the text.
		unsafe
		{
			let content = wpe_clipboard_content_new();
			wpe_clipboard_content_set_text( content, text.as_ptr() );
			wpe_clipboard_set_content( clipboard, content );
			wpe_clipboard_content_unref( content );
			// What the host wrote is not the page's copy.
			g.clipboard_seen = wpe_clipboard_get_change_count( clipboard );
		}
	}

	/// The text selected in the page, in a text field too, for the primary selection; empty with none.
	pub fn selected_text( &self, done: impl FnOnce( String ) + 'static )
	{
		self.evaluate_javascript( SELECTION_SCRIPT, move |result|
		{
			done( result.map( |json| selection_of( &json ) ).unwrap_or_default() );
		} );
	}

	/// Insert `text` into the editable at `( x, y )`, in host pixels, as the middle button pastes.
	pub fn paste_at( &self, x: f64, y: f64, text: &str )
	{
		let zoom = self.zoom().max( 0.01 );
		let Some( ( vx, vy ) ) = self.inner.lock().ok().map( |g| g.to_view( x, y ) ) else { return };
		self.evaluate_javascript( &paste_script( vx / zoom, vy / zoom, text ), |_| {} );
	}
}

fn clipboard_of( display: *mut WPEDisplay ) -> Option<*mut WPEClipboard>
{
	if display.is_null() { return None; }
	// SAFETY: a live display; the clipboard is owned by it.
	let clipboard = unsafe { wpe_display_get_clipboard( display ) };
	( !clipboard.is_null() ).then_some( clipboard )
}

fn read_text( clipboard: *mut WPEClipboard ) -> Option<String>
{
	for format in TEXT_FORMATS
	{
		let mut size = 0;
		// SAFETY: a live clipboard; the returned buffer is ours to free.
		let data = unsafe { wpe_clipboard_read_text( clipboard, format.as_ptr() as *const c_char, &mut size ) };
		if data.is_null() { continue; }
		// SAFETY: WebKit returns `size` bytes at `data`.
		let text = String::from_utf8_lossy( unsafe { std::slice::from_raw_parts( data as *const u8, size ) } ).into_owned();
		unsafe { g_free( data as *mut _ ) };
		if !text.is_empty() { return Some( text ); }
	}
	None
}