Fix Desktop cursor and canvas resize handoff
Make the embedded Xpra Desktop use the browser cursor as the only visible cursor by suppressing the shadow pointer overlay and pointer-position renderer without blocking pointer input. Prefer the active Office host iframe when choosing the Desktop frame, then force resize recovery during modal-to-canvas docking so the Xpra desktop, window, and canvas refill the canvas after handoff.
Alessandro committed
May 2, 2026 at 18:05 UTC
2f5f98521d10e08ab21f4b8767110454aacf72ec
2 files changed
+76
plugins/_office/extensions/webui/right_canvas_register_surfaces/register-office.js
+13
@@ -28,11 +28,24 @@ export default async function registerOfficeSurface(canvas) {
28
icon: "desktop_windows",
29
order: 20,
30
modalPath: "/plugins/_office/webui/main.html",
31
+ async beginDockHandoff() {
32
+ const office = globalThis.Alpine?.store?.("office");
33
+ office?.beforeDesktopHostHandoff?.();
34
+ },
35
+ async finishDockHandoff(payload = {}) {
36
+ const office = globalThis.Alpine?.store?.("office");
37
+ if (payload.opened !== false) office?.afterDesktopHostShown?.({ source: "dock" });
38
+ },
39
+ async cancelDockHandoff() {
40
+ const office = globalThis.Alpine?.store?.("office");
41
+ office?.cancelDesktopHostHandoff?.();
42
+ },
43
async open(payload = {}) {
44
const panel = await waitForElement('[data-surface-id="office"] .office-panel');
45
const office = globalThis.Alpine?.store?.("office");
46
await office?.onMount?.(panel, { mode: "canvas" });
47
await office?.onOpen?.(payload);
48
+ office?.afterDesktopHostShown?.({ source: payload?.source || "canvas" });
49
},
50
async close(payload = {}) {
51
const office = globalThis.Alpine?.store?.("office");
plugins/_office/webui/office-store.js
+63
@@ -1043,6 +1043,8 @@ const model = {
1043
1044
desktopFrame(preferred = null) {
1045
if (this.isUsableDesktopFrame(preferred)) return preferred;
1046
+ const rootFrame = this._root?.querySelector?.("[data-office-desktop-frame]");
1047
+ if (this.isUsableDesktopFrame(rootFrame)) return rootFrame;
1048
const frames = this.desktopFrames();
1049
return frames
1050
.filter((frame) => this.isUsableDesktopFrame(frame))
@@ -1077,6 +1079,34 @@ const model = {
1079
}
1080
},
1081
1082
+ afterDesktopHostShown() {
1083
+ if (!this.hasOfficialOffice()) return;
1084
+ this._desktopResizeKey = "";
1085
+ this._desktopResizeSuspended = false;
1086
+ this._desktopResizePending = false;
1087
+ this.restoreDesktopFrames();
1088
+ this.requestDesktopViewportSync({ force: true, frame: this.desktopFrame() });
1089
+ for (const delay of [720, 1280]) {
1090
+ globalThis.setTimeout(() => {
1091
+ this.requestDesktopViewportSync({ force: true, frame: this.desktopFrame() });
1092
+ }, delay);
1093
+ }
1094
+ },
1095
+
1096
+ beforeDesktopHostHandoff() {
1097
+ this.stopDesktopResizeObserver();
1098
+ this.stopXpraDesktopPrime();
1099
+ this._desktopResizeKey = "";
1100
+ this._desktopResizeSuspended = true;
1101
+ this._desktopResizePending = true;
1102
+ },
1103
+
1104
+ cancelDesktopHostHandoff() {
1105
+ this._desktopResizeSuspended = false;
1106
+ this._desktopResizePending = false;
1107
+ this.requestDesktopViewportSync({ force: true, frame: this.desktopFrame() });
1108
+ },
1109
+
1110
onDesktopFrameLoaded(event = null) {
1111
if (event?.target?.getAttribute?.("src") === "about:blank") return;
1112
this.error = "";
@@ -1267,6 +1297,7 @@ const model = {
1297
const client = remoteWindow.client;
1298
if (!client) return false;
1299
this.installXpraDesktopClientPatches(remoteWindow, client);
1300
+ this.installXpraDesktopCursorPatches(remoteWindow, remoteDocument, client);
1301
this.installXpraDesktopKeyboardBridge(frame, remoteWindow, remoteDocument, client);
1302
const container = client.container || remoteDocument?.querySelector?.("#screen");
1303
if (!container) return false;
@@ -1439,6 +1470,11 @@ const model = {
1470
.windowbuttons {
1471
display: none !important;
1472
}
1473
+ #shadow_pointer {
1474
+ display: none !important;
1475
+ visibility: hidden !important;
1476
+ opacity: 0 !important;
1477
+ }
1478
.window,
1479
.window.border,
1480
.window.desktop,
@@ -1470,6 +1506,33 @@ const model = {
1506
remoteDocument.head?.appendChild(style);
1507
},
1508
1509
+ installXpraDesktopCursorPatches(remoteWindow, remoteDocument, client) {
1510
+ if (!remoteWindow || !remoteDocument || !client) return;
1511
+ const hideShadowPointer = () => {
1512
+ const pointer = remoteDocument.getElementById?.("shadow_pointer");
1513
+ pointer?.style?.setProperty("display", "none", "important");
1514
+ pointer?.style?.setProperty("visibility", "hidden", "important");
1515
+ pointer?.style?.setProperty("opacity", "0", "important");
1516
+ };
1517
+ hideShadowPointer();
1518
+
1519
+ const pointerPacket = remoteWindow.PACKET_TYPES?.pointer_position || "pointer-position";
1520
+ if (!client.__a0XpraDesktopCursorPatched) {
1521
+ if (typeof client._process_pointer_position === "function") {
1522
+ client.__a0OriginalProcessPointerPosition = client._process_pointer_position;
1523
+ }
1524
+ client._process_pointer_position = function patchedProcessPointerPosition(packet) {
1525
+ hideShadowPointer();
1526
+ this.__a0LastPointerPosition = packet;
1527
+ return false;
1528
+ };
1529
+ client.__a0XpraDesktopCursorPatched = true;
1530
+ }
1531
+ if (client.packet_handlers && pointerPacket) {
1532
+ client.packet_handlers[pointerPacket] = client._process_pointer_position;
1533
+ }
1534
+ },
1535
+
1536
installXpraDesktopFramePatches(remoteWindow, remoteDocument) {
1537
if (!remoteWindow || !remoteDocument) return;
1538
remoteWindow.__a0XpraDesktopFramePatches ||= {};