Files
rustguac/tests/browser/monitor-pointer.html
T
pletch 010f41d15a feat(mouse): replay the moves the browser coalesced away during a drag
A busy main thread does not delay pointer movement, it discards it: moves
arriving while a handler runs are merged into one, and what is dispatched
afterwards carries only the last position. A drag feels that immediately --
the path between two points is gone, so a dragged scrollbar thumb is dropped
rather than merely lagged -- while a hover or a video degrades gracefully
under the same load.

getCoalescedEvents() returns what was merged. Replaying it in order restores
the path, though not the timing: nothing running on a blocked thread can do
that, and the positions still arrive together once the thread is free. What
changes is that the remote end sees where the pointer went rather than only
where it ended up.

Deliberately additive. The mousemove listener is untouched and still delivers
every position it did before, so touch, the compatibility mouse events it
produces and the counter that swallows them all behave as they did; the
dispatched position arrives twice and the second is dropped by the
unchanged-coordinates check already in move(). Migrating this file to pointer
events outright is the tidier shape and a far larger change -- every listener
would have to move together, or synthetic events from touch would arrive with
nothing left counting them down.

Only while a button is held, because the path matters for a drag and the
endpoint is all that matters for a hover, and replaying every merged move of
an idle pointer would multiply a session's upstream mouse traffic for movement
nobody can perceive. Capped and evenly sampled, because a pointer reporting at
1000Hz behind a thread blocked for 100ms offers a hundred positions for one
drag, and answering a rendering problem with a traffic one is no answer. An
ordinary pointer never reaches the cap.

The pop-out monitor windows get the same replay. They bypass Guacamole.Mouse
entirely -- it does not track the X axis correctly in a popup -- and map the
pointer themselves, so a drag on a second head would otherwise lose its path
exactly as the first one did. The sampler moves to a static so there is one
definition of the rule rather than two that can drift, and that path needs one
thing the main display does not: it sends unconditionally, where move() drops
an unchanged position, so it keeps what it last sent and drops a repeat --
keyed on the buttons as well as the position, since a press and a release
happen at one place and are not repeats of each other. Feature-detected on the
event rather than on PointerEvent, because a pop-out window is a realm of its
own and the constructor this page can see is not the one that made the event
it is handed.

That pointer handling lives in Guacamole.MonitorPointer rather than inline in
client.html, because these windows cannot be opened without a live
multi-monitor session: while it sat in the page the only thing a test could do
was match the source text and hope, and the two things most worth checking
there are a coordinate mapping and a suppression rule -- exactly the kind that
look right in a diff and are off by a term. It takes the canvas, a function
returning this monitor's rectangle in the combined framebuffer (a call rather
than a captured value, since the layout changes under a running window) and a
function to send a state.

Writing those tests found a real defect in the wheel, which sends a momentary
press and release: it built one state object, sent it, mutated it and sent it
again, which works only while every consumer copies synchronously. It sends
two states now.

Tested rather than tried, because this path only runs once the browser has
already thrown something away: the case it exists for is the one that cannot
be reliably produced, and a broken version feels like the behaviour it
replaced. Twenty tests cover the mapping, the button mask, the repeat
suppression, the wheel's reset of it and the replay, plus a harness driving
real pointer input through the debugger with the main thread deliberately
blocked -- because the unit tests feed events shaped the way the code expects,
and that assumption is the one worth checking against a browser.

(cherry picked from commit d8ce743fe0801bd8c776281ee5541abf8040a339)
2026-10-03 13:00:09 +10:00

70 lines
2.7 KiB
HTML

<!doctype html>
<meta charset="utf-8">
<title>pop-out monitor pointer, in a real browser</title>
<style>
body { background:#111; color:#ddd; font:13px/1.6 ui-monospace,monospace; margin:0; padding:12px; }
#screen { position:absolute; left:100px; top:80px; width:800px; height:400px;
background:#234; display:block; }
#out { position:absolute; top:500px; left:12px; white-space:pre; }
</style>
<div>drag across the blue area &mdash; real PointerEvents, real coalescing</div>
<canvas id="screen" width="1920" height="1080"></canvas>
<div id="out">&hellip;</div>
<script src="/guac/Namespace.js"></script>
<script src="/guac/Event.js"></script>
<script src="/guac/Position.js"></script>
<script src="/guac/Mouse.js"></script>
<script src="/guac/MonitorPointer.js"></script>
<script>
/*
* Guacamole.MonitorPointer against the browser's own PointerEvents, which is
* the half tests/monitor-pointer.mjs cannot reach: there the coalesced events
* are fakes shaped the way the code expects them, and whether the browser
* really merges moves under load -- and what it puts in the list when it does
* -- is its business and not something to take on trust from a stub.
*
* Driven by tests/browser/drive-pointer.mjs.
*/
const canvas = document.getElementById('screen');
/* The monitor this stands for: a second head, offset in the combined
* framebuffer, and a different size from the canvas showing it, so a mapping
* that drops a term cannot land on the right answer by luck. */
const rect = { left: 2560, top: 0, width: 1920, height: 1080 };
const sent = [];
window.__sent = sent;
window.__maxCoalesced = 0;
/* Captured ahead of the module's own listener, purely to record what the
* browser handed over. */
canvas.addEventListener('pointermove', function (e) {
if (e.getCoalescedEvents)
window.__maxCoalesced = Math.max(window.__maxCoalesced,
e.getCoalescedEvents().length);
}, true);
Guacamole.MonitorPointer(canvas, function () { return rect; }, function (state) {
sent.push({
x : Math.round(state.x), y : Math.round(state.y),
left : state.left, middle : state.middle, right : state.right,
up : state.up, down : state.down
});
document.getElementById('out').textContent =
sent.length + ' sent, max coalesced ' + window.__maxCoalesced + '\n'
+ sent.slice(-6).map(function (s) { return JSON.stringify(s); }).join('\n');
});
/* Something expensive on the main thread, so moves arriving while it runs are
* merged rather than delivered. Without this there is nothing to recover and
* the run proves nothing. */
window.__block = function (ms) {
var until = performance.now() + ms;
while (performance.now() < until) { /* deliberately */ }
};
</script>