git.lucas.co / cce-browser
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 }