Skip to content

fix(web): fit the canvas to the host element, not the window - #2006

Open
meanaverage (meanaverage) wants to merge 2 commits into
Devolutions:masterfrom
meanaverage:fix/web-component-fit-to-host
Open

meanaverage (meanaverage) wants to merge 2 commits into
Devolutions:masterfrom
meanaverage:fix/web-component-fit-to-host

Conversation

@meanaverage

Copy link
Copy Markdown
Contributor

Problem

fitResize and realResize compute the space available as window.innerWidth/innerHeight minus the wrapper's position, which assumes the component reaches the window's bottom-right corner. When <iron-remote-desktop> is embedded in anything smaller (a split pane, a sidebar, a tab next to other UI), the canvas is sized past the host's edges and gets cropped.

Change

  • New src/lib/availableArea.ts: availableAreaCorner(host) returns the host element's bottom-right corner, falling back to the window while the host has no size yet (not laid out).
  • Fit and real scaling measure to that corner (the host via $host()). Full scaling still uses the window.
  • No public API change.

Testing

  • New src/lib/availableArea.test.ts (3 tests); npm test (21 passing), npm run check (0 errors), npm run lint, npm run build.
  • In use: embedded in a desktop app pane that is smaller than the window, the canvas now fits the pane exactly (checked at several pane sizes), where it previously overflowed to the window's edges.

Side note for embedders: the component refits on window resize only, so a host that changes size on its own can call setScale(ScreenScale.Fit) from a ResizeObserver.

Prepared with AI assistance; I reviewed the change and ran the tests above.

@github-actions github-actions Bot added risk/low Self-contained change with no cross-crate behavioral effect scope/web Affects the web/WASM ecosystem size/S Size: up to 199 counted lines and 5 files; exceeds XS in either measure needs-review A human reviewer is the current next actor labels Sep 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Automated review will not run because this contributor is not yet eligible under the automation policy.

Contributors become eligible after one qualifying IronRDP pull request is merged into master. Maintainer review is required.

@github-actions github-actions Bot added risk/medium Behavioral change that does not substantially alter a core public API needs-review A human reviewer is the current next actor risk/unknown Risk could not be determined automatically; needs maintainer-level scrutiny risk/low Self-contained change with no cross-crate behavioral effect and removed risk/low Self-contained change with no cross-crate behavioral effect needs-review A human reviewer is the current next actor risk/medium Behavioral change that does not substantially alter a core public API risk/unknown Risk could not be determined automatically; needs maintainer-level scrutiny labels Sep 28, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR 2006 fixes canvas sizing when the component is embedded smaller than the window: fitResize and realResize now measure to the host element's bottom-right corner (availableAreaCorner($host())) instead of the window corner, with a window fallback while the host has no layout size; fullResize intentionally still uses the window. The new availableArea.ts module is small and pure, with tests covering host-sized, unlayouted-host, and missing-host cases. Independent inspection of the component found no correctness defects: viewport-coordinate math between wrapper.getBoundingClientRect() and the host box is consistent, host-style helpers touch inner rather than the host box, and the zero-size fallback is sound. Both specialist candidates are valid low-severity maintainability tidy-ups and are accepted; one independent low-severity consistency observation about 'full' scaling is added.

  1. [code-compressor] getWindowSize() is now a pointless pass-through wrapper — low 🟡 — web-client/iron-remote-desktop/src/iron-remote-desktop.svelte
    After this PR, getWindowSize() only forwards to windowCorner() and its single remaining caller is fullResize(). Inlining windowCorner() at that call site and deleting the wrapper removes a one-line indirection with no behavior change, since windowCorner's name already conveys intent.
  2. [general] Full scaling still measures to the window, overflowing embedded hosts — low 🟡 — web-client/iron-remote-desktop/src/iron-remote-desktop.svelte
    The PR fixes fit/real to stay within the host, but fullResize still sizes the wrapper to window.innerWidth/innerHeight. In an embedder pane smaller than the window, selecting 'full' can size the wrapper past the host edges — the same cropping problem this PR fixes for the other modes. Possibly intentional (fill the viewport), but inconsistent for embedded use and worth confirming or documenting.

Comment on lines +7 to +31
export function windowCorner(win: Window = window): Corner {
const docElem = win.document.documentElement;
const body = win.document.getElementsByTagName('body')[0];
return {
x: win.innerWidth ?? docElem.clientWidth ?? body.clientWidth,
y: win.innerHeight ?? docElem.clientHeight ?? body.clientHeight,
};
}

