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 ||= {};