git.lucas.co / cce-browser
web browser (Servo)
git clone https://git.lucas.co/cce-browser.git

commitac0283295e90afa4abed73036e64fc42cd512ec1
parentaef307cb97
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-02 13:02
fix(wpe): stamp held buttons on pointer events, so a drag selects

WebKit reads a drag off the move event's own modifiers, not off the press
it saw earlier. Every pointer move went out with an empty mask, so
press-drag-release over text was a hover and selected nothing (dragging
links and images was dead the same way). The host now keeps the held
buttons as WPE_MODIFIER_POINTER_* bits and puts them on moves, presses
and releases.

examples/wpe_select drags across a line of text and reads the selection
back: empty before, "alpha bravo charlie d" after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

 examples/wpe_select.rs | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++
 src/wpe/host.rs        | 22 +++++++++++++++--
 src/wpe/input.rs       | 10 ++++++++
 3 files changed, 95 insertions(+), 2 deletions(-)

diff --git a/examples/wpe_select.rs b/examples/wpe_select.rs
new file mode 100644
index 0000000..0161b8c
--- /dev/null
+++ b/examples/wpe_select.rs
@@ -0,0 +1,65 @@
+//! Proves a press-drag-release over text selects it.
+//!
+//! WebKit decides whether a pointer move is a drag from the button bits in
+//! the move event's modifiers, not from the press it saw earlier; a move
+//! that reports no held button is a hover, and nothing is selected. The page
+//! mirrors `getSelection()` into `document.title`, which page-state sync
+//! already reads back.
+//!
+//! `cargo run --release -p cce-browser --example wpe_select`
+
+#[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() {
+    use cce_ui::widget::MouseButton;
+
+    let page = "data:text/html,<html><body style='margin:0;font:40px monospace'>\
+        <p id=t style='margin:0'>alpha bravo charlie delta echo</p>\
+        <script>document.title='sel:';\
+        document.addEventListener('selectionchange',()=>\
+        document.title='sel:'+getSelection().toString())</script></body></html>";
+    let mut host = wpe::WebKitHost::new(url::Url::parse(page).unwrap(), (1200, 800));
+    let settle = |h: &mut wpe::WebKitHost, n: u32| {
+        for _ in 0..n {
+            h.pump();
+            std::thread::sleep(std::time::Duration::from_millis(50));
+        }
+    };
+    settle(&mut host, 40);
+    host.focus(true);
+
+    // Press at the start of the line, drag right in steps, release.
+    host.mouse_move(2.0, 20.0);
+    settle(&mut host, 2);
+    host.mouse_button_ui(MouseButton::Left, true, 2.0, 20.0);
+    settle(&mut host, 2);
+    for x in (20..=500).step_by(40) {
+        host.mouse_move(x as f32, 20.0);
+        settle(&mut host, 1);
+    }
+    host.mouse_button_ui(MouseButton::Left, false, 500.0, 20.0);
+    settle(&mut host, 8);
+
+    let title = host.title().unwrap_or_default();
+    println!("title={title:?}");
+    let selected = title.strip_prefix("sel:").unwrap_or("");
+    println!("{}", if selected.trim().is_empty() { "FAIL: nothing selected" } else { "OK: drag selected text" });
+}
diff --git a/src/wpe/host.rs b/src/wpe/host.rs
index e8c544e..a2f59e3 100644
--- a/src/wpe/host.rs
+++ b/src/wpe/host.rs
@@ -194,6 +194,14 @@ pub struct WebKitHost {
     /// 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,
+    /// The pointer buttons the page is holding, as `WPE_MODIFIER_POINTER_*`
+    /// bits, stamped on every pointer event.
+    ///
+    /// WebKit reads a drag off the *move* event's own modifiers, not off the
+    /// press it saw earlier: a move reporting no held button is a hover, so
+    /// press-drag-release over text selected nothing (and dragged nothing)
+    /// while every move went out with an empty mask.
+    held_buttons: Cell<WPEModifiers::Type>,
 }
 
 unsafe fn cstr(s: &str) -> CString {
@@ -348,6 +356,7 @@ impl WebKitHost {
                 watcher: None,
                 spare: None,
                 window_focused: false,
+                held_buttons: Cell::new(0),
             };
             // The account watcher's channel, in its own script world. Both
             // halves are registered here, once, on the shared content
@@ -1139,7 +1148,7 @@ impl WebKitHost {
                 view,
                 WPEInputSource::WPE_INPUT_SOURCE_MOUSE,
                 input::now_ms(),
-                0,
+                self.held_buttons.get(),
                 x,
                 y,
                 0.0,
@@ -1164,6 +1173,15 @@ impl WebKitHost {
             } else {
                 0
             };
+            // The mask describes the state *after* this event, which is
+            // what a DOM `buttons` reads on mousedown and mouseup.
+            let bit = input::button_modifier(n);
+            let held = if pressed {
+                self.held_buttons.get() | bit
+            } else {
+                self.held_buttons.get() & !bit
+            };
+            self.held_buttons.set(held);
             let e = wpe_event_pointer_button_new(
                 if pressed {
                     WPEEventType::WPE_EVENT_POINTER_DOWN
@@ -1173,7 +1191,7 @@ impl WebKitHost {
                 view,
                 WPEInputSource::WPE_INPUT_SOURCE_MOUSE,
                 time,
-                0,
+                held,
                 n,
                 x,
                 y,
diff --git a/src/wpe/input.rs b/src/wpe/input.rs
index a1ae723..455dcbb 100644
--- a/src/wpe/input.rs
+++ b/src/wpe/input.rs
@@ -110,6 +110,16 @@ pub(super) fn button_number(b: MouseButton) -> Option<u32> {
     })
 }
 
+/// An X11 button number's held-button bit, the form a pointer event's
+/// modifiers carry it in (`WPE_MODIFIER_POINTER_BUTTON1` is button 1, and the
+/// five are consecutive).
+pub(super) fn button_modifier(n: u32) -> WPEModifiers::Type {
+    match n {
+        1..=5 => WPEModifiers::WPE_MODIFIER_POINTER_BUTTON1 << (n - 1),
+        _ => 0,
+    }
+}
+
 pub(super) fn modifiers(ctrl: bool, shift: bool, alt: bool) -> WPEModifiers::Type {
     let mut m: WPEModifiers::Type = 0;
     if ctrl {