/**
* Bottom-right corner, in viewport coordinates, of the area the component may draw in.
*
* The fit and real scalings measure from the component's wrapper to this corner. When the component is
* embedded in something smaller than the window (a split pane, a sidebar), that is the host element's box,
* not the window: measuring to the window's corner would size the canvas past the host's edges. A host
* without a size yet (not laid out) falls back to the window.
*/
export function availableAreaCorner(host: Element | null | undefined, win: Window = window): Corner {
if (host) {
const box = host.getBoundingClientRect();
if (box.width > 0 && box.height > 0) {
return { x: box.right, y: box.bottom };
}
}
return windowCorner(win);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[code-compressor] Speculative win parameter is never supplied by any caller — low 🟡 — windowCorner and availableAreaCorner accept `win: Window = window`, but no caller (component or tests) passes a custom window. Removing the parameter and using the global directly drops dead plumbing with identical behavior; the only tradeoff is slightly less convenient window mocking, which existing tests do not use.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 61dbce8: dropped the win parameter, and inlined getWindowSize() into fullResize(), its only remaining caller.

On full scaling still measuring to the window: that is deliberate, full fills the viewport, as before. This PR only changes fit and real, the two that are meant to stay inside the component.

@github-actions github-actions Bot added the ai-reviewed/1 One automated review completed label Sep 29, 2026
@github-actions github-actions Bot added risk/medium Behavioral change that does not substantially alter a core public API and removed risk/low Self-contained change with no cross-crate behavioral effect labels Oct 1, 2026
The fit and real scalings measured the space available from the wrapper to
the window's bottom-right corner, which assumes the component reaches the
window's edges. Embedded in anything smaller (a split pane, a sidebar), the
canvas was sized past the host's edges and got cropped.

Measure to the host element's box instead (falling back to the window while
the host has no size). The full scaling still uses the window.
… wrapper

No caller passes a window to windowCorner or availableAreaCorner, and
getWindowSize only forwarded to windowCorner for the full scaling.
@meanaverage
meanaverage (meanaverage) force-pushed the fix/web-component-fit-to-host branch from 61dbce8 to d4edfa6 Compare October 1, 2026 18:22
@github-actions github-actions Bot added automation-failed Exact-head automated classification or review failed or was unavailable risk/unknown Risk could not be determined automatically; needs maintainer-level scrutiny and removed risk/medium Behavioral change that does not substantially alter a core public API labels Oct 1, 2026
@CBenoit

Benoît Cortier (CBenoit) commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

PR automation is failing because of a picky-krb 0.12.5 incompatibility, fixed on master by #2074. Please rebase on master to fix it.

Update: no rebase needed anymore. picky-krb 0.12.5 was yanked from crates.io (re-released as 0.13.0), so the API check builds again without changes to this branch. PR automation has been re-run here and passes.

@github-actions github-actions Bot added the risk/low Self-contained change with no cross-crate behavioral effect label Oct 7, 2026
@github-actions github-actions Bot removed risk/unknown Risk could not be determined automatically; needs maintainer-level scrutiny automation-failed Exact-head automated classification or review failed or was unavailable labels Oct 7, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR #2006 fixes fit/real scaling to measure available space from the host element's bottom-right corner instead of the window, so the canvas fits embedded contexts (split panes, sidebars). Verified independently in pr-head: availableAreaCorner($host()) preserves old behavior whenever the host is absent or has zero size (identical windowCorner() fallback chain), fullResize deliberately stays on the window viewport, and viewport-coordinate arithmetic (box.right/bottom minus wrapper x/y) is consistent. The initial resize runs 150ms after visibility, after layout, so the zero-size fallback is a safety net. No correctness or API issues found; the two code-compressor candidates are valid, low-severity, behavior-preserving style simplifications and are published unchanged.

  1. [code-compressor] getAvailableCorner() is a one-line indirection; inline availableAreaCorner($host()) at its two callers — low 🟡 — web-client/iron-remote-desktop/src/iron-remote-desktop.svelte
    The wrapper adds a named function plus a comment whose content (fit/real measure within the host element) is already stated in availableAreaCorner's doc-comment, to adapt a single call. With only two call sites (fitResize and realResize), replacing getAvailableCorner() with availableAreaCorner($host()) removes four lines and one hop while preserving behavior exactly; $host() makes the host argument self-explanatory at the call site. Verified behavior-preserving in pr-head. Tradeoff: call sites get slightly longer and lose the local comment, but the lib documentation carries the same rationale.

Comment on lines +7 to +14
export function windowCorner(): Corner {
const docElem = document.documentElement;
const body = document.getElementsByTagName('body')[0];
return {
x: window.innerWidth ?? docElem.clientWidth ?? body.clientWidth,
y: window.innerHeight ?? docElem.clientHeight ?? body.clientHeight,
};
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[code-compressor] Reach the body via document.body instead of getElementsByTagName('body')[0] — low 🟡 — The new windowCorner() relocates the old lookup verbatim, including document.getElementsByTagName('body')[0]. document.body returns the same element with one call and no indexing, and has identical null behavior (if body is missing, the property access on the fallback chain's last operand throws the same way in both forms; the docElem ??-fallbacks are unaffected). Verified in pr-head. An optional simplification of moved code, not a defect; keeping it verbatim is also defensible for a pure relocation.

@github-actions github-actions Bot added ai-reviewed/2 Two automated reviews completed needs-author-action The pull request author is the current next actor and removed ai-reviewed/1 One automated review completed labels Oct 7, 2026

This branch was successfully deployed

1 active deployment
llm-providers — d4edfa69 Deployed Oct 1, 2026 by meanaverage via Classify pull request #1564
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-reviewed/2 Two automated reviews completed needs-author-action The pull request author is the current next actor risk/low Self-contained change with no cross-crate behavioral effect scope/web Affects the web/WASM ecosystem size/S Size: up to 199 counted lines and 5 files; exceeds XS in either measure

Development

Successfully merging this pull request may close these issues.

2 participants