web browser (Servo)
git clone https://git.lucas.co/cce-browser.git
src/wpe/ime.rs (10.1K)
1 //! Which page text field is open for typing, as WebKit itself reports it.
2 //!
3 //! WebKit tells the embedder about an editable element through an input-method
4 //! context: `focus_in` when one takes focus in a focused view, `focus_out` when
5 //! it loses it, `set_cursor_area` as its caret moves. It asks the *display* for
6 //! that context (`create_input_method_context`), so supplying our own is all it
7 //! takes to hear about a field in any page, frame or shadow root — `<input>`,
8 //! `<textarea>` and `contenteditable` alike — with no script injected.
9 //!
10 //! The browser claims the field from `display_list` with it
11 //! (`cce_ui::text_input::claim`), which is what raises the compositor's
12 //! on-screen keyboard after a tap, exactly as it does for the chrome's own
13 //! fields.
14 //!
15 //! Only the announcements are taken. Every other vfunc is left to the base
16 //! class, so key events are not filtered and reach the page as they did before
17 //! this context existed.
18 //!
19 //! The one thing the context does not say is `inputmode="none"` — the page
20 //! draws its own keyboard, or wants none. WebKit has a hint for it
21 //! (`INHIBIT_OSK`) but 2.52 never sets it (the field arrives as plain
22 //! `SPELLCHECK`, measured), so a small watcher in every frame reports it
23 //! instead ([`WATCH_JS`]), and a freshly opened field is not claimed until
24 //! that report is in or [`REPORT_WAIT`] has passed. Claimed at once, the
25 //! board would be up before the page could say it wants none.
26
27 use std::cell::{Cell, RefCell};
28 use std::ffi::c_void;
29 use std::time::{Duration, Instant};
30
31 use super::ffi::*;
32
33 /// A page field with focus: its caret in the view's logical px, once WebKit
34 /// has said where it is.
35 #[derive(Clone, Copy, Default)]
36 struct Field {
37 view: usize,
38 focused: bool,
39 caret: Option<(i32, i32, i32, i32)>,
40 /// When WebKit said this field opened.
41 since: Option<Instant>,
42 }
43
44 /// The script world the `inputmode` watcher runs in: page script cannot see
45 /// it, nor post on its channel.
46 const WORLD: &str = "cce-ime";
47 /// The channel it reports on.
48 const CHANNEL: &str = "cceIme";
49
50 /// Runs in every frame, and when focus moves reports whether the focused
51 /// element asks for no keyboard: `"none"` or `"text"`. Only the shown tab
52 /// speaks — every tab shares the channel, and a background tab is unmapped,
53 /// so hidden. Visibility rather than `document.hasFocus()`, which also
54 /// needs the window to hold the keyboard: WebKit opens a field and the board
55 /// can follow a tap without that (a seat with no keyboard device). A frame
56 /// whose focus is inside a child frame leaves it to the child, which runs
57 /// its own copy, so a field in a cross-origin frame is reported too.
58 const WATCH_JS: &str = r#"(() => {
59 const h = window.webkit && window.webkit.messageHandlers;
60 if (!h || !h.cceIme) return;
61 // After the event, not in it: mid-move, activeElement is still the old one.
62 document.addEventListener('focusin', () => setTimeout(() => {
63 if (document.visibilityState !== 'visible') return;
64 let el = document.activeElement;
65 while (el && el.shadowRoot && el.shadowRoot.activeElement) el = el.shadowRoot.activeElement;
66 if (!el || el.tagName === 'IFRAME' || el.tagName === 'FRAME') return;
67 const none = (el.inputMode || '').toLowerCase() === 'none';
68 try { h.cceIme.postMessage(none ? 'none' : 'text'); } catch (e) {}
69 }, 0), true);
70 })();"#;
71
72 /// How long a freshly opened field waits for the watcher before it is
73 /// claimed anyway. The report comes from the same focus change as WebKit's
74 /// `focus_in` and lands within a few ms of it, either side; this only
75 /// bounds a page the watcher cannot run in.
76 const REPORT_WAIT: Duration = Duration::from_millis(100);
77
78 /// A report this much older than WebKit's `focus_in` is about an earlier
79 /// field, not this one.
80 const REPORT_EARLY: Duration = Duration::from_millis(300);
81
82 thread_local! {
83 /// Per context, keyed by its pointer. A context is one view's for life.
84 static FIELDS: RefCell<Vec<(usize, Field)>> = const { RefCell::new(Vec::new()) };
85 /// A field opened, closed or moved since the browser last asked.
86 static CHANGED: Cell<bool> = const { Cell::new(false) };
87 /// The watcher's last report: when, and whether it was `inputmode=none`.
88 static REPORT: Cell<Option<(Instant, bool)>> = const { Cell::new(None) };
89 }
90
91 static mut TYPE: GType = 0;
92 static mut PARENT_FINALIZE: Option<unsafe extern "C" fn(*mut GObject)> = None;
93
94 unsafe fn context_type() -> GType {
95 if TYPE == 0 {
96 TYPE = super::subclass::register_subclass(
97 wpe_input_method_context_get_type(),
98 "CceWpeInputMethodContext",
99 class_init,
100 );
101 }
102 TYPE
103 }
104
105 fn update(ctx: *mut WPEInputMethodContext, f: impl FnOnce(&mut Field)) {
106 let view = unsafe { wpe_input_method_context_get_view(ctx) } as usize;
107 FIELDS.with(|m| {
108 let mut m = m.borrow_mut();
109 let at = match m.iter().position(|(c, _)| *c == ctx as usize) {
110 Some(at) => at,
111 None => {
112 m.push((ctx as usize, Field { view, ..Field::default() }));
113 m.len() - 1
114 }
115 };
116 f(&mut m[at].1);
117 });
118 CHANGED.set(true);
119 }
120
121 unsafe extern "C" fn focus_in(ctx: *mut WPEInputMethodContext) {
122 log::debug!("page field: focus in");
123 update(ctx, |f| {
124 f.focused = true;
125 f.since = Some(Instant::now());
126 });
127 // Wake the loop when the wait for the watcher runs out: an idle page
128 // sends nothing else that would.
129 g_timeout_add_once(REPORT_WAIT.as_millis() as u32 + 10, Some(on_report_due), std::ptr::null_mut());
130 }
131
132 unsafe extern "C" fn on_report_due(_data: gpointer) {
133 CHANGED.set(true);
134 }
135
136 unsafe extern "C" fn focus_out(ctx: *mut WPEInputMethodContext) {
137 log::debug!("page field: focus out");
138 update(ctx, |f| {
139 f.focused = false;
140 f.caret = None;
141 });
142 }
143
144 unsafe extern "C" fn set_cursor_area(
145 ctx: *mut WPEInputMethodContext,
146 x: i32,
147 y: i32,
148 w: i32,
149 h: i32,
150 ) {
151 update(ctx, |f| f.caret = Some((x, y, w, h)));
152 }
153
154 unsafe extern "C" fn finalize(obj: *mut GObject) {
155 FIELDS.with(|m| m.borrow_mut().retain(|(c, _)| *c != obj as usize));
156 CHANGED.set(true);
157 if let Some(parent) = PARENT_FINALIZE {
158 parent(obj);
159 }
160 }
161
162 unsafe extern "C" fn class_init(class: *mut c_void, _data: *mut c_void) {
163 let c = class as *mut WPEInputMethodContextClass;
164 (*c).focus_in = Some(focus_in);
165 (*c).focus_out = Some(focus_out);
166 (*c).set_cursor_area = Some(set_cursor_area);
167 PARENT_FINALIZE = (*c).parent_class.finalize;
168 (*c).parent_class.finalize = Some(finalize);
169 }
170
171 /// `WPEDisplayClass::create_input_method_context`.
172 pub(super) unsafe extern "C" fn create_context(
173 _d: *mut WPEDisplay,
174 view: *mut WPEView,
175 ) -> *mut WPEInputMethodContext {
176 let prop = std::ffi::CString::new("view").unwrap();
177 g_object_new(context_type(), prop.as_ptr(), view, std::ptr::null::<std::ffi::c_char>())
178 as *mut WPEInputMethodContext
179 }
180
181 /// The open field in `view`, if one is: its caret's rectangle in the view's
182 /// logical px, or `None` inside when WebKit has not placed the caret yet.
183 ///
184 /// A field that asks for no keyboard is not reported: `inputmode="none"`,
185 /// by the watcher's last report or WebKit's `INHIBIT_OSK` hint, and a field
186 /// just opened is held back until the watcher has spoken (see the module
187 /// notes). Both are read now rather than at `focus_in`, because moving from
188 /// one field to the next says no focus in or out — only a new caret, and a
189 /// new report.
190 pub(super) fn field(view: *mut WPEView) -> Option<Option<(i32, i32, i32, i32)>> {
191 let (ctx, field) = FIELDS.with(|m| {
192 m.borrow().iter().find(|(_, f)| f.view == view as usize && f.focused).copied()
193 })?;
194 let hints = unsafe { wpe_input_method_context_get_input_hints(ctx as *mut WPEInputMethodContext) };
195 if hints & WPEInputHints::WPE_INPUT_HINT_INHIBIT_OSK != 0 {
196 return None;
197 }
198 let since = field.since.unwrap_or_else(Instant::now);
199 match REPORT.get() {
200 Some((at, none)) if at + REPORT_EARLY >= since => {
201 if none {
202 return None;
203 }
204 }
205 _ if since.elapsed() < REPORT_WAIT => return None,
206 _ => {}
207 }
208 Some(field.caret)
209 }
210
211 /// Install the `inputmode` watcher on the content manager every tab is
212 /// built against, with its channel. Once per host, for its whole life.
213 pub(super) unsafe fn install_watch(ucm: *mut WebKitUserContentManager) {
214 let (name, world) = (cstr(CHANNEL), cstr(WORLD));
215 if webkit_user_content_manager_register_script_message_handler(ucm, name.as_ptr(), world.as_ptr()) == 0 {
216 log::warn!("could not register the inputmode channel");
217 return;
218 }
219 let signal = cstr(&format!("script-message-received::{CHANNEL}"));
220 g_signal_connect_data(
221 ucm as *mut _,
222 signal.as_ptr(),
223 Some(std::mem::transmute::<usize, unsafe extern "C" fn()>(on_report as *const () as usize)),
224 std::ptr::null_mut(),
225 None,
226 0,
227 );
228 let source = cstr(WATCH_JS);
229 // Every frame, from document start: a field is often in a frame, and a
230 // page can focus one before its own load finishes.
231 let script = webkit_user_script_new_for_world(
232 source.as_ptr(),
233 WebKitUserContentInjectedFrames::WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES,
234 WebKitUserScriptInjectionTime::WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START,
235 world.as_ptr(),
236 std::ptr::null(),
237 std::ptr::null(),
238 );
239 webkit_user_content_manager_add_script(ucm, script);
240 webkit_user_script_unref(script);
241 }
242
243 unsafe extern "C" fn on_report(_ucm: *mut WebKitUserContentManager, value: *mut JSCValue, _data: gpointer) {
244 let raw = jsc_value_to_string(value);
245 if raw.is_null() {
246 return;
247 }
248 let none = std::ffi::CStr::from_ptr(raw).to_bytes() == b"none";
249 g_free(raw as *mut _);
250 log::debug!("page field: inputmode {}", if none { "none" } else { "text" });
251 REPORT.set(Some((Instant::now(), none)));
252 CHANGED.set(true);
253 }
254
255 fn cstr(s: &str) -> std::ffi::CString {
256 std::ffi::CString::new(s).expect("no NUL")
257 }
258
259 /// Whether any field opened, closed or moved since the last call.
260 pub(super) fn take_changed() -> bool {
261 CHANGED.replace(false)
262 }