web browser (Servo)
git clone https://git.lucas.co/cce-browser.git
fix(wpe): give pages window focus, so the text caret shows
WebKit paints a caret only in a page that is both focused (the view) and
active (the toplevel's ACTIVE state). Keystrokes reach a field with
neither, so the only symptom was typing into a field with no caret.
Two causes:
- Nothing in the app called WebKitHost::focus, and it never set the
toplevel's ACTIVE state anyway. handle_focus_change now calls it; it
sets both, and activate() moves view focus to the tab being shown.
- The toplevel was created with max_views = 1 (the spike's value), and a
full toplevel refuses wpe_view_set_toplevel silently. Only the first
tab ever attached, so every later tab also lacked the window's scale
and rendered at half resolution on a 2x output. 0 is unlimited.
examples/wpe_focus.rs checks document.hasFocus() across window focus
changes, a new tab and a tab switch; it failed before and passes now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WPE-PORT.md | 1 +
examples/wpe_focus.rs | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++
src/main.rs | 2 ++
src/webview.rs | 2 +-
src/wpe/host.rs | 47 +++++++++++++++++++++++++++------
5 files changed, 115 insertions(+), 9 deletions(-)
diff --git a/WPE-PORT.md b/WPE-PORT.md
index 87ae6ce..1569543 100644
--- a/WPE-PORT.md
+++ b/WPE-PORT.md
@@ -333,6 +333,7 @@ The examples are the test suite; all need `--features wpe`.
| `wpe_host` | boot, frames, page state, navigation, history |
| `wpe_input` | pointer / keyboard / wheel reaching the page, read back via `document.title` |
| `wpe_tabs` | several views on one display, and a **backgrounded** tab still updating |
+| `wpe_focus` | window focus reaching every tab's page (`document.hasFocus()`), the condition WebKit paints the text caret on |
| `wpe_loop` | blocking on GLib's fds vs polling, with the wakeup counts |
| `wpe_dark` | force-dark, asserted on rendered pixels rather than on the call |
diff --git a/examples/wpe_focus.rs b/examples/wpe_focus.rs
new file mode 100644
index 0000000..2e97fe7
--- /dev/null
+++ b/examples/wpe_focus.rs
@@ -0,0 +1,72 @@
+//! Proves the page believes it is focused — the condition WebKit gates the
+//! text caret on.
+//!
+//! A caret is painted only in a frame that is both *focused* (the view's
+//! focus) and *active* (the toplevel's `ACTIVE` state). Without either, a
+//! field takes typing perfectly well and shows no caret, which is how it
+//! shipped. `document.hasFocus()` reads the same two conditions, so the page
+//! reports it into `document.title` and this reads it back, across the window
+//! losing and regaining focus and across a tab switch.
+//!
+//! `cargo run --release -p cce-browser --example wpe_focus`
+
+#[cfg(not(feature = "wpe"))]
+fn main() {
+ eprintln!("build with --features wpe");
+}
+
+#[cfg(feature = "wpe")]
+#[derive(Debug, Clone, Copy)]
+pub enum EditingCommand { Copy, Cut, Paste }
+
+#[cfg(feature = "wpe")]
+#[path = "../src/pages.rs"]
+mod pages;
+#[cfg(feature = "wpe")]
+#[path = "../src/downloads.rs"]
+mod downloads;
+#[cfg(feature = "wpe")]
+#[path = "../src/wpe/mod.rs"]
+mod wpe;
+
+#[cfg(feature = "wpe")]
+fn main() {
+ let page = "data:text/html,<input autofocus><script>\
+ setInterval(() => document.title = 'focus=' + document.hasFocus(), 50)\
+ </script>";
+ let mut host = wpe::WebKitHost::new(url::Url::parse(page).unwrap(), (400, 300));
+ let settle = |h: &mut wpe::WebKitHost, n: u32| {
+ for _ in 0..n {
+ h.pump();
+ std::thread::sleep(std::time::Duration::from_millis(50));
+ }
+ };
+
+ let mut ok = true;
+ let mut expect = |h: &mut wpe::WebKitHost, what: &str, want: bool| {
+ settle(h, 10);
+ let got = h.title();
+ let pass = got.as_deref() == Some(if want { "focus=true" } else { "focus=false" });
+ ok &= pass;
+ println!("{what:<34} {got:?} {}", if pass { "OK" } else { "WRONG" });
+ };
+
+ settle(&mut host, 30);
+ expect(&mut host, "loaded, window not focused", false);
+ host.focus(true);
+ expect(&mut host, "window focused", true);
+ host.focus(false);
+ expect(&mut host, "window unfocused", false);
+ host.focus(true);
+ expect(&mut host, "window refocused", true);
+
+ // A tab opened while the window is focused must arrive focused too.
+ host.open_tab(url::Url::parse(page).unwrap());
+ settle(&mut host, 30);
+ expect(&mut host, "new tab", true);
+ host.activate(0);
+ expect(&mut host, "back to the first tab", true);
+
+ println!("\nfocus: {}", if ok { "OK" } else { "BROKEN" });
+ std::process::exit(if ok { 0 } else { 1 });
+}
diff --git a/src/main.rs b/src/main.rs
index 96a041b..8cfbdeb 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -2269,6 +2269,8 @@ impl Application for BrowserApp {
}
fn handle_focus_change(&mut self, focused: bool, needs_rebuild: &mut bool) {
+ // The page's own focus: without it WebKit paints no text caret.
+ self.host.focus(focused);
// A settings change can move the bar to the other edge, so a reload
// that changed anything has to redraw the chrome.
if focused && self.reload_settings() {
diff --git a/src/webview.rs b/src/webview.rs
index 7c629f0..90969b0 100644
--- a/src/webview.rs
+++ b/src/webview.rs
@@ -844,7 +844,7 @@ impl ServoHost {
/// Page focus. A no-op for Servo, which tracks focus itself; present so
/// both backends accept the same call.
- pub fn focus(&self, _focused: bool) {}
+ pub fn focus(&mut self, _focused: bool) {}
/// Forward a key to the page, modifiers included.
///
diff --git a/src/wpe/host.rs b/src/wpe/host.rs
index d360d6b..fcee5a9 100644
--- a/src/wpe/host.rs
+++ b/src/wpe/host.rs
@@ -190,6 +190,10 @@ pub struct WebKitHost {
/// the colour scheme is display-level and force-dark lives in the shared
/// user-content-manager it was built with.
spare: Option<(*mut WebKitWebView, *mut WPEView, Rc<TabState>)>,
+ /// Whether the window holds keyboard focus, as last told by [`Self::focus`].
+ /// Kept so a tab made active later inherits it: focus belongs to the view,
+ /// and only the active tab's view should have it.
+ window_focused: bool,
}
unsafe fn cstr(s: &str) -> CString {
@@ -306,7 +310,12 @@ impl WebKitHost {
true
}));
- let toplevel = wpe_display_create_toplevel(display, 1);
+ // `0` is no view limit, and every tab's view must fit: a full
+ // toplevel refuses `wpe_view_set_toplevel` *silently*. At `1`
+ // (the spike's value) only the first tab ever attached, and every
+ // later one ran without the window's scale (rendering at half
+ // resolution on a 2x output) or its ACTIVE state (no text caret).
+ let toplevel = wpe_display_create_toplevel(display, 0);
// Scale is 1 until the first `resize` from a real window; the
// constructor's size is already logical.
wpe_toplevel_resized(toplevel, size_px.0 as i32, size_px.1 as i32);
@@ -338,6 +347,7 @@ impl WebKitHost {
ucm: webkit_user_content_manager_new(),
watcher: None,
spare: None,
+ window_focused: false,
};
// The account watcher's channel, in its own script world. Both
// halves are registered here, once, on the shared content
@@ -628,6 +638,9 @@ impl WebKitHost {
}
unsafe {
if let Some(old) = self.tabs.get(self.active) {
+ if self.window_focused {
+ wpe_view_focus_out(old.view);
+ }
wpe_view_unmap(old.view);
wpe_view_set_visible(old.view, 0);
}
@@ -638,6 +651,9 @@ impl WebKitHost {
wpe_view_map(tab.view);
let (lw, lh) = self.logical_size();
wpe_view_resized(tab.view, lw, lh);
+ if self.window_focused {
+ wpe_view_focus_in(tab.view);
+ }
}
}
@@ -1215,15 +1231,30 @@ impl WebKitHost {
}
}
- /// Page focus. Without this the page has no focused frame and keyboard
- /// input is dropped, which looks exactly like a broken key mapping.
- pub fn focus(&self, focused: bool) {
+ /// Window focus, from the runner's keyboard enter/leave.
+ ///
+ /// WebKit needs **two** things before it paints a text caret: the view
+ /// focused and the toplevel `ACTIVE`. Keystrokes reach a focused field
+ /// with neither, so the only symptom of missing this is a field you can
+ /// type into with no caret in it — which shipped, unnoticed, because
+ /// nothing called this at all. `document.hasFocus()` reads the same pair;
+ /// `examples/wpe_focus.rs` checks it.
+ pub fn focus(&mut self, focused: bool) {
+ self.window_focused = focused;
unsafe {
- let view = self.active_tab().view;
- if focused {
- wpe_view_focus_in(view)
+ let state = wpe_toplevel_get_state(self.toplevel);
+ let state = if focused {
+ state | WPEToplevelState::WPE_TOPLEVEL_STATE_ACTIVE
} else {
- wpe_view_focus_out(view)
+ state & !WPEToplevelState::WPE_TOPLEVEL_STATE_ACTIVE
+ };
+ wpe_toplevel_state_changed(self.toplevel, state);
+ if let Some(tab) = self.tabs.get(self.active) {
+ if focused {
+ wpe_view_focus_in(tab.view)
+ } else {
+ wpe_view_focus_out(tab.view)
+ }
}
}
}