ltk_webkit/wpe/
input_method.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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

//! The view's input-method context: editable focus out, committed text in.

use std::ffi::{ c_char, c_int, c_uint, CString };
use std::mem::size_of;
use std::ptr;
use std::sync::{ Mutex, OnceLock, Weak };

use glib_sys::{ gpointer, GType };
use gobject_sys::
{
	g_object_get_data, g_object_new, g_object_set_data_full, g_object_unref,
	g_signal_emit_by_name, g_type_register_static_simple, GObject,
};
use wpe_webkit_sys::
{
	webkit_input_method_context_get_type, webkit_web_view_set_input_method_context,
	WebKitInputMethodContext, WebKitInputMethodContextClass, WebKitWebView,
};

use super::{ Inner, WebView };

const INNER_KEY: &[ u8 ] = b"ltk-webkit-im-inner\0";

impl WebView
{
	/// Whether an editable element of the page holds the focus.
	pub fn editable_focused( &self ) -> bool
	{
		self.inner.lock().map( |g| g.editable_focused ).unwrap_or( false )
	}

	/// Insert text committed by the host's input method at the page's cursor.
	pub fn commit_text( &self, text: &str )
	{
		let Some( context ) = self.im_context() else { return };
		let Ok( text_c )    = CString::new( text ) else { return };
		// SAFETY: `context` lives as long as the view; the signal copies the string.
		unsafe { g_signal_emit_by_name( context as *mut GObject, b"committed\0".as_ptr() as *const c_char, text_c.as_ptr() ); }
	}

	/// Delete `before` characters before the page's cursor and `after` after it.
	pub fn delete_surrounding_text( &self, before: u32, after: u32 )
	{
		let Some( context ) = self.im_context() else { return };
		let offset          = -( before as c_int );
		let count           = ( before + after ) as c_uint;
		// SAFETY: as in `commit_text`; the arguments match the signal's `(int, uint)`.
		unsafe { g_signal_emit_by_name( context as *mut GObject, b"delete-surrounding\0".as_ptr() as *const c_char, offset, count ); }
	}

	fn im_context( &self ) -> Option<*mut WebKitInputMethodContext>
	{
		let context = self.inner.lock().ok()?.im_context;
		if context.is_null() { None } else { Some( context ) }
	}
}

/// Give `webview` a context bound to `inner`. The view owns it from then on.
pub( crate ) unsafe fn install( webview: *mut WebKitWebView, inner: Weak<Mutex<Inner>> ) -> *mut WebKitInputMethodContext
{
	// SAFETY: a fresh GObject of our registered type; GObject releases the boxed `Weak` with it.
	unsafe
	{
		let context = g_object_new( context_type(), ptr::null::<c_char>() ) as *mut WebKitInputMethodContext;
		if context.is_null() { return ptr::null_mut(); }
		let data = Box::into_raw( Box::new( inner ) ) as gpointer;
		g_object_set_data_full( context as *mut GObject, INNER_KEY.as_ptr() as *const c_char, data, Some( drop_weak ) );
		webkit_web_view_set_input_method_context( webview, context );
		g_object_unref( context as *mut GObject );
		context
	}
}

fn context_type() -> GType
{
	static TYPE: OnceLock<GType> = OnceLock::new();
	// SAFETY: the subclass adds no fields, so both struct sizes are the parent's.
	*TYPE.get_or_init( || unsafe
	{
		g_type_register_static_simple(
			webkit_input_method_context_get_type(),
			b"LtkWebkitInputMethodContext\0".as_ptr() as *const c_char,
			size_of::<WebKitInputMethodContextClass>() as c_uint,
			Some( class_init ),
			size_of::<WebKitInputMethodContext>() as c_uint,
			None,
			0,
		)
	} )
}

unsafe extern "C" fn class_init( class: gpointer, _data: gpointer )
{
	let class = class as *mut WebKitInputMethodContextClass;
	// SAFETY: GObject passes the class struct of the type being initialised.
	unsafe
	{
		( *class ).notify_focus_in  = Some( on_focus_in );
		( *class ).notify_focus_out = Some( on_focus_out );
	}
}

unsafe extern "C" fn on_focus_in( context: *mut WebKitInputMethodContext )
{
	unsafe { set_editable_focused( context, true ); }
}

unsafe extern "C" fn on_focus_out( context: *mut WebKitInputMethodContext )
{
	unsafe { set_editable_focused( context, false ); }
}

unsafe fn set_editable_focused( context: *mut WebKitInputMethodContext, focused: bool )
{
	// SAFETY: the key is NUL-terminated; a missing key yields null.
	let raw = unsafe { g_object_get_data( context as *mut GObject, INNER_KEY.as_ptr() as *const c_char ) };
	if raw.is_null() { return; }
	let Some( inner ) = unsafe { &*( raw as *const Weak<Mutex<Inner>> ) }.upgrade() else { return };
	if let Ok( mut g ) = inner.lock() { g.editable_focused = focused; };
}

unsafe extern "C" fn drop_weak( data: gpointer )
{
	if data.is_null() { return; }
	// SAFETY: produced by `Box::into_raw` in `install`, reclaimed exactly once.
	drop( unsafe { Box::from_raw( data as *mut Weak<Mutex<Inner>> ) } );
}