web browser (Servo)
git clone https://git.lucas.co/cce-browser.git
src/wpe/host.rs (114.3K)
1 //! `WebKitHost` — the WPE-backed twin of `webview.rs`'s `ServoHost`.
2 //!
3 //! Deliberately mirrors that type's method surface so `main.rs` can switch
4 //! engines by changing a type name rather than its logic. Frames land in
5 //! cce-ui's image registry exactly as before, so `display_list` is unchanged:
6 //! the page is still one full-bleed quad.
7 //!
8 //! **Loop integration is the one real difference.** Servo had an
9 //! `EventLoopWaker` that pushed `Message::Spin` into calloop from its own
10 //! threads; WPE runs on a GLib `GMainContext`. [`WebKitHost::pump`] therefore
11 //! drains that context non-blockingly, which keeps the same shape as
12 //! `ServoHost::pump` but means *something has to call it*. Today that is the
13 //! app's `tick`. The correct fix is to put the context's pollfds into calloop
14 //! so the app wakes only when GLib has work — see WPE-PORT.md; doing it by
15 //! polling first keeps this milestone about the engine, not the event loop.
16
17 use std::cell::{Cell, RefCell};
18 use std::ffi::{c_char, c_void, CString};
19 use std::rc::Rc;
20
21 use url::Url;
22
23 use cce_ui::widget::{KeyEvent, MouseButton};
24
25 use super::ffi::*;
26 use super::glib_source::GlibPoll;
27 use super::input;
28 use super::subclass::{types, FRAME_SINK};
29
30 /// Page state a tab's WebKit signals write into.
31 ///
32 /// Held behind an `Rc` because each connected signal owns a reference: the
33 /// closure outlives any borrow we could hand it, and the webview may emit
34 /// after the `Tab` has moved within `tabs` (a `Vec` reallocates).
35 #[derive(Default)]
36 struct TabState {
37 title: RefCell<Option<String>>,
38 url: RefCell<Option<Url>>,
39 loading: Cell<bool>,
40 /// Set by any signal, cleared by `pump`. This is what lets a *background*
41 /// tab report a title change — the old polling only ever looked at the
42 /// active webview.
43 dirty: Cell<bool>,
44 /// The tab's WebProcess died, and why (`WebKitWebProcessTerminationReason`).
45 /// Set by the signal, taken by `pump`, which puts the error page up —
46 /// outside the signal, so the load is not started from inside WebKit's
47 /// own teardown of the process.
48 terminated: Cell<Option<WebKitWebProcessTerminationReason::Type>>,
49 /// WebKit's responsiveness timer gave up on the WebProcess: a message
50 /// has gone ~3s without an answer. Cleared when it answers again.
51 unresponsive: Cell<bool>,
52 /// The person chose to wait on this hang, so it is not asked about again
53 /// until the page recovers and hangs anew.
54 hang_waived: Cell<bool>,
55 /// When the outstanding [`WebKitHost::ping`] went out, if one has not
56 /// been answered yet.
57 ping_since: Cell<Option<std::time::Instant>>,
58 /// The process is being stopped as a deadlock, so its termination should
59 /// reload the page rather than show the error page.
60 auto_reload: Cell<bool>,
61 /// When this tab was last recovered that way.
62 last_auto: Cell<Option<std::time::Instant>>,
63 }
64
65 /// How long every page process must sit idle while the active tab is
66 /// unresponsive before the hang is taken for a deadlock and recovered
67 /// without asking.
68 const DEADLOCK_WATCH: std::time::Duration = std::time::Duration::from_secs(5);
69
70 /// A tab is recovered automatically at most this often. A page that
71 /// deadlocks on every load is left to the prompt instead of reloading in a
72 /// loop.
73 const AUTO_RECOVER_GAP: std::time::Duration = std::time::Duration::from_secs(120);
74
75 /// A deadlock in progress: when the watch began, on which tab, and every
76 /// page process's CPU time at that moment.
77 struct DeadlockWatch {
78 tab: Rc<TabState>,
79 since: std::time::Instant,
80 ticks: std::collections::HashMap<i32, u64>,
81 }
82
83 /// CPU time (user + system, in `/proc` clock ticks) of every WPEWebProcess
84 /// below this one — they sit under bubblewrap, so not as direct children.
85 ///
86 /// WebKit has no API that says which process serves which tab, so the
87 /// watch reads all of them. That is what makes it conservative: one busy
88 /// process anywhere is enough to leave the hang to the prompt.
89 fn web_process_ticks() -> std::collections::HashMap<i32, u64> {
90 let me = std::process::id() as i32;
91 let stat = |pid: i32| std::fs::read_to_string(format!("/proc/{pid}/stat")).ok();
92 // The fields after the parenthesized command name, which may hold spaces.
93 let fields = |s: &str| -> Vec<String> {
94 s.rsplit_once(')').map_or_else(Vec::new, |(_, rest)| {
95 rest.split_whitespace().map(str::to_string).collect()
96 })
97 };
98 let mut out = std::collections::HashMap::new();
99 for entry in std::fs::read_dir("/proc").into_iter().flatten().flatten() {
100 let Ok(pid) = entry.file_name().to_string_lossy().parse::<i32>() else { continue };
101 let comm = std::fs::read_to_string(format!("/proc/{pid}/comm")).unwrap_or_default();
102 if comm.trim() != "WPEWebProcess" {
103 continue;
104 }
105 let Some(s) = stat(pid) else { continue };
106 let f = fields(&s);
107 let mut parent = f.get(1).and_then(|p| p.parse::<i32>().ok());
108 let mut ours = false;
109 for _ in 0..4 {
110 match parent {
111 Some(p) if p == me => {
112 ours = true;
113 break;
114 }
115 Some(p) if p > 1 => {
116 parent = stat(p).and_then(|s| fields(&s).get(1).and_then(|p| p.parse().ok()));
117 }
118 _ => break,
119 }
120 }
121 // utime and stime are fields 14 and 15; `f` starts at field 3.
122 let ticks = |i: usize| f.get(i).and_then(|t| t.parse::<u64>().ok()).unwrap_or(0);
123 if ours {
124 out.insert(pid, ticks(11) + ticks(12));
125 }
126 }
127 out
128 }
129
130 /// How long a page may leave a ping unanswered before it counts as hung —
131 /// WebKit's own responsiveness timeout.
132 const HANG_GRACE: std::time::Duration = std::time::Duration::from_secs(3);
133
134 /// The world the ping runs in, so the page never sees it.
135 const PING_WORLD: &str = "cce-ping";
136
137 /// One tab: its webview plus the app-visible page state and the last frame
138 /// uploaded to the image registry (id, w px, h px). Same shape as
139 /// `webview::Tab` so the chrome reads it identically.
140 pub struct Tab {
141 webview: *mut WebKitWebView,
142 view: *mut WPEView,
143 state: Rc<TabState>,
144 pub title: Option<String>,
145 pub url: Option<Url>,
146 pub loading: bool,
147 image: Option<(u32, u32, u32)>,
148 /// Under `CCE_BROWSER_DAMAGE_CHECK` only: the picture `image` should
149 /// hold, patched region by region alongside it.
150 mirror: Option<Vec<u8>>,
151 }
152
153 impl Drop for Tab {
154 fn drop(&mut self) {
155 // Unref the webview *first*: destroying it runs the closures'
156 // destroy-notify, which releases their `Rc<TabState>` refs. Dropping
157 // the state before the object that can still emit into it would be a
158 // use-after-free.
159 unsafe { g_object_unref(self.webview as *mut _) };
160 if let Some((id, ..)) = self.image {
161 cce_ui::vk::free_image(id);
162 }
163 }
164 }
165
166 /// `notify::` handler shared by title / uri / is-loading: read the property
167 /// straight back off the emitting webview and stash it.
168 unsafe extern "C" fn on_notify(
169 obj: *mut GObject,
170 _pspec: *mut GParamSpec,
171 data: gpointer,
172 ) {
173 let st = &*(data as *const TabState);
174 let wv = obj as *mut WebKitWebView;
175 *st.title.borrow_mut() = from_cstr(webkit_web_view_get_title(wv));
176 if let Some(u) = from_cstr(webkit_web_view_get_uri(wv)).and_then(|u| Url::parse(&u).ok()) {
177 *st.url.borrow_mut() = Some(u);
178 }
179 st.loading.set(webkit_web_view_is_loading(wv) != 0);
180 st.dirty.set(true);
181 }
182
183 /// Releases the `Rc` ref a connection owned, when the closure is destroyed.
184 unsafe extern "C" fn drop_state_ref(data: gpointer, _closure: *mut GClosure) {
185 drop(Rc::from_raw(data as *const TabState));
186 }
187
188 unsafe fn connect_notify(wv: *mut WebKitWebView, signal: &str, state: &Rc<TabState>) {
189 connect_state(wv, signal, on_notify as *const () as usize, state);
190 }
191
192 /// Connect `cb` with a `TabState` as its data.
193 unsafe fn connect_state(wv: *mut WebKitWebView, signal: &str, cb: usize, state: &Rc<TabState>) {
194 let name = cstr(signal);
195 // Each connection owns its own ref, handed back by `drop_state_ref`.
196 let raw = Rc::into_raw(state.clone()) as gpointer;
197 g_signal_connect_data(
198 wv as *mut _,
199 name.as_ptr(),
200 Some(std::mem::transmute::<usize, unsafe extern "C" fn()>(cb)),
201 raw,
202 Some(drop_state_ref),
203 0,
204 );
205 }
206
207 /// The tab's WebProcess is gone — crashed, killed for memory, or stopped by
208 /// [`WebKitHost::stop_unresponsive`]. Without this the tab just froze on its
209 /// last frame, with nothing saying the page behind it no longer existed.
210 unsafe extern "C" fn on_terminated(
211 _wv: *mut WebKitWebView,
212 reason: WebKitWebProcessTerminationReason::Type,
213 data: gpointer,
214 ) {
215 let st = &*(data as *const TabState);
216 st.terminated.set(Some(reason));
217 st.unresponsive.set(false);
218 st.hang_waived.set(false);
219 st.ping_since.set(None);
220 st.dirty.set(true);
221 }
222
223 /// The ping came back — or failed because the process is gone, which the
224 /// termination signal reports on its own.
225 unsafe extern "C" fn on_ping(source: *mut GObject, res: *mut GAsyncResult, data: gpointer) {
226 let st = Rc::from_raw(data as *const TabState);
227 let mut err: *mut GError = std::ptr::null_mut();
228 let v = webkit_web_view_evaluate_javascript_finish(source as *mut WebKitWebView, res, &mut err);
229 if !v.is_null() {
230 g_object_unref(v as *mut _);
231 }
232 if !err.is_null() {
233 g_error_free(err);
234 }
235 st.ping_since.set(None);
236 if !st.unresponsive.get() {
237 st.hang_waived.set(false);
238 }
239 }
240
241 /// Fires once the grace has run out. It does nothing itself: being a GLib
242 /// source is what wakes the loop, and the `pump` that follows is where an
243 /// unanswered ping is noticed. Without it, a hung page — which sends nothing
244 /// — would leave the loop asleep and the question unasked.
245 unsafe extern "C" fn on_ping_due(_data: gpointer) {}
246
247 unsafe extern "C" fn on_responsive(obj: *mut GObject, _pspec: *mut GParamSpec, data: gpointer) {
248 let st = &*(data as *const TabState);
249 let responsive = webkit_web_view_get_is_web_process_responsive(obj as *mut WebKitWebView) != 0;
250 st.unresponsive.set(!responsive);
251 if responsive {
252 st.hang_waived.set(false);
253 }
254 }
255
256 /// The page shown in place of one whose WebProcess died. Loaded as
257 /// *alternate* HTML for the dead page's own URL, so the URL bar, the saved
258 /// session and Reload all still mean the real page.
259 fn terminated_page(url: Option<&Url>, reason: WebKitWebProcessTerminationReason::Type) -> String {
260 use crate::pages::{html_escape, page};
261 use WebKitWebProcessTerminationReason::*;
262 let (title, why) = match reason {
263 WEBKIT_WEB_PROCESS_EXCEEDED_MEMORY_LIMIT => (
264 "This page ran out of memory",
265 "Its renderer used more memory than it is allowed and was stopped.",
266 ),
267 WEBKIT_WEB_PROCESS_TERMINATED_BY_API => (
268 "This page was stopped",
269 "Its renderer had stopped responding, and was shut down.",
270 ),
271 _ => ("This page crashed", "Its renderer exited unexpectedly."),
272 };
273 let meta = match url {
274 Some(u) => {
275 let u = html_escape(u.as_str());
276 format!("{u}<a href=\"{u}\">Reload</a>")
277 }
278 None => String::new(),
279 };
280 page(title, &meta, &format!("<p class=empty>{why}</p>"), "")
281 }
282
283 /// The frame handed over by `render_buffer`, drained by `pump`. A slot, not a
284 /// queue: only the newest frame is ever shown, and the engine will not run far
285 /// ahead of a browser that has not released the one it is holding.
286 /// Counters behind `CCE_BROWSER_FRAME_DEBUG=1`: how many frames the engine
287 /// finished against how many were actually read back. The gap between them is
288 /// what pacing saves, and it is invisible from the outside — a browser that
289 /// skips nine frames in ten looks exactly like one that copies all ten.
290 #[derive(Default)]
291 struct FrameCounts {
292 produced: u64,
293 read: u64,
294 /// Of `read`, how many copied only their damage.
295 partial: u64,
296 /// Bytes copied out of the engine's buffers.
297 bytes: u64,
298 }
299
300 /// Read whole frames only, ignoring damage. The escape hatch if a page is
301 /// ever drawn stale; `CCE_BROWSER_DAMAGE_CHECK` is how to find out.
302 fn full_frames() -> bool {
303 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
304 *ON.get_or_init(|| std::env::var_os("CCE_BROWSER_FULL_FRAMES").is_some())
305 }
306
307 /// Check every region read against the whole frame: each tab keeps a CPU
308 /// copy of its picture, patched exactly as its image is, and any pixel that
309 /// disagrees with the engine's buffer is logged. Costs a full copy and a
310 /// compare per frame, so it is a test switch, not a mode.
311 fn damage_check() -> bool {
312 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
313 *ON.get_or_init(|| std::env::var_os("CCE_BROWSER_DAMAGE_CHECK").is_some())
314 }
315
316 fn frame_debug() -> bool {
317 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
318 *ON.get_or_init(|| std::env::var_os("CCE_BROWSER_FRAME_DEBUG").is_some())
319 }
320
321 #[derive(Default)]
322 struct Pending {
323 /// The newest finished buffer the engine has handed over, still unread.
324 /// Read and released at the next `pump`; superseded by a newer one, which
325 /// hands this one back **unread** — that skipped copy is the whole point
326 /// of holding it rather than copying in the callback.
327 held: Option<(*mut WPEView, *mut WPEBuffer)>,
328 /// Per view, what its frames changed since the last one that was read —
329 /// including every frame handed back unread in between, whose changes
330 /// the next readback still has to carry. No entry: nothing changed.
331 damage: std::collections::HashMap<usize, super::damage::Damage>,
332 counts: FrameCounts,
333 /// When the counters were last reported.
334 reported: Option<std::time::Instant>,
335 }
336
337 pub struct WebKitHost {
338 display: *mut WPEDisplay,
339 toplevel: *mut WPEToplevel,
340 tabs: Vec<Tab>,
341 active: usize,
342 size_px: (u32, u32),
343 scale: f32,
344 pending: Rc<std::cell::RefCell<Pending>>,
345 /// GLib's pollfd set, mirrored into one epoll fd for calloop.
346 poll: Option<GlibPoll>,
347 /// Shared with the `cce:` pages, exactly as `ServoHost` holds them —
348 /// bookmarks and history are app state, not engine state, so they cross
349 /// the backend swap unchanged.
350 history: std::sync::Arc<crate::pages::History>,
351 bookmarks: std::sync::Arc<crate::pages::Bookmarks>,
352 favorites: std::sync::Arc<crate::pages::Favorites>,
353 history_enabled: bool,
354 force_dark: bool,
355 /// Serves the `cce:` pages. Boxed and leaked into the scheme callback,
356 /// so it must outlive every webview.
357 protocol: Rc<crate::pages::CceProtocol>,
358 downloads: std::sync::Arc<crate::downloads::Downloads>,
359 clear_cookies: std::sync::Arc<std::sync::atomic::AtomicBool>,
360 session: *mut WebKitNetworkSession,
361 download_started: Rc<Cell<bool>>,
362 /// A page asked something and is blocked until we answer.
363 prompts: Rc<RefCell<Prompts>>,
364 /// A frame has been uploaded that nothing has drawn yet.
365 ///
366 /// The readback is paced by this: while it is set, a finished buffer is
367 /// left *held* instead of being copied over a picture nobody saw — and
368 /// since a held buffer is not yet acknowledged, the engine waits on it
369 /// too (see `frame_drawn`).
370 pending_draw: Cell<bool>,
371 /// The injected account watcher, kept so the setting can take it away
372 /// again. `None` when account autocomplete is off, which is also when no
373 /// page carries the script at all.
374 watcher: Option<*mut WebKitUserScript>,
375 /// Retained only so tests can assert on rendered output; the registry
376 /// owns the copy that actually gets drawn.
377 /// Top-left pixel of the last frame — three bytes, not the frame.
378 last_pixel: Option<(u8, u8, u8)>,
379 /// Installed on every webview when force-dark is on.
380 ucm: *mut WebKitUserContentManager,
381 /// A pre-built hidden webview parked on about:blank, WebProcess already
382 /// spawned. `open_tab` adopts it and pays only the navigation — measured
383 /// at ~65ms to a live internal page against ~250ms building from scratch
384 /// (~200ms of which is webview creation + process spawn). The price is
385 /// one idle WebProcess held per window. Theme changes reach it anyway:
386 /// the colour scheme is display-level and force-dark lives in the shared
387 /// user-content-manager it was built with.
388 spare: Option<(*mut WebKitWebView, *mut WPEView, Rc<TabState>)>,
389 /// Whether the window holds keyboard focus, as last told by [`Self::focus`].
390 /// Kept so a tab made active later inherits it: focus belongs to the view,
391 /// and only the active tab's view should have it.
392 window_focused: bool,
393 /// The pointer buttons the page is holding, as `WPE_MODIFIER_POINTER_*`
394 /// bits, stamped on every pointer event.
395 ///
396 /// WebKit reads a drag off the *move* event's own modifiers, not off the
397 /// press it saw earlier: a move reporting no held button is a hover, so
398 /// press-drag-release over text selected nothing (and dragged nothing)
399 /// while every move went out with an empty mask.
400 held_buttons: Cell<WPEModifiers::Type>,
401 /// A hang being watched to see whether it is a deadlock.
402 deadlock_watch: Option<DeadlockWatch>,
403 /// The injected vi focus watcher; `None` while vi mode is off, which is
404 /// also when no page carries it.
405 vi_watcher: Option<*mut WebKitUserScript>,
406 }
407
408 unsafe fn cstr(s: &str) -> CString {
409 CString::new(s).expect("no interior nul")
410 }
411
412 impl WebKitHost {
413 /// Boot WPE and open the first tab.
414 ///
415 /// One host per process: the frame sink and the GType registrations are
416 /// process-wide. That matches the app (one browser window per process)
417 /// but is worth knowing before writing a test that builds two.
418 pub fn new(url: Url, size_px: (u32, u32)) -> Self {
419 unsafe {
420 let t = types();
421 let display = g_object_new(t.display, std::ptr::null::<c_char>()) as *mut WPEDisplay;
422 let mut err: *mut GError = std::ptr::null_mut();
423 assert!(
424 wpe_display_connect(display, &mut err) != 0,
425 "wpe_display_connect failed"
426 );
427
428 // Persisted profile. The data directory persists website data
429 // (localStorage, IndexedDB, service workers) on its own, but the
430 // cookie store stays memory-only until it is explicitly given a
431 // file — the set_persistent_storage call below, without which
432 // every launch starts logged out of every site even though the
433 // rest of the profile survives. Same location and the same 0700
434 // reasoning as the Servo backend — the jar holds live sessions.
435 let profile = crate::pages::state_dir().join("profile");
436 let _ = std::fs::create_dir_all(&profile);
437 {
438 use std::os::unix::fs::PermissionsExt;
439 let _ = std::fs::set_permissions(&profile, std::fs::Permissions::from_mode(0o700));
440 }
441 let (data_dir, cache_dir) = (
442 cstr(&profile.to_string_lossy()),
443 cstr(&profile.join("cache").to_string_lossy()),
444 );
445 let session = webkit_network_session_new(data_dir.as_ptr(), cache_dir.as_ptr());
446 let cookie_db = cstr(&profile.join("cookies.sqlite").to_string_lossy());
447 webkit_cookie_manager_set_persistent_storage(
448 webkit_network_session_get_cookie_manager(session),
449 cookie_db.as_ptr(),
450 WebKitCookiePersistentStorage::WEBKIT_COOKIE_PERSISTENT_STORAGE_SQLITE,
451 );
452
453 let history = std::sync::Arc::new(crate::pages::History::load());
454 let bookmarks = std::sync::Arc::new(crate::pages::Bookmarks::load());
455 let favorites = std::sync::Arc::new(crate::pages::Favorites::load());
456 let downloads = std::sync::Arc::new(crate::downloads::Downloads::default());
457 let clear_cookies =
458 std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
459 let protocol = Rc::new(crate::pages::CceProtocol {
460 history: history.clone(),
461 bookmarks: bookmarks.clone(),
462 favorites: favorites.clone(),
463 downloads: downloads.clone(),
464 clear_cookies: clear_cookies.clone(),
465 });
466
467 // The `cce:` scheme, served straight out of the app exactly as the
468 // Servo backend serves it — same routing table, so the pages and
469 // their mutating links behave identically on both engines.
470 let ctx = webkit_web_context_get_default();
471 let scheme = cstr("cce");
472 webkit_web_context_register_uri_scheme(
473 ctx,
474 scheme.as_ptr(),
475 Some(on_cce_request),
476 Rc::into_raw(protocol.clone()) as gpointer,
477 None,
478 );
479
480 let download_started = Rc::new(Cell::new(false));
481
482 // WebKit fetches downloads itself, and decides what *is* one by
483 // content type — so the extension sniff `is_download_url` exists
484 // for is simply not needed here, and neither is the argv/URL-bar
485 // blind spot it created.
486 let ctxs = Rc::new(DownloadCtx {
487 downloads: downloads.clone(),
488 started: download_started.clone(),
489 });
490 let sig = cstr("download-started");
491 g_signal_connect_data(
492 session as *mut _,
493 sig.as_ptr(),
494 Some(std::mem::transmute::<_, unsafe extern "C" fn()>(
495 on_download_started
496 as unsafe extern "C" fn(*mut GObject, *mut WebKitDownload, gpointer),
497 )),
498 Rc::into_raw(ctxs) as gpointer,
499 None,
500 0,
501 );
502
503 let prompts = Rc::new(RefCell::new(Prompts::default()));
504 let pending = Rc::new(std::cell::RefCell::new(Pending::default()));
505 let sink = pending.clone();
506 FRAME_SINK = Some(Box::new(move |view: *mut WPEView, buffer: *mut WPEBuffer, damage: &[(i32, i32, i32, i32)]| {
507 let mut slot = sink.borrow_mut();
508 slot.damage
509 .entry(view as usize)
510 .or_insert_with(|| super::damage::Damage::Rects(Vec::new()))
511 .add(damage);
512 // Replace, never accumulate: the newest frame wins. The one it
513 // supersedes goes back to the engine **without being read** —
514 // several frames can be dispatched inside a single pump's
515 // drain, and only the last of them will ever be shown, so the
516 // rest are not worth 35 MB of copying each.
517 // It will never be shown, so it is as done as it will get:
518 // say both halves, or that view composites nothing again.
519 if let Some((old_view, old_buffer)) = slot.held.replace((view, buffer)) {
520 wpe_view_buffer_rendered(old_view, old_buffer);
521 wpe_view_buffer_released(old_view, old_buffer);
522 }
523 if frame_debug() {
524 slot.counts.produced += 1;
525 }
526 true
527 }));
528
529 // `0` is no view limit, and every tab's view must fit: a full
530 // toplevel refuses `wpe_view_set_toplevel` *silently*. At `1`
531 // (the spike's value) only the first tab ever attached, and every
532 // later one ran without the window's scale (rendering at half
533 // resolution on a 2x output) or its ACTIVE state (no text caret).
534 let toplevel = wpe_display_create_toplevel(display, 0);
535 // Scale is 1 until the first `resize` from a real window; the
536 // constructor's size is already logical.
537 wpe_toplevel_resized(toplevel, size_px.0 as i32, size_px.1 as i32);
538
539 let mut host = Self {
540 display,
541 toplevel,
542 tabs: Vec::new(),
543 active: usize::MAX, // sentinel: force activate() to do the work
544 size_px,
545 scale: 1.0,
546 pending,
547 poll: GlibPoll::new()
548 .map_err(|e| log::warn!("no GLib epoll bridge ({e}); pump will poll"))
549 .ok(),
550 history: history.clone(),
551 bookmarks: bookmarks.clone(),
552 favorites,
553 history_enabled: true,
554 force_dark: false,
555 protocol,
556 downloads,
557 clear_cookies,
558 session,
559 download_started,
560 pending_draw: Cell::new(false),
561 prompts,
562 last_pixel: None,
563 ucm: webkit_user_content_manager_new(),
564 watcher: None,
565 spare: None,
566 window_focused: false,
567 held_buttons: Cell::new(0),
568 deadlock_watch: None,
569 vi_watcher: None,
570 };
571 // The account watcher's channel, in its own script world. Both
572 // halves are registered here, once, on the shared content
573 // manager every tab is built against.
574 host.register_account_channel();
575 host.register_vi_channel();
576 // Which page fields want no on-screen keyboard (`ime.rs`).
577 super::ime::install_watch(host.ucm);
578 host.open_tab(url);
579 host
580 }
581 }
582
583 /// Listen for the account watcher's messages, in its private world.
584 ///
585 /// The world is the security boundary: page script cannot post on a
586 /// channel registered for another world, so a message arriving here came
587 /// from the injected watcher and not from the page pretending to be one.
588 fn register_account_channel(&self) {
589 use super::formwatch;
590 unsafe {
591 let name = cstr(formwatch::CHANNEL);
592 let world = cstr(formwatch::WORLD);
593 if webkit_user_content_manager_register_script_message_handler(
594 self.ucm,
595 name.as_ptr(),
596 world.as_ptr(),
597 ) == 0
598 {
599 log::warn!("could not register the account message channel");
600 return;
601 }
602 let signal = cstr(&format!("script-message-received::{}", formwatch::CHANNEL));
603 g_signal_connect_data(
604 self.ucm as *mut _,
605 signal.as_ptr(),
606 Some(std::mem::transmute::<usize, unsafe extern "C" fn()>(
607 on_account_message as *const () as usize,
608 )),
609 Rc::into_raw(self.prompts.clone()) as gpointer,
610 Some(drop_prompts_ref),
611 0,
612 );
613
614 // The fill asks: a channel with a reply, so a credential goes back
615 // to exactly the frame that asked. Same world, same guarantee —
616 // page script cannot ask on it.
617 let name = cstr(formwatch::FILL_CHANNEL);
618 if webkit_user_content_manager_register_script_message_handler_with_reply(
619 self.ucm,
620 name.as_ptr(),
621 world.as_ptr(),
622 ) == 0
623 {
624 log::warn!("could not register the account fill channel");
625 return;
626 }
627 let signal = cstr(&format!(
628 "script-message-with-reply-received::{}",
629 formwatch::FILL_CHANNEL
630 ));
631 g_signal_connect_data(
632 self.ucm as *mut _,
633 signal.as_ptr(),
634 Some(std::mem::transmute::<usize, unsafe extern "C" fn()>(
635 on_fill_ask as *const () as usize,
636 )),
637 Rc::into_raw(self.prompts.clone()) as gpointer,
638 Some(drop_prompts_ref),
639 0,
640 );
641 }
642 }
643
644 /// Install or remove the login-field watcher — the whole page-side
645 /// footprint of the feature, so a browser with accounts turned off
646 /// injects nothing at all.
647 pub fn set_accounts_enabled(&mut self, on: bool) {
648 use super::formwatch;
649 unsafe {
650 match (on, self.watcher.take()) {
651 (true, None) => {
652 let source = cstr(formwatch::WATCH_JS);
653 let world = cstr(formwatch::WORLD);
654 // Every frame — sign-in forms are often a frame of their
655 // own — and at document start, so the listeners are in
656 // place before a login page's own script runs.
657 let script = webkit_user_script_new_for_world(
658 source.as_ptr(),
659 WebKitUserContentInjectedFrames::WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES,
660 WebKitUserScriptInjectionTime::WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START,
661 world.as_ptr(),
662 std::ptr::null(),
663 std::ptr::null(),
664 );
665 webkit_user_content_manager_add_script(self.ucm, script);
666 self.watcher = Some(script);
667 }
668 (false, Some(script)) => {
669 webkit_user_content_manager_remove_script(self.ucm, script);
670 webkit_user_script_unref(script);
671 self.drop_fill_asks();
672 }
673 // Already in the asked-for state; `take` above is why the
674 // enabled case has to put its handle back.
675 (true, Some(script)) => self.watcher = Some(script),
676 (false, None) => {}
677 }
678 }
679 }
680
681 /// The next login-field event the watcher reported.
682 pub fn take_form_event(&self) -> Option<super::formwatch::FormEvent> {
683 self.prompts.borrow_mut().form_events.pop_front()
684 }
685
686 /// Drop anything the watcher reported for a page that is going away, so a
687 /// stale focus cannot open a list over the next one.
688 pub fn clear_form_events(&self) {
689 self.prompts.borrow_mut().form_events.clear();
690 }
691
692 /// Put a picked account into the login fields of the document `frame`.
693 ///
694 /// Answers that document's fill ask with the credential, as data on the
695 /// reply — the watcher fills its own recorded fields. Returns false, and
696 /// sends nothing, when that document has no ask open: it navigated, a
697 /// newer ask displaced it, or the tab was switched away from.
698 pub fn fill_credentials(&self, frame: &str, username: &str, password: &str) -> bool {
699 let reply = {
700 let mut p = self.prompts.borrow_mut();
701 let Some(at) = p.fill_asks.iter().position(|(t, _)| t == frame) else {
702 return false;
703 };
704 p.fill_asks.remove(at).map(|(_, r)| r)
705 };
706 let Some(reply) = reply else { return false };
707 let answer = super::formwatch::fill_reply(username, password);
708 unsafe { answer_fill(reply, Some(&answer)) };
709 true
710 }
711
712 /// Answer every open fill ask with nothing. On a tab switch and on a
713 /// navigation: whatever field asked is no longer the one on screen.
714 pub fn drop_fill_asks(&self) {
715 let asks: Vec<_> = self.prompts.borrow_mut().fill_asks.drain(..).collect();
716 for (_, reply) in asks {
717 unsafe { answer_fill(reply, None) };
718 }
719 }
720
721 // ---- vi mode ----
722
723 /// Listen for the vi focus watcher, in its own private world — the same
724 /// guarantee as the account channel: page script cannot post on it.
725 fn register_vi_channel(&self) {
726 unsafe {
727 let name = cstr(crate::vi::CHANNEL);
728 let world = cstr(crate::vi::WORLD);
729 if webkit_user_content_manager_register_script_message_handler(
730 self.ucm,
731 name.as_ptr(),
732 world.as_ptr(),
733 ) == 0
734 {
735 log::warn!("could not register the vi message channel");
736 return;
737 }
738 let signal = cstr(&format!("script-message-received::{}", crate::vi::CHANNEL));
739 g_signal_connect_data(
740 self.ucm as *mut _,
741 signal.as_ptr(),
742 Some(std::mem::transmute::<usize, unsafe extern "C" fn()>(
743 on_vi_message as *const () as usize,
744 )),
745 Rc::into_raw(self.prompts.clone()) as gpointer,
746 Some(drop_prompts_ref),
747 0,
748 );
749 }
750 }
751
752 /// Install or remove the vi focus watcher. Off, pages carry nothing.
753 pub fn set_vi_enabled(&mut self, on: bool) {
754 unsafe {
755 match (on, self.vi_watcher.take()) {
756 (true, None) => {
757 let source = cstr(&crate::vi::focus_watch_js());
758 let world = cstr(crate::vi::WORLD);
759 // Every frame: the field being clicked into is often in one.
760 let script = webkit_user_script_new_for_world(
761 source.as_ptr(),
762 WebKitUserContentInjectedFrames::WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES,
763 WebKitUserScriptInjectionTime::WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START,
764 world.as_ptr(),
765 std::ptr::null(),
766 std::ptr::null(),
767 );
768 webkit_user_content_manager_add_script(self.ucm, script);
769 self.vi_watcher = Some(script);
770 }
771 (false, Some(script)) => {
772 webkit_user_content_manager_remove_script(self.ucm, script);
773 webkit_user_script_unref(script);
774 }
775 (true, Some(script)) => self.vi_watcher = Some(script),
776 (false, None) => {}
777 }
778 }
779 self.prompts.borrow_mut().vi_focus = None;
780 }
781
782 /// Whether the focused element takes text, if focus moved since last asked.
783 pub fn take_vi_focus(&self) -> Option<bool> {
784 self.prompts.borrow_mut().vi_focus.take()
785 }
786
787 /// The page text field open for typing in the active tab: its caret in
788 /// window logical px, or the whole page until WebKit has placed the
789 /// caret. What `display_list` claims, so a tap on a page field raises
790 /// the on-screen keyboard.
791 ///
792 /// Not gated on `window_focused`: the toolkit enables text input only
793 /// while the compositor has given this surface the text-input focus,
794 /// which already says the same thing — and `window_focused` follows
795 /// `wl_keyboard`, which a seat with no keyboard device never enters.
796 pub fn page_text_field(&self) -> Option<(f32, f32, f32, f32)> {
797 let tab = self.tabs.get(self.active)?;
798 let field = super::ime::field(tab.view)?;
799 let (w, h) = self.logical_size();
800 let (x, y, cw, ch) = field.unwrap_or((0, 0, w, h));
801 Some((x as f32, y as f32, cw.max(1) as f32, ch.max(1) as f32))
802 }
803
804 /// Whether a page field opened, closed or moved since the last call:
805 /// the frame that claims it has to be built.
806 pub fn take_page_text_field_changed(&self) -> bool {
807 super::ime::take_changed()
808 }
809
810 /// Run `script` in the active tab's top frame, in the vi world, and queue
811 /// its result as a string under `tag` for [`Self::take_vi_result`]. A
812 /// failed script answers with an empty string, so a caller waiting on
813 /// it is never left waiting.
814 pub fn vi_eval(&self, script: &str, tag: u32) {
815 let Some(t) = self.tabs.get(self.active) else { return };
816 let ctx = Box::new((self.prompts.clone(), tag));
817 unsafe {
818 let (script, world) = (cstr(script), cstr(crate::vi::WORLD));
819 webkit_web_view_evaluate_javascript(
820 t.webview,
821 script.as_ptr(),
822 -1,
823 world.as_ptr(),
824 std::ptr::null(),
825 std::ptr::null_mut(),
826 Some(on_vi_eval),
827 Box::into_raw(ctx) as gpointer,
828 );
829 }
830 }
831
832 pub fn take_vi_result(&self) -> Option<(u32, String)> {
833 self.prompts.borrow_mut().vi_results.pop_front()
834 }
835
836 fn find_controller(&self) -> Option<*mut WebKitFindController> {
837 let t = self.tabs.get(self.active)?;
838 Some(unsafe { webkit_web_view_get_find_controller(t.webview) })
839 }
840
841 /// Find `text` in the active page, highlighting every match and
842 /// scrolling to the first. Smart case, as qutebrowser does it: case
843 /// matters only when the text has a capital in it.
844 pub fn find(&self, text: &str, backwards: bool) {
845 let Some(fc) = self.find_controller() else { return };
846 use WebKitFindOptions::*;
847 let mut opts = WEBKIT_FIND_OPTIONS_WRAP_AROUND;
848 if !text.chars().any(char::is_uppercase) {
849 opts |= WEBKIT_FIND_OPTIONS_CASE_INSENSITIVE;
850 }
851 if backwards {
852 opts |= WEBKIT_FIND_OPTIONS_BACKWARDS;
853 }
854 let c = unsafe { cstr(text) };
855 unsafe { webkit_find_controller_search(fc, c.as_ptr(), opts, 1000) };
856 }
857
858 /// The next match, in the search's own direction.
859 pub fn find_next(&self) {
860 if let Some(fc) = self.find_controller() {
861 unsafe { webkit_find_controller_search_next(fc) }
862 }
863 }
864
865 pub fn find_prev(&self) {
866 if let Some(fc) = self.find_controller() {
867 unsafe { webkit_find_controller_search_previous(fc) }
868 }
869 }
870
871 /// Drop the search and its highlights.
872 pub fn find_finish(&self) {
873 if let Some(fc) = self.find_controller() {
874 unsafe { webkit_find_controller_search_finish(fc) }
875 }
876 self.prompts.borrow_mut().find_result = None;
877 }
878
879 /// How the last search went: the match count, 0 for none.
880 pub fn take_find_result(&self) -> Option<u32> {
881 self.prompts.borrow_mut().find_result.take()
882 }
883
884 fn build_webview(&self, url: &Url, state: &Rc<TabState>) -> (*mut WebKitWebView, *mut WPEView) {
885 unsafe {
886 let (p_display, p_ucm, p_session) = (
887 cstr("display"),
888 cstr("user-content-manager"),
889 cstr("network-session"),
890 );
891 let wv = g_object_new(
892 webkit_web_view_get_type(),
893 p_display.as_ptr(),
894 self.display,
895 p_ucm.as_ptr(),
896 self.ucm,
897 p_session.as_ptr(),
898 self.session,
899 std::ptr::null::<c_char>(),
900 ) as *mut WebKitWebView;
901 set_features(wv);
902 let view = webkit_web_view_get_wpe_view(wv);
903 wpe_view_set_toplevel(view, self.toplevel);
904 // Signals, not polling: a background tab has to be able to report
905 // its title without anyone asking the active webview.
906 for sig in ["notify::title", "notify::uri", "notify::is-loading"] {
907 connect_notify(wv, sig, state);
908 }
909 // A dead or hung WebProcess. The first gets an error page in
910 // `pump`; the second is offered to the chrome to ask about.
911 connect_state(wv, "web-process-terminated", on_terminated as *const () as usize, state);
912 connect_state(
913 wv,
914 "notify::is-web-process-responsive",
915 on_responsive as *const () as usize,
916 state,
917 );
918 // A page's alert/confirm/prompt, and HTTP auth challenges. Both
919 // are held open and answered later, so the chrome can draw a real
920 // dialog rather than the handler having to decide inline.
921 connect_raw(
922 wv,
923 "script-dialog",
924 on_script_dialog as *const () as usize,
925 &self.prompts,
926 );
927 connect_raw(
928 wv,
929 "authenticate",
930 on_authenticate as *const () as usize,
931 &self.prompts,
932 );
933 // Right-click reaches the page as button 3; if the page does not
934 // preventDefault, WebKit asks for a menu here. Returning TRUE
935 // claims presentation, so the chrome draws it.
936 connect_raw(
937 wv,
938 "context-menu",
939 on_context_menu as *const () as usize,
940 &self.prompts,
941 );
942 // A `<select>` asking to open. WPE draws no popup of its own: a
943 // select nobody answers here simply never opens.
944 connect_raw(
945 wv,
946 "show-option-menu",
947 on_show_option_menu as *const () as usize,
948 &self.prompts,
949 );
950 // A middle-clicked link is a background tab, not a navigation.
951 connect_raw(
952 wv,
953 "decide-policy",
954 on_decide_policy as *const () as usize,
955 &self.prompts,
956 );
957 // Find-in-page answers on the controller, not the view.
958 let fc = webkit_web_view_get_find_controller(wv);
959 for (signal, cb) in [
960 ("found-text", on_found_text as *const () as usize),
961 ("failed-to-find-text", on_failed_to_find as *const () as usize),
962 ] {
963 let name = cstr(signal);
964 g_signal_connect_data(
965 fc as *mut _,
966 name.as_ptr(),
967 Some(std::mem::transmute::<usize, unsafe extern "C" fn()>(cb)),
968 Rc::into_raw(self.prompts.clone()) as gpointer,
969 Some(drop_prompts_ref),
970 0,
971 );
972 }
973 let (lw, lh) = self.logical_size();
974 wpe_view_resized(view, lw, lh);
975 wpe_view_set_visible(view, 1);
976 wpe_view_map(view);
977 let curl = cstr(url.as_str());
978 webkit_web_view_load_uri(wv, curl.as_ptr());
979 (wv, view)
980 }
981 }
982
983 /// Build the hidden spare webview so its WebProcess is up before the
984 /// next `open_tab` needs it.
985 fn prewarm_spare(&mut self) {
986 if self.spare.is_some() {
987 return;
988 }
989 let state = Rc::new(TabState::default());
990 let url = Url::parse("about:blank").expect("about:blank");
991 let (wv, view) = self.build_webview(&url, &state);
992 unsafe {
993 wpe_view_unmap(view);
994 wpe_view_set_visible(view, 0);
995 }
996 self.spare = Some((wv, view, state));
997 }
998
999 pub fn open_tab(&mut self, url: Url) {
1000 self.add_tab(url, true);
1001 }
1002
1003 /// Open `url` in a new tab behind the active one: it loads, and shows
1004 /// up in the strip, but the page on screen and its focus stay put.
1005 pub fn open_background_tab(&mut self, url: Url) {
1006 self.add_tab(url, false);
1007 }
1008
1009 fn add_tab(&mut self, url: Url, show: bool) {
1010 let (webview, view, state) = match self.spare.take() {
1011 // Adopt the prewarmed webview; only the navigation is paid.
1012 Some((wv, view, state)) => {
1013 unsafe {
1014 let curl = cstr(url.as_str());
1015 webkit_web_view_load_uri(wv, curl.as_ptr());
1016 }
1017 (wv, view, state)
1018 }
1019 None => {
1020 let state = Rc::new(TabState::default());
1021 let (wv, view) = self.build_webview(&url, &state);
1022 if !show {
1023 // Built mapped, like every webview; a background one
1024 // starts the way a switched-away tab is left.
1025 unsafe {
1026 wpe_view_unmap(view);
1027 wpe_view_set_visible(view, 0);
1028 }
1029 }
1030 (wv, view, state)
1031 }
1032 };
1033 state.loading.set(true);
1034 *state.url.borrow_mut() = Some(url.clone());
1035 self.tabs.push(Tab {
1036 webview,
1037 view,
1038 state,
1039 title: None,
1040 url: Some(url),
1041 loading: true,
1042 image: None,
1043 mirror: None,
1044 });
1045 if show {
1046 self.activate(self.tabs.len() - 1);
1047 }
1048 // Replace the spare right away, but after the load started, so the
1049 // page fetch runs while this builds — measured, it does not show up
1050 // in the click-to-tab time.
1051 self.prewarm_spare();
1052 }
1053
1054 /// Close a tab. Returns false when that was the last one (the app should
1055 /// exit; the tab is gone either way). Mirrors `ServoHost::close_tab`,
1056 /// including how the next active index is chosen.
1057 pub fn close_tab(&mut self, index: usize) -> bool {
1058 if index >= self.tabs.len() {
1059 return true;
1060 }
1061 let was_active = index == self.active;
1062 let old_active = self.active;
1063 self.close_option_menu();
1064 // Anything still held belongs to a view that may be the one about to
1065 // be destroyed; hand it back while it is still safe to. Losing that
1066 // frame costs a repaint, which the tab change causes anyway.
1067 self.release_held();
1068 // Dropping the Tab unrefs the webview and frees its registry image.
1069 drop(self.tabs.remove(index));
1070 if self.tabs.is_empty() {
1071 return false;
1072 }
1073 let next = if was_active {
1074 index.min(self.tabs.len() - 1)
1075 } else if old_active > index {
1076 old_active - 1
1077 } else {
1078 old_active
1079 };
1080 self.active = usize::MAX; // force activate() to do the work
1081 self.activate(next);
1082 true
1083 }
1084
1085 /// Give back an unread buffer, if one is being held.
1086 fn release_held(&self) {
1087 if let Some((view, buffer)) = self.pending.borrow_mut().held.take() {
1088 unsafe {
1089 wpe_view_buffer_rendered(view, buffer);
1090 wpe_view_buffer_released(view, buffer);
1091 }
1092 }
1093 }
1094
1095 /// Make tab `index` visible and focused. Mirrors `ServoHost::activate`,
1096 /// including the `usize::MAX` sentinel so the first call is not a no-op.
1097 pub fn activate(&mut self, index: usize) {
1098 if index >= self.tabs.len() || index == self.active {
1099 return;
1100 }
1101 unsafe {
1102 if let Some(old) = self.tabs.get(self.active) {
1103 if self.window_focused {
1104 wpe_view_focus_out(old.view);
1105 }
1106 wpe_view_unmap(old.view);
1107 wpe_view_set_visible(old.view, 0);
1108 }
1109 self.active = index;
1110 // A select's list belongs to the page going away.
1111 self.close_option_menu();
1112 // Login fields reported by, and fill asks from, the tab going
1113 // away: a list must not open over the next one, and a pick made
1114 // there must have nowhere to land.
1115 self.clear_form_events();
1116 self.drop_fill_asks();
1117 let tab = &self.tabs[index];
1118 wpe_view_set_toplevel(tab.view, self.toplevel);
1119 wpe_view_set_visible(tab.view, 1);
1120 wpe_view_map(tab.view);
1121 let (lw, lh) = self.logical_size();
1122 wpe_view_resized(tab.view, lw, lh);
1123 if self.window_focused {
1124 wpe_view_focus_in(tab.view);
1125 }
1126 }
1127 }
1128
1129 pub fn tab_count(&self) -> usize {
1130 self.tabs.len()
1131 }
1132 pub fn active_index(&self) -> usize {
1133 self.active
1134 }
1135 pub fn tab(&self, index: usize) -> Option<&Tab> {
1136 self.tabs.get(index)
1137 }
1138 fn active_tab(&self) -> &Tab {
1139 &self.tabs[self.active]
1140 }
1141
1142 /// The epoll fd carrying GLib's pollfd set, for `register_sources`.
1143 /// `None` if the bridge could not be created, in which case the app must
1144 /// fall back to calling [`Self::pump`] on a timer.
1145 pub fn poll_fd(&self) -> Option<std::os::fd::BorrowedFd<'_>> {
1146 self.poll.as_ref().map(|p| p.fd())
1147 }
1148
1149 /// An owned duplicate of [`Self::poll_fd`], for handing to calloop.
1150 ///
1151 /// calloop wants to own what it polls, and the borrow above is tied to
1152 /// `&self`. A dup refers to the same epoll instance, so registrations
1153 /// made through the original are still what this observes.
1154 pub fn poll_fd_owned(&self) -> Option<std::os::fd::OwnedFd> {
1155 let fd = self.poll.as_ref()?.fd();
1156 rustix::io::dup(fd).ok()
1157 }
1158
1159 /// When GLib next needs a pump that no fd will announce, as of the last
1160 /// pump (`GlibPoll::deadline`); `None` when nothing is scheduled.
1161 pub fn glib_deadline(&self) -> Option<std::time::Instant> {
1162 self.poll.as_ref().and_then(|p| p.deadline)
1163 }
1164
1165 /// Whether pumps must keep coming even with GLib quiet: no GLib poll to
1166 /// watch at all, or the deadlock watch timing a hung tab (it only
1167 /// advances when pumped, and a hung tab is exactly the one producing no
1168 /// GLib activity).
1169 pub fn wants_heartbeat(&self) -> bool {
1170 self.poll.is_none() || self.deadlock_watch.is_some()
1171 }
1172
1173 /// Drain GLib's pending work, then upload any frame it produced.
1174 /// Returns (new frame, any state change) like `ServoHost::pump`.
1175 pub fn pump(&mut self) -> (bool, bool) {
1176 // Clear the inner epoll first: calloop is level-triggered on that fd,
1177 // so leaving it readable across a pump that does not consume the
1178 // underlying socket would spin the loop.
1179 if let Some(p) = &self.poll {
1180 p.drain();
1181 }
1182 // `cce://cookies/clear` runs on WebKit's fetch path and cannot reach
1183 // the session from there, so it sets the flag and this acts on it —
1184 // the same relay `ServoHost::pump` uses. Timespan 0 clears them all.
1185 if self
1186 .clear_cookies
1187 .swap(false, std::sync::atomic::Ordering::SeqCst)
1188 {
1189 unsafe {
1190 webkit_website_data_manager_clear(
1191 webkit_network_session_get_website_data_manager(self.session),
1192 WebKitWebsiteDataTypes::WEBKIT_WEBSITE_DATA_COOKIES,
1193 0,
1194 std::ptr::null_mut(),
1195 None,
1196 std::ptr::null_mut(),
1197 );
1198 }
1199 }
1200 unsafe {
1201 while g_main_context_iteration(std::ptr::null_mut(), 0) != 0 {}
1202 }
1203 // WebKit opens and drops sockets as it loads, so the set that matters
1204 // is the one *after* dispatch, not before.
1205 if let Some(p) = &mut self.poll {
1206 p.sync();
1207 }
1208 self.watch_deadlock();
1209 // Nothing has drawn the last frame yet, so reading another would be
1210 // copying over a picture that was never shown. Leave the buffer held
1211 // until the draw. (Its view is waiting on `rendered` meanwhile, so it
1212 // is not followed by another; one from a different view supersedes
1213 // it and hands it back unread.)
1214 if self.pending_draw.get() {
1215 return (false, self.sync_page_state());
1216 }
1217 // One readback per pump, of the newest buffer only: everything the
1218 // engine rendered in between was handed back unread.
1219 let held = self.pending.borrow_mut().held.take();
1220 let dirty = self.sync_page_state();
1221 let Some((view, buffer)) = held else {
1222 return (false, dirty);
1223 };
1224 let damage = self.pending.borrow_mut().damage.remove(&(view as usize));
1225 // The frame belongs to the tab that drew it, which is not always the
1226 // one on screen. A view no tab owns is the prewarmed spare, or a tab
1227 // closed since: nothing shows it.
1228 let Some(index) = self.tabs.iter().position(|t| t.view == view) else {
1229 unsafe {
1230 wpe_view_buffer_rendered(view, buffer);
1231 wpe_view_buffer_released(view, buffer);
1232 }
1233 return (false, dirty);
1234 };
1235 let read = unsafe {
1236 let read = self.read_frame(index, buffer, damage);
1237 // Read now and drawn at the next frame: as good as on screen, so
1238 // the engine may start on the next one while this one waits for
1239 // the draw. That next one is then held, unacknowledged, until
1240 // the draw has happened — which is what keeps the engine to the
1241 // chrome's rate. (Said at the draw instead, the two never
1242 // overlapped, and a Muji banner animating at 60 fps in a visible
1243 // window dropped to 30.)
1244 wpe_view_buffer_rendered(view, buffer);
1245 // The pixels are ours now; the memory can go back.
1246 wpe_view_buffer_released(view, buffer);
1247 read
1248 };
1249 if frame_debug() {
1250 let mut p = self.pending.borrow_mut();
1251 p.counts.read += 1;
1252 if let Some((bytes, partial)) = read {
1253 p.counts.bytes += bytes as u64;
1254 p.counts.partial += partial as u64;
1255 }
1256 let now = std::time::Instant::now();
1257 let due = p.reported.is_none_or(|t| now.duration_since(t).as_secs_f32() >= 1.0);
1258 if due {
1259 p.reported = Some(now);
1260 let c = std::mem::take(&mut p.counts);
1261 log::info!(
1262 "frames: engine produced {}, read back {} ({} handed back unread), \
1263 {} of them only their damage; {:.1} MB copied",
1264 c.produced,
1265 c.read,
1266 c.produced.saturating_sub(c.read),
1267 c.partial,
1268 c.bytes as f64 / 1e6
1269 );
1270 }
1271 }
1272 if read.is_none() || index != self.active {
1273 return (false, true);
1274 }
1275 self.pending_draw.set(true);
1276 (true, true)
1277 }
1278
1279 /// Copy a finished frame into tab `index`'s image: only the damaged
1280 /// regions when the image already holds the frame before them, the
1281 /// whole frame otherwise. Returns the bytes copied and whether it was
1282 /// regions, or `None` for a buffer that could not be read.
1283 unsafe fn read_frame(
1284 &mut self,
1285 index: usize,
1286 buffer: *mut WPEBuffer,
1287 damage: Option<super::damage::Damage>,
1288 ) -> Option<(usize, bool)> {
1289 let shm = ShmFrame::of(buffer)?;
1290 let (w, h) = (shm.width, shm.height);
1291 // The one pixel anything actually reads back (see `sample_pixel`),
1292 // kept instead of a copy of the whole frame. Cloning 35 MB per frame
1293 // to serve a three-byte question cost 7 ms of every frame.
1294 let p0 = std::slice::from_raw_parts(shm.data, 4);
1295 self.last_pixel = Some((p0[2], p0[1], p0[0]));
1296 let tab = &mut self.tabs[index];
1297 // Regions only make sense against the picture they change: this
1298 // tab's image, at this size. No damage at all means nothing changed.
1299 let current = tab.image.is_some_and(|(_, iw, ih)| (iw, ih) == (w, h));
1300 let regions = match damage {
1301 _ if full_frames() || !current => None,
1302 None => Some(Vec::new()),
1303 Some(d) => d.regions(w, h),
1304 };
1305 if let (Some(regions), Some((id, ..))) = (regions, tab.image) {
1306 let len = super::damage::packed_len(®ions);
1307 let mut px = cce_ui::vk::recycle_buffer(len);
1308 super::damage::pack(shm.data, shm.stride, ®ions, &mut px);
1309 if let Some(mirror) = tab.mirror.as_mut() {
1310 check_regions(mirror, &shm, &px, ®ions, index);
1311 }
1312 cce_ui::vk::update_pixel_regions(id, px, w, h, cce_ui::vk::PixelFormat::Bgra, regions);
1313 return Some((len, true));
1314 }
1315 let px = shm.copy_all();
1316 let len = px.len();
1317 if damage_check() {
1318 tab.mirror = Some(px.clone());
1319 }
1320 match tab.image {
1321 // Same tab, same size: replace the contents of the image that is
1322 // already there. No allocation, no descriptor, and above all no
1323 // image freed — freeing one waits for the whole device to go idle,
1324 // which on this path meant once per frame.
1325 Some((id, iw, ih)) if (iw, ih) == (w, h) => {
1326 cce_ui::vk::update_pixels(id, px, w, h, cce_ui::vk::PixelFormat::Bgra);
1327 }
1328 _ => {
1329 let id = cce_ui::vk::upload_pixels(px, w, h, cce_ui::vk::PixelFormat::Bgra);
1330 if let Some((old, ..)) = tab.image.replace((id, w, h)) {
1331 cce_ui::vk::free_image(old);
1332 }
1333 }
1334 }
1335 Some((len, false))
1336 }
1337
1338 /// Re-paint the page into a renderer that has just replaced the one the
1339 /// tab images were uploaded to.
1340 ///
1341 /// An image id belongs to a **renderer**, not to the process: `cce-ui`'s
1342 /// `window_runner` repairs a lost Wayland transport by opening a new
1343 /// session around the same `Application`, which rebuilds the renderer and
1344 /// with it the image table. A draw for an unknown id is skipped rather
1345 /// than reported, so the chrome came back over an empty page.
1346 ///
1347 /// Two halves. Dropping the ids is the easy one. The hard one is that
1348 /// nothing would otherwise provoke a new frame: a page that has finished
1349 /// loading renders once and then only on damage, so `pump` would find no
1350 /// buffer held and the window would sit blank until the user scrolled or
1351 /// navigated. Remapping the active view is the nudge — it is what
1352 /// `activate` already relies on to get a frame out of a tab being
1353 /// switched to.
1354 ///
1355 /// A buffer still held from the old session is deliberately kept: its
1356 /// pixels are fine, and the next `pump` uploads them under a fresh id.
1357 pub fn renderer_replaced(&mut self) {
1358 for tab in &mut self.tabs {
1359 if let Some((id, ..)) = tab.image.take() {
1360 // A free for an id the new renderer never had is a no-op, and
1361 // ids are process-unique, so this cannot reach a live image.
1362 cce_ui::vk::free_image(id);
1363 }
1364 }
1365 unsafe {
1366 let view = self.active_tab().view;
1367 wpe_view_unmap(view);
1368 wpe_view_set_visible(view, 1);
1369 wpe_view_map(view);
1370 let (lw, lh) = self.logical_size();
1371 wpe_view_resized(view, lw, lh);
1372 }
1373 }
1374
1375 /// The chrome drew: whatever was uploaded is on screen, so the next
1376 /// engine frame is worth reading. Called from `display_list`.
1377 ///
1378 /// Returns whether a frame is already waiting to be read. Its view is
1379 /// held up until it is (`pump` says `rendered` as it reads), and having
1380 /// been held up it makes no noise that would turn the loop — so the
1381 /// caller must wake a pump, or the page sits on that frame until GLib's
1382 /// next timeout.
1383 ///
1384 /// This is what paces the engine to the chrome: one frame being drawn,
1385 /// one more finished and waiting, and nothing composited beyond that. So
1386 /// the page runs at the output's refresh while the window is up, at the
1387 /// runner's starvation fallback (~4 a second) with the display off, and
1388 /// not at all where nothing draws. Anything that reads frames without a
1389 /// chrome (the examples) must call this too, or the page stops after its
1390 /// second frame.
1391 pub fn frame_drawn(&self) -> bool {
1392 self.pending_draw.set(false);
1393 self.pending.borrow().held.is_some()
1394 }
1395
1396 /// Fold each tab's signal-written state into the fields the chrome reads.
1397 ///
1398 /// Every tab, not just the active one — that is the whole point of moving
1399 /// off polling. The tab strip shows a title per tab, so a background tab
1400 /// finishing a load has to be visible without switching to it.
1401 fn sync_page_state(&mut self) -> bool {
1402 let mut changed = false;
1403 for tab in &mut self.tabs {
1404 if !tab.state.dirty.replace(false) {
1405 continue;
1406 }
1407 if let Some(reason) = tab.state.terminated.take() {
1408 // Stopped as a deadlock: just load the page again. A plain
1409 // load, not a reload, so a page that came from a form post
1410 // is not posted twice.
1411 let reload = tab.state.auto_reload.take().then_some(tab.url.as_ref()).flatten();
1412 if let Some(u) = reload {
1413 log::warn!("reloading {u} after stopping its deadlocked web process");
1414 unsafe {
1415 let c = cstr(u.as_str());
1416 webkit_web_view_load_uri(tab.webview, c.as_ptr());
1417 }
1418 } else {
1419 // Loading anything respawns a WebProcess; this loads the
1420 // error page under the dead page's URL. Reload — the chrome's
1421 // or the page's link — then fetches the real one.
1422 log::warn!(
1423 "web process for {} terminated (reason {reason})",
1424 tab.url.as_ref().map_or("<no url>", |u| u.as_str())
1425 );
1426 unsafe {
1427 let html = cstr(&terminated_page(tab.url.as_ref(), reason));
1428 let uri = tab.url.as_ref().map(|u| cstr(u.as_str()));
1429 webkit_web_view_load_alternate_html(
1430 tab.webview,
1431 html.as_ptr(),
1432 uri.as_ref().map_or(std::ptr::null(), |u| u.as_ptr()),
1433 // The page's own URL as the base too: without one the
1434 // error page is `about:blank` to itself, so its
1435 // Reload link — refused outright when the dead page
1436 // was a `file:` — had nowhere real to go.
1437 uri.as_ref().map_or(std::ptr::null(), |u| u.as_ptr()),
1438 );
1439 }
1440 }
1441 }
1442 tab.title = tab.state.title.borrow().clone();
1443 if let Some(u) = tab.state.url.borrow().clone() {
1444 tab.url = Some(u);
1445 }
1446 tab.loading = tab.state.loading.get();
1447 changed = true;
1448 }
1449 changed
1450 }
1451
1452 /// Top-left pixel of the last frame, for tests that need to assert on
1453 /// what was actually rendered rather than on what was configured.
1454 pub fn sample_pixel(&self) -> Option<(u8, u8, u8)> {
1455 self.last_pixel
1456 }
1457
1458 pub fn image(&self) -> Option<(u32, u32, u32)> {
1459 self.active_tab().image
1460 }
1461 pub fn title(&self) -> Option<String> {
1462 self.active_tab().title.clone()
1463 }
1464 pub fn url(&self) -> Option<Url> {
1465 self.active_tab().url.clone()
1466 }
1467 pub fn loading(&self) -> bool {
1468 self.active_tab().loading
1469 }
1470
1471 /// The active tab's WebProcess has stopped answering, and the person has
1472 /// not already chosen to wait on it.
1473 pub fn active_unresponsive(&self) -> bool {
1474 self.tabs.get(self.active).is_some_and(|t| self.hung(t) && !t.state.hang_waived.get())
1475 }
1476
1477 /// The tab's process has stopped answering — by WebKit's timer or an
1478 /// overdue ping. Never while a page dialog or auth challenge is up: the
1479 /// process is blocked on *us* then, and a ping sent just before it
1480 /// opened goes unanswered for as long as it stays open.
1481 fn hung(&self, t: &Tab) -> bool {
1482 let p = self.prompts.borrow();
1483 if p.dialog.is_some() || p.auth.is_some() {
1484 return false;
1485 }
1486 let ping_overdue = t.state.ping_since.get().is_some_and(|at| at.elapsed() >= HANG_GRACE);
1487 t.state.unresponsive.get() || ping_overdue
1488 }
1489
1490 /// Recover a deadlocked page without asking.
1491 ///
1492 /// A deadlock and a busy page look alike from here — both stop
1493 /// answering — but a deadlocked process burns no CPU (the 2026-10-05
1494 /// one sat at zero, every thread parked on a lock) and a spinning script
1495 /// burns a whole core. So while the active tab is hung, every page
1496 /// process's CPU time is sampled; if none of them has used more than a
1497 /// sliver of it across `DEADLOCK_WATCH`, the process is stopped and the
1498 /// page loaded again. Anything busier is left to the prompt, as is a
1499 /// hang the person chose to wait on, and a tab recovered within
1500 /// `AUTO_RECOVER_GAP`.
1501 fn watch_deadlock(&mut self) {
1502 let state = self
1503 .tabs
1504 .get(self.active)
1505 .filter(|t| self.hung(t) && !t.state.hang_waived.get())
1506 .map(|t| t.state.clone());
1507 let Some(state) = state else {
1508 self.deadlock_watch = None;
1509 return;
1510 };
1511 if state.last_auto.get().is_some_and(|at| at.elapsed() < AUTO_RECOVER_GAP) {
1512 return;
1513 }
1514 let watching = self.deadlock_watch.as_ref().filter(|w| Rc::ptr_eq(&w.tab, &state));
1515 let Some(watch) = watching else {
1516 self.deadlock_watch = Some(DeadlockWatch {
1517 tab: state,
1518 since: std::time::Instant::now(),
1519 ticks: web_process_ticks(),
1520 });
1521 return;
1522 };
1523 let elapsed = watch.since.elapsed();
1524 if elapsed < DEADLOCK_WATCH {
1525 return;
1526 }
1527 let now = web_process_ticks();
1528 // 5% of one core, at /proc's 100 ticks a second. A process that
1529 // appeared mid-watch is measured from zero, which counts its whole
1530 // startup against it — on the side of not stopping anything.
1531 let budget = (elapsed.as_secs_f64() * 100.0 * 0.05) as u64;
1532 let idle = !now.is_empty()
1533 && now.iter().all(|(pid, t)| {
1534 t.saturating_sub(watch.ticks.get(pid).copied().unwrap_or(0)) <= budget
1535 });
1536 if !idle {
1537 // Busy: a script, most likely. Watch again from here, so a page
1538 // that stops spinning and then deadlocks is still caught.
1539 self.deadlock_watch = Some(DeadlockWatch {
1540 tab: state,
1541 since: std::time::Instant::now(),
1542 ticks: now,
1543 });
1544 return;
1545 }
1546 self.deadlock_watch = None;
1547 log::warn!(
1548 "{} has not answered in {:.0}s and no page process is running; \
1549 stopping it as deadlocked",
1550 self.tabs[self.active].url.as_ref().map_or("<no url>", |u| u.as_str()),
1551 (elapsed + HANG_GRACE).as_secs_f64(),
1552 );
1553 state.auto_reload.set(true);
1554 state.last_auto.set(Some(std::time::Instant::now()));
1555 self.stop_unresponsive();
1556 }
1557
1558 /// A dialog or auth challenge was answered: whatever ping was waiting
1559 /// behind it was waiting on the person, not on a hung page.
1560 fn forget_ping(&self) {
1561 if let Some(t) = self.tabs.get(self.active) {
1562 t.state.ping_since.set(None);
1563 }
1564 }
1565
1566 /// Ask the active page's main thread for an answer, to learn whether it
1567 /// is still there.
1568 ///
1569 /// WebKit's own responsiveness timer misses the commonest hang: pointer
1570 /// events are queued behind an unacknowledged one, and a *move* — which
1571 /// always comes before a click — does not start the timer. So the
1572 /// clicks behind it are never even sent, and the page that ignores them
1573 /// is never reported. A no-op script, in a world the page cannot see,
1574 /// is answered by the same main thread, so its silence is the hang.
1575 fn ping(&self) {
1576 let Some(t) = self.tabs.get(self.active) else { return };
1577 if t.state.ping_since.get().is_some() {
1578 return;
1579 }
1580 t.state.ping_since.set(Some(std::time::Instant::now()));
1581 unsafe {
1582 let (script, world) = (cstr("0"), cstr(PING_WORLD));
1583 webkit_web_view_evaluate_javascript(
1584 t.webview,
1585 script.as_ptr(),
1586 -1,
1587 world.as_ptr(),
1588 std::ptr::null(),
1589 std::ptr::null_mut(),
1590 Some(on_ping),
1591 Rc::into_raw(t.state.clone()) as gpointer,
1592 );
1593 g_timeout_add_once(HANG_GRACE.as_millis() as u32 + 50, Some(on_ping_due), std::ptr::null_mut());
1594 }
1595 }
1596
1597 /// Leave the active tab's hang alone until it recovers.
1598 pub fn wait_unresponsive(&self) {
1599 if let Some(t) = self.tabs.get(self.active) {
1600 t.state.hang_waived.set(true);
1601 }
1602 }
1603
1604 /// Kill the active tab's hung WebProcess. The termination signal follows,
1605 /// and with it the error page offering a reload.
1606 pub fn stop_unresponsive(&self) {
1607 if let Some(t) = self.tabs.get(self.active) {
1608 unsafe { webkit_web_view_terminate_web_process(t.webview) }
1609 }
1610 }
1611
1612 pub fn load(&self, url: Url) {
1613 unsafe {
1614 let c = cstr(url.as_str());
1615 webkit_web_view_load_uri(self.active_tab().webview, c.as_ptr());
1616 }
1617 }
1618 pub fn reload(&self) {
1619 unsafe { webkit_web_view_reload(self.active_tab().webview) }
1620 }
1621 pub fn back(&self) {
1622 unsafe { webkit_web_view_go_back(self.active_tab().webview) }
1623 }
1624 pub fn forward(&self) {
1625 unsafe { webkit_web_view_go_forward(self.active_tab().webview) }
1626 }
1627 pub fn can_go_back(&self) -> bool {
1628 unsafe { webkit_web_view_can_go_back(self.active_tab().webview) != 0 }
1629 }
1630 pub fn can_go_forward(&self) -> bool {
1631 unsafe { webkit_web_view_can_go_forward(self.active_tab().webview) != 0 }
1632 }
1633
1634 // ---- settings and app-side state ----
1635 //
1636 // These exist so `WebKitHost` and `ServoHost` present the same surface;
1637 // bookmarks and history are app state either way, so they are identical.
1638
1639 pub fn set_history_enabled(&mut self, on: bool) {
1640 self.history_enabled = on;
1641 }
1642
1643 /// Install or remove the inverting user stylesheet.
1644 ///
1645 /// Simpler than the Servo path, which needed *two* timed reloads to let
1646 /// a user-content change and a scheme flip settle. WebKit applies user
1647 /// content to live pages, so a reload is enough — and only to re-run
1648 /// pages that already computed their colours.
1649 pub fn set_force_dark(&mut self, on: bool) {
1650 if on == self.force_dark {
1651 return;
1652 }
1653 self.force_dark = on;
1654 unsafe {
1655 if on {
1656 let css = cstr(FORCE_DARK_CSS);
1657 let sheet = webkit_user_style_sheet_new(
1658 css.as_ptr(),
1659 WebKitUserContentInjectedFrames::WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES,
1660 WebKitUserStyleLevel::WEBKIT_USER_STYLE_LEVEL_USER,
1661 std::ptr::null(),
1662 std::ptr::null(),
1663 );
1664 webkit_user_content_manager_add_style_sheet(self.ucm, sheet);
1665 webkit_user_style_sheet_unref(sheet);
1666 } else {
1667 webkit_user_content_manager_remove_all_style_sheets(self.ucm);
1668 }
1669 for tab in &self.tabs {
1670 webkit_web_view_reload(tab.webview);
1671 }
1672 }
1673 }
1674
1675 /// What pages see for `prefers-color-scheme`, via WPE's own setting.
1676 pub fn set_color_scheme_dark(&self, dark: bool) {
1677 unsafe {
1678 let settings = wpe_display_get_settings(self.display);
1679 let key = cstr("/wpe-platform/dark-mode");
1680 let mut err: *mut GError = std::ptr::null_mut();
1681 wpe_settings_set_boolean(
1682 settings,
1683 key.as_ptr(),
1684 dark as gboolean,
1685 WPESettingsSource::WPE_SETTINGS_SOURCE_APPLICATION,
1686 &mut err,
1687 );
1688 }
1689 }
1690
1691 /// A navigation became a download since the last check.
1692 pub fn take_download_started(&self) -> bool {
1693 self.download_started.replace(false)
1694 }
1695
1696 pub fn active_bookmarked(&self) -> bool {
1697 self.active_tab()
1698 .url
1699 .as_ref()
1700 .is_some_and(|u| self.bookmarks.contains(u.as_str()))
1701 }
1702
1703 pub fn toggle_bookmark(&self) {
1704 let tab = self.active_tab();
1705 if let Some(url) = &tab.url {
1706 self.bookmarks
1707 .toggle(url.as_str(), tab.title.as_deref().unwrap_or(""));
1708 }
1709 }
1710
1711 /// The bookmarks store, shared with the `cce://bookmarks` page; the
1712 /// chrome's bookmarks menu lists and edits it directly.
1713 pub fn bookmarks(&self) -> std::sync::Arc<crate::pages::Bookmarks> {
1714 self.bookmarks.clone()
1715 }
1716
1717 /// The favorites store, shared with the `cce://favorites` page; the
1718 /// chrome reads the strip from it.
1719 pub fn favorites(&self) -> std::sync::Arc<crate::pages::Favorites> {
1720 self.favorites.clone()
1721 }
1722
1723 pub fn active_favorited(&self) -> bool {
1724 self.active_tab()
1725 .url
1726 .as_ref()
1727 .is_some_and(|u| self.favorites.contains(u.as_str()))
1728 }
1729
1730 pub fn toggle_favorite(&self) {
1731 let tab = self.active_tab();
1732 if let Some(url) = &tab.url {
1733 self.favorites
1734 .toggle(url.as_str(), tab.title.as_deref().unwrap_or(""));
1735 }
1736 }
1737
1738 /// Clipboard on the page. WebKit takes these as named editing commands,
1739 /// so unlike the Servo backend there is no separate clipboard delegate to
1740 /// implement — it goes through the platform clipboard itself.
1741 /// Push the system selection into WPE. Separated so it can be done
1742 /// ahead of a paste rather than in the same breath — the web process is
1743 /// a different process, and the content has to reach it.
1744 pub fn sync_clipboard(&self) {
1745 unsafe { super::subclass::sync_system_clipboard(self.display) }
1746 }
1747
1748 pub fn editing_action_cmd(&self, command: crate::EditingCommand) {
1749 unsafe {
1750 // WebKit will not read a clipboard it thinks is empty, so the
1751 // system selection has to be pushed in before Paste runs.
1752 if matches!(command, crate::EditingCommand::Paste) {
1753 super::subclass::sync_system_clipboard(self.display);
1754 }
1755 let c = cstr(match command {
1756 crate::EditingCommand::Copy => "Copy",
1757 crate::EditingCommand::Cut => "Cut",
1758 crate::EditingCommand::Paste => "Paste",
1759 });
1760 webkit_web_view_execute_editing_command(self.active_tab().webview, c.as_ptr());
1761 }
1762 }
1763
1764 // ---- pending prompts ----
1765
1766 /// The dialog a page is currently blocked on, if any. Cloned rather than
1767 /// taken: the chrome redraws from this every frame, and the page stays
1768 /// blocked until [`Self::respond_dialog`].
1769 pub fn pending_dialog(&self) -> Option<PendingDialog> {
1770 self.prompts.borrow().dialog.as_ref().map(|(_, d)| d.clone())
1771 }
1772
1773 /// One-shot: the context menu the page just requested, if any. Taken
1774 /// rather than cloned — the chrome opens it once, at the pointer.
1775 pub fn take_context_menu(&self) -> Option<ContextMenuInfo> {
1776 self.prompts.borrow_mut().context_menu.take()
1777 }
1778
1779 /// One-shot: the `<select>` list the page just asked to show, if any.
1780 /// The menu itself stays held until [`Self::pick_option`] or
1781 /// [`Self::close_option_menu`] answers it, or the page closes it.
1782 pub fn take_option_menu(&self) -> Option<OptionMenuInfo> {
1783 self.prompts.borrow_mut().option_menu_new.take()
1784 }
1785
1786 /// Whether a select's list is still waiting on an answer. False once the
1787 /// page has closed it itself — the select was removed, the page
1788 /// navigated — which is how the chrome learns to take its list down.
1789 pub fn option_menu_open(&self) -> bool {
1790 self.prompts.borrow().option_menu.is_some()
1791 }
1792
1793 /// Choose option `index` and close the list. The select changes value
1794 /// and fires its `input`/`change` as for any pick.
1795 pub fn pick_option(&self, index: usize) {
1796 let Some(menu) = self.prompts.borrow_mut().option_menu.take() else { return };
1797 unsafe {
1798 webkit_option_menu_activate_item(menu, index as u32);
1799 webkit_option_menu_close(menu);
1800 g_object_unref(menu as *mut _);
1801 }
1802 }
1803
1804 /// Close the list without choosing. Nothing is selected on the way
1805 /// (`select_item` is never called), so closing leaves the value as it was.
1806 pub fn close_option_menu(&self) {
1807 let menu = {
1808 let mut p = self.prompts.borrow_mut();
1809 p.option_menu_new = None;
1810 p.option_menu.take()
1811 };
1812 // Taken out first: `close` emits the menu's own close signal, whose
1813 // handler borrows the prompts too.
1814 if let Some(menu) = menu {
1815 unsafe {
1816 webkit_option_menu_close(menu);
1817 g_object_unref(menu as *mut _);
1818 }
1819 }
1820 }
1821
1822 /// Links the pages asked to open in background tabs since the last call.
1823 pub fn take_background_opens(&self) -> Vec<Url> {
1824 std::mem::take(&mut self.prompts.borrow_mut().background_opens)
1825 }
1826
1827 /// Fetch `uri` through WebKit's download pipeline — same signals, same
1828 /// store, same `cce://downloads` page as a navigated download. This is
1829 /// what "Download Link/Image" in the context menu dispatches to.
1830 pub fn download_uri(&self, uri: &str) {
1831 unsafe {
1832 let c = cstr(uri);
1833 webkit_web_view_download_uri(self.active_tab().webview, c.as_ptr());
1834 }
1835 }
1836
1837 pub fn pending_auth(&self) -> Option<PendingAuth> {
1838 self.prompts.borrow().auth.as_ref().map(|(_, a)| a.clone())
1839 }
1840
1841 /// Answer the page. `text` carries a `prompt`'s reply; it is ignored for
1842 /// alert and confirm.
1843 pub fn respond_dialog(&self, ok: bool, text: Option<&str>) {
1844 let Some((dialog, pending)) = self.prompts.borrow_mut().dialog.take() else {
1845 return;
1846 };
1847 self.forget_ping();
1848 unsafe {
1849 if pending.prompt_default.is_some() {
1850 // A cancelled prompt must return null, not "" — a page
1851 // distinguishes the two.
1852 if ok {
1853 let t = cstr(text.unwrap_or(""));
1854 webkit_script_dialog_prompt_set_text(dialog, t.as_ptr());
1855 } else {
1856 webkit_script_dialog_prompt_set_text(dialog, std::ptr::null());
1857 }
1858 } else if pending.has_cancel {
1859 webkit_script_dialog_confirm_set_confirmed(dialog, ok as gboolean);
1860 }
1861 webkit_script_dialog_close(dialog);
1862 webkit_script_dialog_unref(dialog);
1863 }
1864 }
1865
1866 /// Answer an auth challenge, or cancel it. Credentials are used for this
1867 /// session only — `WEBKIT_CREDENTIAL_PERSISTENCE_FOR_SESSION` — rather
1868 /// than written to the profile, which would need a deliberate decision
1869 /// about storing passwords on disk.
1870 pub fn respond_auth(&self, credentials: Option<(&str, &str)>) {
1871 let Some((request, _)) = self.prompts.borrow_mut().auth.take() else {
1872 return;
1873 };
1874 self.forget_ping();
1875 unsafe {
1876 match credentials {
1877 Some((user, password)) => {
1878 let (u, p) = (cstr(user), cstr(password));
1879 let cred = webkit_credential_new(
1880 u.as_ptr(),
1881 p.as_ptr(),
1882 WebKitCredentialPersistence::WEBKIT_CREDENTIAL_PERSISTENCE_FOR_SESSION,
1883 );
1884 webkit_authentication_request_authenticate(request, cred);
1885 webkit_credential_free(cred);
1886 }
1887 None => webkit_authentication_request_cancel(request),
1888 }
1889 g_object_unref(request as *mut _);
1890 }
1891 }
1892
1893 // ---- input ----
1894 //
1895 // Coordinates are device pixels relative to the view origin, matching
1896 // `ServoHost`'s convention so `main.rs` scales them the same way. Events
1897 // are refcounted; `wpe_view_event` takes its own reference, so each one is
1898 // unreffed here after delivery.
1899
1900 pub fn mouse_move(&self, x_px: f32, y_px: f32) {
1901 unsafe {
1902 let view = self.active_tab().view;
1903 let (x, y) = self.to_logical(x_px, y_px);
1904 let e = wpe_event_pointer_move_new(
1905 WPEEventType::WPE_EVENT_POINTER_MOVE,
1906 view,
1907 WPEInputSource::WPE_INPUT_SOURCE_MOUSE,
1908 input::now_ms(),
1909 self.held_buttons.get(),
1910 x,
1911 y,
1912 0.0,
1913 0.0,
1914 );
1915 self.send(view, e);
1916 }
1917 }
1918
1919 pub fn mouse_button_ui(&self, button: MouseButton, pressed: bool, x_px: f32, y_px: f32) {
1920 let Some(n) = input::button_number(button) else {
1921 return;
1922 };
1923 if pressed {
1924 self.ping();
1925 }
1926 unsafe {
1927 let view = self.active_tab().view;
1928 let time = input::now_ms();
1929 // WPE tracks double/triple clicks for us; a frozen clock here
1930 // would make every click read as a repeat.
1931 let (x, y) = self.to_logical(x_px, y_px);
1932 let press_count = if pressed {
1933 wpe_view_compute_press_count(view, x, y, n, time)
1934 } else {
1935 0
1936 };
1937 // The mask describes the state *after* this event, which is
1938 // what a DOM `buttons` reads on mousedown and mouseup.
1939 let bit = input::button_modifier(n);
1940 let held = if pressed {
1941 self.held_buttons.get() | bit
1942 } else {
1943 self.held_buttons.get() & !bit
1944 };
1945 self.held_buttons.set(held);
1946 let e = wpe_event_pointer_button_new(
1947 if pressed {
1948 WPEEventType::WPE_EVENT_POINTER_DOWN
1949 } else {
1950 WPEEventType::WPE_EVENT_POINTER_UP
1951 },
1952 view,
1953 WPEInputSource::WPE_INPUT_SOURCE_MOUSE,
1954 time,
1955 held,
1956 n,
1957 x,
1958 y,
1959 press_count,
1960 );
1961 self.send(view, e);
1962 }
1963 }
1964
1965 /// Wheel deltas in device pixels, in cce-ui's winit convention (positive
1966 /// = scroll up), passed through **unchanged**.
1967 ///
1968 /// Measured, not assumed: WPE already inverts on the way to the DOM, so a
1969 /// negation here double-inverts and the page scrolls backwards. An
1970 /// earlier cut negated these and `examples/wpe_input` caught it — the
1971 /// page reported `deltaY` of the wrong sign.
1972 pub fn wheel(&self, dx_px: f64, dy_px: f64, x_px: f32, y_px: f32) {
1973 // cce-ui publishes the gesture phase of the wheel event being
1974 // dispatched: a trackpad's finger lift arrives as a zero delta in
1975 // FingerEnd, which is WebKit's scroll-stop — the signal its own
1976 // kinetic scrolling keys off. Finger phases report the touchpad
1977 // source so the engine treats the deltas as a gesture, not clicks.
1978 let phase = cce_ui::widget::scroll_motion::current_scroll_phase();
1979 let (source, is_stop) = match phase {
1980 cce_ui::widget::ScrollPhase::Wheel => (WPEInputSource::WPE_INPUT_SOURCE_MOUSE, 0),
1981 cce_ui::widget::ScrollPhase::Finger => (WPEInputSource::WPE_INPUT_SOURCE_TOUCHPAD, 0),
1982 cce_ui::widget::ScrollPhase::FingerEnd => (WPEInputSource::WPE_INPUT_SOURCE_TOUCHPAD, 1),
1983 };
1984 unsafe {
1985 let view = self.active_tab().view;
1986 let (x, y) = self.to_logical(x_px, y_px);
1987 let e = wpe_event_scroll_new(
1988 view,
1989 source,
1990 input::now_ms(),
1991 0,
1992 dx_px / self.scale as f64,
1993 dy_px / self.scale as f64,
1994 1, // precise deltas: these are pixels, not notches
1995 is_stop,
1996 x,
1997 y,
1998 );
1999 self.send(view, e);
2000 }
2001 }
2002
2003 /// Takes cce-ui's `KeyEvent` directly — the keysym mapping lives in
2004 /// `input`, so the chrome never learns engine vocabulary.
2005 pub fn key_ui(&self, event: &KeyEvent) {
2006 let Some(keyval) = input::keyval(&event.logical_key) else {
2007 return;
2008 };
2009 let pressed = input::is_pressed(event);
2010 unsafe {
2011 let view = self.active_tab().view;
2012 let e = wpe_event_keyboard_new(
2013 if pressed {
2014 WPEEventType::WPE_EVENT_KEYBOARD_KEY_DOWN
2015 } else {
2016 WPEEventType::WPE_EVENT_KEYBOARD_KEY_UP
2017 },
2018 view,
2019 WPEInputSource::WPE_INPUT_SOURCE_KEYBOARD,
2020 input::now_ms(),
2021 input::modifiers(event.ctrl, event.shift, event.alt),
2022 0, // hardware keycode: unknown to us, and WebKit works off keyval
2023 keyval,
2024 );
2025 self.send(view, e);
2026 }
2027 }
2028
2029 /// Window focus, from the runner's keyboard enter/leave.
2030 ///
2031 /// WebKit needs **two** things before it paints a text caret: the view
2032 /// focused and the toplevel `ACTIVE`. Keystrokes reach a focused field
2033 /// with neither, so the only symptom of missing this is a field you can
2034 /// type into with no caret in it — which shipped, unnoticed, because
2035 /// nothing called this at all. `document.hasFocus()` reads the same pair;
2036 /// `examples/wpe_focus.rs` checks it.
2037 pub fn focus(&mut self, focused: bool) {
2038 self.window_focused = focused;
2039 unsafe {
2040 let state = wpe_toplevel_get_state(self.toplevel);
2041 let state = if focused {
2042 state | WPEToplevelState::WPE_TOPLEVEL_STATE_ACTIVE
2043 } else {
2044 state & !WPEToplevelState::WPE_TOPLEVEL_STATE_ACTIVE
2045 };
2046 wpe_toplevel_state_changed(self.toplevel, state);
2047 if let Some(tab) = self.tabs.get(self.active) {
2048 if focused {
2049 wpe_view_focus_in(tab.view)
2050 } else {
2051 wpe_view_focus_out(tab.view)
2052 }
2053 }
2054 }
2055 }
2056
2057 unsafe fn send(&self, view: *mut WPEView, event: *mut WPEEvent) {
2058 if event.is_null() {
2059 return;
2060 }
2061 wpe_view_event(view, event);
2062 wpe_event_unref(event);
2063 }
2064
2065 /// Resize, in **physical** pixels — `ServoHost`'s convention, so
2066 /// `main.rs` passes `content_px()` to either backend unchanged.
2067 ///
2068 /// WPE wants the opposite split: a **logical** size plus a scale, and it
2069 /// produces a buffer of `size * scale`. Handing it physical pixels while
2070 /// leaving the scale at 1 makes it lay out 2400x1600 *CSS* pixels on a 2x
2071 /// display — the viewport reads as twice as wide as it is and the whole
2072 /// page renders at half size. That is the bug this converts away.
2073 pub fn resize(&mut self, width_px: u32, height_px: u32, scale: f32) {
2074 self.size_px = (width_px.max(1), height_px.max(1));
2075 self.scale = scale.max(0.01);
2076 let (lw, lh) = self.logical_size();
2077 unsafe {
2078 wpe_toplevel_scale_changed(self.toplevel, self.scale as f64);
2079 wpe_toplevel_resized(self.toplevel, lw, lh);
2080 let view = self.active_tab().view;
2081 wpe_view_resized(view, lw, lh);
2082 }
2083 }
2084
2085 /// The view size WPE works in: physical divided back out by the scale.
2086 fn logical_size(&self) -> (i32, i32) {
2087 (
2088 ((self.size_px.0 as f32 / self.scale).round() as i32).max(1),
2089 ((self.size_px.1 as f32 / self.scale).round() as i32).max(1),
2090 )
2091 }
2092
2093 /// Physical pointer coordinates into the view's logical space, for the
2094 /// same reason as `resize` — a click at the bottom of a 2x window would
2095 /// otherwise land twice as far down the page as the cursor.
2096 fn to_logical(&self, x_px: f32, y_px: f32) -> (f64, f64) {
2097 ((x_px / self.scale) as f64, (y_px / self.scale) as f64)
2098 }
2099 }
2100
2101 /// Copy an SHM buffer's pixels out for the image registry.
2102 ///
2103 /// `WPE_PIXEL_FORMAT_ARGB8888` is B,G,R,A in memory on little-endian, which
2104 /// is handed over **as BGRA** rather than swizzled: the sampler reads either
2105 /// channel order at no cost, and rearranging 35 MB of bytes per frame on the
2106 /// CPU cost 7.4 ms at this display's fullscreen size — most of a frame budget,
2107 /// spent on nothing.
2108 ///
2109 /// The destination comes from `cce_ui::vk::recycle_buffer`, so in the steady
2110 /// state this allocates nothing: a fresh frame-sized `Vec` per frame was
2111 /// another 4.5 ms, almost all of it zeroing and page faults rather than
2112 /// copying. What remains is one memcpy per row, and only when the stride
2113 /// forces it — a tight stride is copied whole.
2114 ///
2115 /// Called from `pump`, never from the frame callback: a buffer superseded
2116 /// before the next pump is never read at all.
2117 ///
2118 /// The stride is not assumed to equal `width * 4`.
2119 /// WebKit features every webview runs with, off by default in this build.
2120 ///
2121 /// * `PropagateDamagingInformation` — each frame reports what it repainted,
2122 /// so `pump` copies only that (see `damage.rs`). Without it a page that
2123 /// can scroll costs a whole-window copy sixty times a second while it sits
2124 /// still, for an overlay scrollbar WebKit never stops repainting.
2125 /// * `HiddenPageCSSAnimationSuspension` — a background tab's CSS animations
2126 /// stop. WebKit already stops its `requestAnimationFrame` (checked: 0 a
2127 /// second hidden), but not this, and not its timers. A hidden Muji page
2128 /// measured 67% of a core with it off and 43% on.
2129 const FEATURES: &[(&str, bool)] =
2130 &[("PropagateDamagingInformation", true), ("HiddenPageCSSAnimationSuspension", true)];
2131
2132 unsafe fn set_features(wv: *mut WebKitWebView) {
2133 let settings = webkit_web_view_get_settings(wv);
2134 let list = webkit_settings_get_all_features();
2135 for &(name, on) in FEATURES {
2136 let found = (0..webkit_feature_list_get_length(list))
2137 .map(|i| webkit_feature_list_get(list, i))
2138 .find(|&f| from_cstr(webkit_feature_get_identifier(f)).as_deref() == Some(name));
2139 match found {
2140 Some(f) => webkit_settings_set_feature_enabled(settings, f, on as gboolean),
2141 // A WebKit upgrade renamed or dropped it: the browser still
2142 // works, it just loses what the feature bought.
2143 None => log::warn!("WebKit has no feature {name}; leaving it as it is"),
2144 }
2145 }
2146 webkit_feature_list_unref(list);
2147 }
2148
2149 /// A mapped SHM frame: where its pixels are, and how they are laid out.
2150 /// Borrowed from the buffer, so it must not outlive the buffer's release.
2151 struct ShmFrame {
2152 data: *const u8,
2153 stride: usize,
2154 width: u32,
2155 height: u32,
2156 }
2157
2158 impl ShmFrame {
2159 /// The buffer's pixels, if it is an SHM buffer with all its rows there.
2160 unsafe fn of(buffer: *mut WPEBuffer) -> Option<Self> {
2161 if g_type_check_instance_is_a(buffer as *mut GTypeInstance, wpe_buffer_shm_get_type()) == 0 {
2162 return None;
2163 }
2164 let shm = buffer as *mut WPEBufferSHM;
2165 let (width, height) = (
2166 wpe_buffer_get_width(buffer) as u32,
2167 wpe_buffer_get_height(buffer) as u32,
2168 );
2169 let mut len: u64 = 0;
2170 let data = g_bytes_get_data(wpe_buffer_shm_get_data(shm), &mut len as *mut u64) as *const u8;
2171 if data.is_null() || width == 0 || height == 0 {
2172 return None;
2173 }
2174 let stride = wpe_buffer_shm_get_stride(shm) as usize;
2175 if (len as usize) < stride * (height as usize - 1) + width as usize * 4 {
2176 return None;
2177 }
2178 Some(Self { data, stride, width, height })
2179 }
2180
2181 /// The whole frame, tightly packed, in a recycled buffer.
2182 unsafe fn copy_all(&self) -> Vec<u8> {
2183 let row = self.width as usize * 4;
2184 let need = row * self.height as usize;
2185 let mut out = cce_ui::vk::recycle_buffer(need);
2186 if self.stride == row {
2187 std::ptr::copy_nonoverlapping(self.data, out.as_mut_ptr(), need);
2188 } else {
2189 for y in 0..self.height as usize {
2190 std::ptr::copy_nonoverlapping(
2191 self.data.add(y * self.stride),
2192 out.as_mut_ptr().add(y * row),
2193 row,
2194 );
2195 }
2196 }
2197 out
2198 }
2199 }
2200
2201 /// `CCE_BROWSER_DAMAGE_CHECK`: patch the tab's CPU copy with the regions just
2202 /// read, as its image is being patched, and compare the result with the
2203 /// engine's whole frame. A mismatch means the damage left something out and
2204 /// the page on screen is stale there; the copy is then resynced so one miss
2205 /// is not reported on every frame after it.
2206 unsafe fn check_regions(
2207 mirror: &mut [u8],
2208 shm: &ShmFrame,
2209 packed: &[u8],
2210 regions: &[super::damage::Rect],
2211 tab: usize,
2212 ) {
2213 let row = shm.width as usize * 4;
2214 let mut at = 0usize;
2215 for &(x, y, w, h) in regions {
2216 let len = w as usize * 4;
2217 for r in 0..h as usize {
2218 let dst = (y as usize + r) * row + x as usize * 4;
2219 mirror[dst..dst + len].copy_from_slice(&packed[at..at + len]);
2220 at += len;
2221 }
2222 }
2223 let truth = shm.copy_all();
2224 let wrong = mirror
2225 .chunks_exact(4)
2226 .zip(truth.chunks_exact(4))
2227 .filter(|(a, b)| a != b)
2228 .count();
2229 if wrong > 0 {
2230 log::warn!(
2231 "damage check: tab {tab} is stale in {wrong} pixels after reading {} region(s) {regions:?}",
2232 regions.len()
2233 );
2234 mirror.copy_from_slice(&truth);
2235 } else {
2236 log::info!("damage check: tab {tab} exact after {} region(s)", regions.len());
2237 }
2238 }
2239
2240 unsafe fn from_cstr(p: *const c_char) -> Option<String> {
2241 (!p.is_null())
2242 .then(|| std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned())
2243 .filter(|s| !s.is_empty())
2244 }
2245
2246
2247 /// Same inverting stylesheet the Servo backend uses, and for the same reason:
2248 /// it is the only thing that darkens a page shipping a hardcoded white with no
2249 /// `prefers-color-scheme` rule to honour.
2250 const FORCE_DARK_CSS: &str = "\
2251 html { background-color: #ffffff !important; filter: invert(1) hue-rotate(180deg) !important; }
2252 img, video, picture, canvas, svg, iframe, embed, object,
2253 [style*=\"background-image\"], [style*=\"background:url\"] {
2254 filter: invert(1) hue-rotate(180deg) !important;
2255 }
2256 ";
2257
2258 /// Serves a `cce:` page. Runs on the main thread, unlike the Servo handler
2259 /// which runs on fetch threads — the `Arc<Mutex<_>>` stores are shared with
2260 /// that backend and stay as they are.
2261 unsafe extern "C" fn on_cce_request(request: *mut WebKitURISchemeRequest, data: gpointer) {
2262 let protocol = &*(data as *const crate::pages::CceProtocol);
2263 let uri = from_cstr(webkit_uri_scheme_request_get_uri(request)).unwrap_or_default();
2264 match protocol.route(&uri) {
2265 Some(html) => {
2266 let len = html.len() as i64;
2267 let bytes = html.into_bytes().into_boxed_slice();
2268 let ptr = Box::into_raw(bytes) as *mut c_void;
2269 // The stream owns the buffer and frees it with g_free, so the box
2270 // is deliberately leaked into it rather than dropped here.
2271 let stream = g_memory_input_stream_new_from_data(ptr, len, Some(free_boxed));
2272 let ctype = cstr("text/html; charset=utf-8");
2273 webkit_uri_scheme_request_finish(request, stream, len, ctype.as_ptr());
2274 g_object_unref(stream as *mut _);
2275 }
2276 None => {
2277 let msg = cstr(&format!("no such cce: page: {uri}"));
2278 let err = g_error_new_literal(1, 0, msg.as_ptr());
2279 webkit_uri_scheme_request_finish_error(request, err);
2280 g_error_free(err);
2281 }
2282 }
2283 }
2284
2285 unsafe extern "C" fn free_boxed(p: gpointer) {
2286 drop(Box::from_raw(p as *mut u8));
2287 }
2288
2289 /// Shared with WebKit's download signals for the life of the process.
2290 struct DownloadCtx {
2291 downloads: std::sync::Arc<crate::downloads::Downloads>,
2292 started: Rc<Cell<bool>>,
2293 }
2294
2295 /// Per-download state, owned by that download's own signal closures.
2296 struct OneDownload {
2297 ctx: Rc<DownloadCtx>,
2298 id: Cell<u64>,
2299 }
2300
2301 unsafe extern "C" fn on_download_started(
2302 _session: *mut GObject,
2303 download: *mut WebKitDownload,
2304 data: gpointer,
2305 ) {
2306 let ctx = &*(data as *const DownloadCtx);
2307 let one = Rc::new(OneDownload {
2308 ctx: Rc::new(DownloadCtx {
2309 downloads: ctx.downloads.clone(),
2310 started: ctx.started.clone(),
2311 }),
2312 id: Cell::new(u64::MAX),
2313 });
2314 ctx.started.set(true);
2315
2316 for (sig, cb) in [
2317 (
2318 "decide-destination",
2319 on_decide_destination as *const () as usize,
2320 ),
2321 ("received-data", on_received_data as *const () as usize),
2322 ("finished", on_finished as *const () as usize),
2323 ("failed", on_failed as *const () as usize),
2324 ] {
2325 let name = cstr(sig);
2326 g_signal_connect_data(
2327 download as *mut _,
2328 name.as_ptr(),
2329 Some(std::mem::transmute::<usize, unsafe extern "C" fn()>(cb)),
2330 Rc::into_raw(one.clone()) as gpointer,
2331 Some(drop_one_download),
2332 0,
2333 );
2334 }
2335 }
2336
2337 unsafe extern "C" fn drop_one_download(data: gpointer, _c: *mut GClosure) {
2338 drop(Rc::from_raw(data as *const OneDownload));
2339 }
2340
2341 /// WebKit asks where to put it, passing the name the *server* suggested —
2342 /// `Content-Disposition` when present, which the extension sniff could never
2343 /// see. Returning TRUE means we handled it.
2344 unsafe extern "C" fn on_decide_destination(
2345 download: *mut WebKitDownload,
2346 suggested: *const c_char,
2347 data: gpointer,
2348 ) -> gboolean {
2349 let one = &*(data as *const OneDownload);
2350 let name = from_cstr(suggested).unwrap_or_else(|| "download".into());
2351 let path = crate::downloads::Downloads::destination_for(&name);
2352
2353 let total = {
2354 let response = webkit_download_get_response(download);
2355 (!response.is_null())
2356 .then(|| webkit_uri_response_get_content_length(response))
2357 .filter(|n| *n > 0)
2358 };
2359 let uri = from_cstr(webkit_download_get_destination(download)).unwrap_or_default();
2360 one.id
2361 .set(one.ctx.downloads.adopt(uri, path.clone(), total));
2362
2363 let dest = cstr(&path.to_string_lossy());
2364 webkit_download_set_destination(download, dest.as_ptr());
2365 1
2366 }
2367
2368 unsafe extern "C" fn on_received_data(
2369 download: *mut WebKitDownload,
2370 _len: u64,
2371 data: gpointer,
2372 ) {
2373 let one = &*(data as *const OneDownload);
2374 if one.id.get() != u64::MAX {
2375 one.ctx.downloads.set_progress(
2376 one.id.get(),
2377 webkit_download_get_received_data_length(download),
2378 None,
2379 );
2380 }
2381 }
2382
2383 unsafe extern "C" fn on_finished(_d: *mut WebKitDownload, data: gpointer) {
2384 let one = &*(data as *const OneDownload);
2385 if one.id.get() != u64::MAX {
2386 one.ctx.downloads.set_finished(one.id.get(), Ok(()));
2387 }
2388 }
2389
2390 unsafe extern "C" fn on_failed(_d: *mut WebKitDownload, error: *mut GError, data: gpointer) {
2391 let one = &*(data as *const OneDownload);
2392 let msg = (!error.is_null())
2393 .then(|| from_cstr((*error).message))
2394 .flatten()
2395 .unwrap_or_else(|| "download failed".into());
2396 if one.id.get() != u64::MAX {
2397 one.ctx.downloads.set_finished(one.id.get(), Err(msg));
2398 }
2399 }
2400
2401 /// What a page is currently blocked on. At most one of each: WebKit will not
2402 /// raise a second dialog on the same view until the first is answered.
2403 #[derive(Default)]
2404 pub(super) struct Prompts {
2405 dialog: Option<(*mut WebKitScriptDialog, PendingDialog)>,
2406 auth: Option<(*mut WebKitAuthenticationRequest, PendingAuth)>,
2407 /// The page asked for a context menu; the chrome draws its own.
2408 context_menu: Option<ContextMenuInfo>,
2409 /// The `<select>` list waiting on an answer, reffed until it gets one or
2410 /// the page closes it. Never more than one: a newer one closes the last.
2411 option_menu: Option<*mut WebKitOptionMenu>,
2412 /// What that list holds, until the chrome takes it to draw.
2413 option_menu_new: Option<OptionMenuInfo>,
2414 /// Links middle-clicked in a page, oldest first, for the chrome to open
2415 /// as background tabs.
2416 background_opens: Vec<Url>,
2417 /// Login fields the account watcher reported, oldest first. A queue and
2418 /// not a slot: a blur followed by a focus is two different states, and
2419 /// collapsing them would leave the list open over the wrong field.
2420 form_events: std::collections::VecDeque<crate::wpe::formwatch::FormEvent>,
2421 /// Open fill asks, oldest first, by the asking document's token. Each
2422 /// is a reply a watcher's promise is waiting on, held with a ref, and
2423 /// every one is answered exactly once: with a credential, or with
2424 /// nothing when it is displaced or dropped.
2425 fill_asks: std::collections::VecDeque<(String, *mut WebKitScriptMessageReply)>,
2426 /// The vi focus watcher's latest word: whether the focused element takes
2427 /// text. Only the newest matters, so a slot, not a queue.
2428 vi_focus: Option<bool>,
2429 /// Answers to [`WebKitHost::vi_eval`], by the caller's tag.
2430 vi_results: std::collections::VecDeque<(u32, String)>,
2431 /// The last find-in-page outcome: the match count, 0 for none.
2432 find_result: Option<u32>,
2433 }
2434
2435 /// What was under the pointer when the page asked for a context menu, read
2436 /// off WebKit's hit test. The chrome builds its menu from this.
2437 #[derive(Debug, Clone, Default)]
2438 pub struct ContextMenuInfo {
2439 /// `(uri, label)` when the hit was a link.
2440 pub link: Option<(String, Option<String>)>,
2441 pub image_uri: Option<String>,
2442 pub is_selection: bool,
2443 pub is_editable: bool,
2444 }
2445
2446 /// A `<select>`'s option list, for the chrome to draw at the select.
2447 #[derive(Debug, Clone)]
2448 pub struct OptionMenuInfo {
2449 pub items: Vec<OptionItem>,
2450 /// The select's box — `(x, y, width, height)` in the view's logical
2451 /// pixels, which are the chrome's.
2452 pub anchor: (f32, f32, f32, f32),
2453 }
2454
2455 /// One row of a select's list: an `<option>`, or an `<optgroup>`'s label.
2456 #[derive(Debug, Clone)]
2457 pub struct OptionItem {
2458 pub label: String,
2459 /// An `<optgroup>` heading: drawn, never picked.
2460 pub group_label: bool,
2461 /// An option inside an `<optgroup>`, drawn indented under its heading.
2462 pub group_child: bool,
2463 pub enabled: bool,
2464 /// The select's current value.
2465 pub selected: bool,
2466 }
2467
2468 /// A page's `alert` / `confirm` / `prompt`, waiting on the chrome.
2469 #[derive(Debug, Clone)]
2470 pub struct PendingDialog {
2471 pub message: String,
2472 /// `Some` for `prompt`, carrying its default text; `None` otherwise.
2473 pub prompt_default: Option<String>,
2474 /// `confirm` and `beforeunload` offer a choice; `alert` only acknowledges.
2475 pub has_cancel: bool,
2476 }
2477
2478 /// An HTTP auth challenge, waiting on the chrome.
2479 #[derive(Debug, Clone)]
2480 pub struct PendingAuth {
2481 pub host: String,
2482 pub realm: String,
2483 /// Set when the previous credentials were rejected — worth telling the
2484 /// user, since the field otherwise looks identical to the first attempt.
2485 pub retry: bool,
2486 }
2487
2488 unsafe fn connect_raw(
2489 wv: *mut WebKitWebView,
2490 signal: &str,
2491 cb: usize,
2492 prompts: &Rc<RefCell<Prompts>>,
2493 ) {
2494 let name = cstr(signal);
2495 g_signal_connect_data(
2496 wv as *mut _,
2497 name.as_ptr(),
2498 Some(std::mem::transmute::<usize, unsafe extern "C" fn()>(cb)),
2499 Rc::into_raw(prompts.clone()) as gpointer,
2500 Some(drop_prompts_ref),
2501 0,
2502 );
2503 }
2504
2505 /// A report from the vi focus watcher. Anything else is dropped.
2506 unsafe extern "C" fn on_vi_message(
2507 _ucm: *mut WebKitUserContentManager,
2508 value: *mut JSCValue,
2509 data: gpointer,
2510 ) {
2511 let prompts = &*(data as *const RefCell<Prompts>);
2512 let raw = jsc_value_to_string(value);
2513 let Some(json) = from_cstr(raw) else { return };
2514 g_free(raw as *mut _);
2515 if let Some(editable) = crate::vi::parse_focus(&json) {
2516 prompts.borrow_mut().vi_focus = Some(editable);
2517 }
2518 }
2519
2520 /// A [`WebKitHost::vi_eval`] script finished: queue what it returned.
2521 unsafe extern "C" fn on_vi_eval(source: *mut GObject, res: *mut GAsyncResult, data: gpointer) {
2522 let ctx = Box::from_raw(data as *mut (Rc<RefCell<Prompts>>, u32));
2523 let mut err: *mut GError = std::ptr::null_mut();
2524 let v = webkit_web_view_evaluate_javascript_finish(source as *mut WebKitWebView, res, &mut err);
2525 let mut text = String::new();
2526 if !v.is_null() {
2527 if jsc_value_is_string(v) != 0 {
2528 let raw = jsc_value_to_string(v);
2529 text = from_cstr(raw).unwrap_or_default();
2530 g_free(raw as *mut _);
2531 }
2532 g_object_unref(v as *mut _);
2533 }
2534 if !err.is_null() {
2535 g_error_free(err);
2536 }
2537 ctx.0.borrow_mut().vi_results.push_back((ctx.1, text));
2538 }
2539
2540 unsafe extern "C" fn on_found_text(_fc: *mut WebKitFindController, count: guint, data: gpointer) {
2541 let prompts = &*(data as *const RefCell<Prompts>);
2542 prompts.borrow_mut().find_result = Some(count);
2543 }
2544
2545 unsafe extern "C" fn on_failed_to_find(_fc: *mut WebKitFindController, data: gpointer) {
2546 let prompts = &*(data as *const RefCell<Prompts>);
2547 prompts.borrow_mut().find_result = Some(0);
2548 }
2549
2550 /// A message from the account watcher. Anything that does not parse as one of
2551 /// its events is dropped without comment — this is a channel the chrome acts
2552 /// on, so it accepts only what it recognizes.
2553 unsafe extern "C" fn on_account_message(
2554 _ucm: *mut WebKitUserContentManager,
2555 value: *mut JSCValue,
2556 data: gpointer,
2557 ) {
2558 let prompts = &*(data as *const RefCell<Prompts>);
2559 let raw = jsc_value_to_string(value);
2560 let Some(json) = from_cstr(raw) else { return };
2561 g_free(raw as *mut _);
2562 if let Some(event) = super::formwatch::parse_event(&json) {
2563 let mut p = prompts.borrow_mut();
2564 // A page that spins on scroll must not grow this without bound; the
2565 // chrome only ever cares about the last few.
2566 if p.form_events.len() > 8 {
2567 p.form_events.pop_front();
2568 }
2569 p.form_events.push_back(event);
2570 }
2571 }
2572
2573 /// A login field asking to be filled. The reply is held until a pick
2574 /// answers it, or a newer ask displaces it. Returning TRUE says it will be
2575 /// answered — later, which is the point.
2576 unsafe extern "C" fn on_fill_ask(
2577 _ucm: *mut WebKitUserContentManager,
2578 value: *mut JSCValue,
2579 reply: *mut WebKitScriptMessageReply,
2580 data: gpointer,
2581 ) -> gboolean {
2582 let prompts = &*(data as *const RefCell<Prompts>);
2583 let raw = jsc_value_to_string(value);
2584 let token = from_cstr(raw);
2585 g_free(raw as *mut _);
2586 // The watcher's tokens are 24 hex digits; anything else is not one.
2587 let Some(token) =
2588 token.filter(|t| t.len() == 24 && t.bytes().all(|b| b.is_ascii_hexdigit()))
2589 else {
2590 webkit_script_message_reply_ref(reply);
2591 answer_fill(reply, None);
2592 return 1;
2593 };
2594 webkit_script_message_reply_ref(reply);
2595 let displaced: Vec<_> = {
2596 let mut p = prompts.borrow_mut();
2597 let mut out = Vec::new();
2598 p.fill_asks.retain(|(t, r)| {
2599 let same = *t == token;
2600 if same {
2601 out.push(*r);
2602 }
2603 !same
2604 });
2605 p.fill_asks.push_back((token, reply));
2606 // Only the focused field's ask matters; a few spare cover a list
2607 // still open while focus wanders between frames.
2608 while p.fill_asks.len() > 4 {
2609 if let Some((_, r)) = p.fill_asks.pop_front() {
2610 out.push(r);
2611 }
2612 }
2613 out
2614 };
2615 for r in displaced {
2616 answer_fill(r, None);
2617 }
2618 1
2619 }
2620
2621 /// Answer a fill ask — with the credential's JSON, or with null — and let
2622 /// go of it.
2623 unsafe fn answer_fill(reply: *mut WebKitScriptMessageReply, value: Option<&str>) {
2624 thread_local! {
2625 /// A context to build reply values in. Any will do: the value is
2626 /// serialized across to the web process, not run here.
2627 static JSC: *mut JSCContext = unsafe { jsc_context_new() };
2628 }
2629 JSC.with(|ctx| {
2630 let v = match value {
2631 // JSON has no raw NUL — serde escapes it — so this cannot fail on
2632 // a credential.
2633 Some(s) => {
2634 let c = cstr(s);
2635 jsc_value_new_string(*ctx, c.as_ptr())
2636 }
2637 None => jsc_value_new_null(*ctx),
2638 };
2639 webkit_script_message_reply_return_value(reply, v);
2640 g_object_unref(v as *mut _);
2641 });
2642 webkit_script_message_reply_unref(reply);
2643 }
2644
2645 unsafe extern "C" fn drop_prompts_ref(data: gpointer, _c: *mut GClosure) {
2646 drop(Rc::from_raw(data as *const RefCell<Prompts>));
2647 }
2648
2649 /// Returning TRUE means *we* will answer. The dialog is reffed and held; the
2650 /// page stays blocked until `respond_dialog` closes it.
2651 unsafe extern "C" fn on_script_dialog(
2652 _wv: *mut WebKitWebView,
2653 dialog: *mut WebKitScriptDialog,
2654 data: gpointer,
2655 ) -> gboolean {
2656 let prompts = &*(data as *const RefCell<Prompts>);
2657 let kind = webkit_script_dialog_get_dialog_type(dialog);
2658 let message = from_cstr(webkit_script_dialog_get_message(dialog)).unwrap_or_default();
2659 let is_prompt = kind == WebKitScriptDialogType::WEBKIT_SCRIPT_DIALOG_PROMPT;
2660 let pending = PendingDialog {
2661 message,
2662 prompt_default: is_prompt
2663 .then(|| from_cstr(webkit_script_dialog_prompt_get_default_text(dialog)))
2664 .flatten()
2665 .or_else(|| is_prompt.then(String::new)),
2666 has_cancel: kind != WebKitScriptDialogType::WEBKIT_SCRIPT_DIALOG_ALERT,
2667 };
2668 webkit_script_dialog_ref(dialog);
2669 prompts.borrow_mut().dialog = Some((dialog, pending));
2670 1
2671 }
2672
2673 /// Same contract: TRUE means we answer, and the request is reffed until we do.
2674 unsafe extern "C" fn on_authenticate(
2675 _wv: *mut WebKitWebView,
2676 request: *mut WebKitAuthenticationRequest,
2677 data: gpointer,
2678 ) -> gboolean {
2679 let prompts = &*(data as *const RefCell<Prompts>);
2680 let pending = PendingAuth {
2681 host: from_cstr(webkit_authentication_request_get_host(request)).unwrap_or_default(),
2682 realm: from_cstr(webkit_authentication_request_get_realm(request)).unwrap_or_default(),
2683 retry: webkit_authentication_request_is_retry(request) != 0,
2684 };
2685 g_object_ref(request as *mut _);
2686 prompts.borrow_mut().auth = Some((request, pending));
2687 1
2688 }
2689
2690 /// A navigation is about to happen. A middle-click on a link — which WebKit
2691 /// reports as an ordinary navigation (or a new-window one, for a
2692 /// `target=_blank` link) carrying the button — is diverted to a background
2693 /// tab: the decision is ignored here and the URL queued for the chrome.
2694 /// Everything else gets WebKit's default.
2695 unsafe extern "C" fn on_decide_policy(
2696 _wv: *mut WebKitWebView,
2697 decision: *mut WebKitPolicyDecision,
2698 kind: WebKitPolicyDecisionType::Type,
2699 data: gpointer,
2700 ) -> gboolean {
2701 if kind != WebKitPolicyDecisionType::WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION
2702 && kind != WebKitPolicyDecisionType::WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION
2703 {
2704 return 0;
2705 }
2706 let action = webkit_navigation_policy_decision_get_navigation_action(
2707 decision as *mut WebKitNavigationPolicyDecision,
2708 );
2709 if action.is_null()
2710 || webkit_navigation_action_get_mouse_button(action) != 2
2711 || webkit_navigation_action_get_navigation_type(action)
2712 != WebKitNavigationType::WEBKIT_NAVIGATION_TYPE_LINK_CLICKED
2713 {
2714 return 0;
2715 }
2716 let request = webkit_navigation_action_get_request(action);
2717 let Some(url) = (!request.is_null())
2718 .then(|| from_cstr(webkit_uri_request_get_uri(request)))
2719 .flatten()
2720 .and_then(|u| Url::parse(&u).ok())
2721 else {
2722 return 0;
2723 };
2724 webkit_policy_decision_ignore(decision);
2725 let prompts = &*(data as *const RefCell<Prompts>);
2726 prompts.borrow_mut().background_opens.push(url);
2727 1
2728 }
2729
2730 /// The page asked for a context menu. Stash what the hit test says was under
2731 /// the pointer and claim presentation; the chrome draws the menu at the
2732 /// pointer position it already tracks (the hit test carries no coordinates).
2733 unsafe extern "C" fn on_context_menu(
2734 _wv: *mut WebKitWebView,
2735 _menu: *mut WebKitContextMenu,
2736 hit: *mut WebKitHitTestResult,
2737 data: gpointer,
2738 ) -> gboolean {
2739 let prompts = &*(data as *const RefCell<Prompts>);
2740 let mut info = ContextMenuInfo::default();
2741 if !hit.is_null() {
2742 if webkit_hit_test_result_context_is_link(hit) != 0 {
2743 if let Some(uri) = from_cstr(webkit_hit_test_result_get_link_uri(hit)) {
2744 info.link = Some((uri, from_cstr(webkit_hit_test_result_get_link_label(hit))));
2745 }
2746 }
2747 if webkit_hit_test_result_context_is_image(hit) != 0 {
2748 info.image_uri = from_cstr(webkit_hit_test_result_get_image_uri(hit));
2749 }
2750 info.is_selection = webkit_hit_test_result_context_is_selection(hit) != 0;
2751 info.is_editable = webkit_hit_test_result_context_is_editable(hit) != 0;
2752 }
2753 prompts.borrow_mut().context_menu = Some(info);
2754 1
2755 }
2756
2757 /// A `<select>` asked to open. Read its options and where it is, hold the
2758 /// menu, and claim it: the chrome draws the list and answers with
2759 /// `pick_option` or `close_option_menu`. Returning FALSE would leave it to
2760 /// WebKit's default, which on WPE is nothing at all.
2761 unsafe extern "C" fn on_show_option_menu(
2762 _wv: *mut WebKitWebView,
2763 menu: *mut WebKitOptionMenu,
2764 rect: *mut WebKitRectangle,
2765 data: gpointer,
2766 ) -> gboolean {
2767 let prompts = &*(data as *const RefCell<Prompts>);
2768 let items = (0..webkit_option_menu_get_n_items(menu))
2769 .map(|i| {
2770 let item = webkit_option_menu_get_item(menu, i);
2771 OptionItem {
2772 label: from_cstr(webkit_option_menu_item_get_label(item)).unwrap_or_default(),
2773 group_label: webkit_option_menu_item_is_group_label(item) != 0,
2774 group_child: webkit_option_menu_item_is_group_child(item) != 0,
2775 enabled: webkit_option_menu_item_is_enabled(item) != 0,
2776 selected: webkit_option_menu_item_is_selected(item) != 0,
2777 }
2778 })
2779 .collect();
2780 let anchor = if rect.is_null() {
2781 (0.0, 0.0, 0.0, 0.0)
2782 } else {
2783 let r = &*rect;
2784 (r.x as f32, r.y as f32, r.width as f32, r.height as f32)
2785 };
2786 g_object_ref(menu as *mut _);
2787 // The page can close the list itself (the select goes away, the page
2788 // navigates); hearing that is how the chrome's copy comes down too.
2789 let name = cstr("close");
2790 g_signal_connect_data(
2791 menu as *mut _,
2792 name.as_ptr(),
2793 Some(std::mem::transmute::<usize, unsafe extern "C" fn()>(
2794 on_option_menu_close as *const () as usize,
2795 )),
2796 // The close handler's own reference, dropped with the connection.
2797 {
2798 Rc::increment_strong_count(data as *const RefCell<Prompts>);
2799 data
2800 },
2801 Some(drop_prompts_ref),
2802 0,
2803 );
2804 let previous = {
2805 let mut p = prompts.borrow_mut();
2806 p.option_menu_new = Some(OptionMenuInfo { items, anchor });
2807 p.option_menu.replace(menu)
2808 };
2809 // Closed outside the borrow: its close handler borrows the prompts.
2810 if let Some(old) = previous {
2811 webkit_option_menu_close(old);
2812 g_object_unref(old as *mut _);
2813 }
2814 1
2815 }
2816
2817 /// A held list was closed — by the page, or by our own `close`. Only the
2818 /// first case finds it still held; ours took it out before closing. WebKit
2819 /// keeps its own reference across the emission, so dropping ours here is
2820 /// safe.
2821 unsafe extern "C" fn on_option_menu_close(menu: *mut WebKitOptionMenu, data: gpointer) {
2822 let prompts = &*(data as *const RefCell<Prompts>);
2823 let mut p = prompts.borrow_mut();
2824 if p.option_menu == Some(menu) {
2825 p.option_menu = None;
2826 p.option_menu_new = None;
2827 drop(p);
2828 g_object_unref(menu as *mut _);
2829 }
2830 }