git.lucas.co / cce-mail
mail client (IMAP/SMTP)
git clone https://git.lucas.co/cce-mail.git

commit0cbb4dc0527b208baf0dc086c81371a327173f9f
parentb61d63e4d6
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-02 13:05
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 an HTML message was a hover and selected nothing,
though main.rs already routed moves to the page for exactly that. The
host now keeps the held buttons as WPE_MODIFIER_POINTER_* bits and puts
them on moves, presses and releases. Same bug, same fix as cce-browser
ac02832.

examples/wpe_select drags across a line painted with a green ::selection
and counts green pixels: 0 before, 1503 after (a double-click control
finds 496 either way).

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

 examples/wpe_select.rs | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++
 src/wpe/host.rs        | 21 ++++++++++++++--
 src/wpe/input.rs       | 10 ++++++++
 3 files changed, 96 insertions(+), 2 deletions(-)

diff --git a/examples/wpe_select.rs b/examples/wpe_select.rs
new file mode 100644
index 0000000..522ba6d
--- /dev/null
+++ b/examples/wpe_select.rs
@@ -0,0 +1,67 @@
+//! Headless check that a press-drag-release over a message selects text.
+//!
+//! WebKit decides whether a pointer move is a drag from the button bits in
+//! the move event's own modifiers; a move reporting no held button is a
+//! hover, and nothing is selected. The page paints its selection pure green,
+//! so the readback answers without any script: green pixels on the text line
+//! mean something is selected. A double-click (which needs no drag) runs
+//! first as the control that the probe can see a selection at all.
+//!
+//! `cargo run --release -p cce-mail --example wpe_select`
+
+#[path = "../src/wpe/mod.rs"]
+mod wpe;
+
+use cce_ui::widget::MouseButton;
+
+fn main() {
+    let html = r##"<!doctype html>
+<html><head><style>
+  body { margin:0; background:#ffffff; color:#000000; font:80px/120px monospace }
+  ::selection { background:#00ff00; color:#000000 }
+</style></head>
+<body><p style="margin:0">alpha bravo charlie</p></body></html>"##;
+
+    let mut view = wpe::MailWebView::new((1200, 600));
+    view.load_html(html);
+    let mut settle = |view: &mut wpe::MailWebView, n: u32| {
+        for _ in 0..n {
+            view.pump();
+            std::thread::sleep(std::time::Duration::from_millis(50));
+        }
+    };
+    settle(&mut view, 40);
+
+    let green = |view: &wpe::MailWebView| {
+        (0..120).step_by(4).flat_map(|y| (0..900).step_by(10).map(move |x| (x, y)))
+            .filter(|&(x, y)| view.sample_pixel(x, y).is_some_and(|(r, g, b)| g > 200 && r < 80 && b < 80))
+            .count()
+    };
+    println!("before: green={}", green(&view));
+    // Control: a double-click selects a word without any drag.
+    view.mouse_button_ui(MouseButton::Left, true, 100.0, 60.0);
+    view.mouse_button_ui(MouseButton::Left, false, 100.0, 60.0);
+    view.mouse_button_ui(MouseButton::Left, true, 100.0, 60.0);
+    view.mouse_button_ui(MouseButton::Left, false, 100.0, 60.0);
+    settle(&mut view, 10);
+    println!("dblclick: green={}", green(&view));
+    view.mouse_button_ui(MouseButton::Left, true, 1100.0, 400.0);
+    view.mouse_button_ui(MouseButton::Left, false, 1100.0, 400.0);
+    settle(&mut view, 10);
+    println!("cleared: green={}", green(&view));
+
+    view.mouse_move(2.0, 60.0);
+    view.mouse_button_ui(MouseButton::Left, true, 2.0, 60.0);
+    settle(&mut view, 2);
+    for x in (40..=700).step_by(60) {
+        view.mouse_move(x as f32, 60.0);
+        settle(&mut view, 1);
+    }
+    view.mouse_button_ui(MouseButton::Left, false, 700.0, 60.0);
+    settle(&mut view, 10);
+
+    let after = green(&view);
+    println!("after drag: green={after}");
+    assert!(after > 0, "drag selected nothing");
+    println!("OK: drag selected text");
+}
diff --git a/src/wpe/host.rs b/src/wpe/host.rs
index 4a4f191..876fc83 100644
--- a/src/wpe/host.rs
+++ b/src/wpe/host.rs
@@ -106,6 +106,14 @@ pub struct MailWebView {
     /// [`image`]: MailWebView::image
     /// [`reupload_frame`]: MailWebView::reupload_frame
     last_frame: Option<(Vec<u8>, u32, u32)>,
+    /// 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 a message selected nothing while every move
+    /// went out with an empty mask.
+    held_buttons: WPEModifiers::Type,
 }
 
 impl Drop for MailWebView {
@@ -185,6 +193,7 @@ impl MailWebView {
                 images_allowed: false,
                 image: None,
                 last_frame: None,
+                held_buttons: 0,
             }
         }
     }
@@ -478,7 +487,7 @@ impl MailWebView {
                 view,
                 WPEInputSource::WPE_INPUT_SOURCE_MOUSE,
                 input::now_ms(),
-                0,
+                self.held_buttons,
                 x,
                 y,
                 0.0,
@@ -503,6 +512,14 @@ impl MailWebView {
             } 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);
+            if pressed {
+                self.held_buttons |= bit;
+            } else {
+                self.held_buttons &= !bit;
+            }
             let e = wpe_event_pointer_button_new(
                 if pressed {
                     WPEEventType::WPE_EVENT_POINTER_DOWN
@@ -512,7 +529,7 @@ impl MailWebView {
                 view,
                 WPEInputSource::WPE_INPUT_SOURCE_MOUSE,
                 time,
-                0,
+                self.held_buttons,
                 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 {