cat / goq
clone

turns a linux box into a private steam streaming appliance: plug in, tap your key, play from another computer

Merge pull request #125 from FelineStateMachine/feat/preserve-portal-window-size

FelineStateMachine committed 92ca5e5
fix(portal): preserve user-selected window geometry
commit
92ca5e5ef25ecd7074b59f16447c0b613fdbd589
parent
2d0a9111e43ae54adda626587fe2fe202cb9d380
parent
ba53bcbc531b18293e2fd635577faa3e0930bb50
author
FelineStateMachine <dami.etoile@gmail.com>
10 files changed, +93 -590
MAGENTS.md5+-
MCargo.lock16++
Mportal/main.js22+---
Mportal/window-geometry.mjs91+----------
Mportal/window-geometry.test.mjs102++++---------
Mportal/window-runtime.mjs130+---------------
Mportal/window-runtime.test.mjs251+++---------------------------
Msrc-tauri/Cargo.toml1+
Msrc-tauri/src/commands/state.rs60-------
Msrc-tauri/src/main.rs5+-
M AGENTS.md +3 -2
M Cargo.lock +16 -0
M portal/main.js +1 -21
@@ -467,7 +467,7 @@ async function showConnectedAttempt({ result, attempt }) {
467467 document.getElementById('frame-canvas').style.display = 'block';468468 document.getElementById('placeholder').classList.add('hidden');469469 document.getElementById('control-bar').classList.add('visible');470- void windowRuntime.fitWindowToIncomingStream();470+ windowRuntime.sizeSurfaceToIncomingStream();471471 updateControlUI();472472 // Land controller users on the deliberate view-only/control boundary.473473 // The connect button is hidden at this point, so retaining its focus
@@ -497,7 +497,6 @@ async function teardownConnectedResources() {
497497 }498498 499499 async function showDisconnectedState() {500- windowRuntime.reset();501500 controlRuntime.resetDisconnected();502501 updatePanelVisibility();503502 setStatus('', 'idle');
@@ -589,19 +588,7 @@ const canvas = document.getElementById('frame-canvas');
589588 const remotePointer = document.getElementById('remote-pointer');590589 const ctx = canvas.getContext('2d');591590 const windowRuntime = createWindowRuntime({592- isConnected: () => connectionState.connected,593591 getFormat: () => videoPipeline.format,594- getGeneration: () => streamRuntime.generation,595- getChromeHeight: () => {596- const topbar = document.querySelector('.topbar');597- const bottombar = document.querySelector('.bottombar');598- return Math.max(0, (topbar?.offsetHeight ?? 0) + (bottombar?.offsetHeight ?? 0));599- },600- getScreenBounds: () => ({601- width: window.screen.availWidth,602- height: window.screen.availHeight,603- }),604- getWindowSize: () => ({ width: window.innerWidth, height: window.innerHeight }),605592 getSurfaceBounds: () => {606593 const main = canvas.parentElement;607594 if (!main) return null;
@@ -612,11 +599,6 @@ const windowRuntime = createWindowRuntime({
612599 canvas.style.width = `${width}px`;613600 canvas.style.height = `${height}px`;614601 },615- applyNativeGeometry: (geometry, unmaximize) => invoke('set_client_window_size', {616- logicalWidth: geometry.width,617- logicalHeight: geometry.height,618- unmaximize,619- }),620602 });621603 controlRuntime = createControlRuntime({622604 getConnection: () => ({
@@ -654,7 +636,6 @@ videoPipeline = createVideoPipelineSession({
654636 sampleAudioSkew: audioPipeline.sampleSkew,655637 onFormatChanged: () => {656638 windowRuntime.sizeSurfaceToIncomingStream();657- if (connectionState.connected) void windowRuntime.fitWindowToIncomingStream();658639 },659640 });660641 streamRuntime = createStreamRuntime({
@@ -1185,7 +1166,6 @@ document.addEventListener('pointerdown', () => {
11851166 window.addEventListener('resize', () => {11861167 windowRuntime.sizeSurfaceToIncomingStream();11871168 renderRemotePointer();1188- windowRuntime.scheduleAspectCorrection();11891169 });11901170 11911171 window.addEventListener('focus', () => {
M portal/window-geometry.mjs +7 -84
@@ -1,99 +1,22 @@
1-const MIN_WINDOW_WIDTH = 480;2-const MIN_STREAM_HEIGHT = 270;3-const MAX_WINDOW_DIMENSION = 8192;1+const MAX_SURFACE_DIMENSION = 8192;42 53 function positiveFinite(value, name) {64 if (!Number.isFinite(value) || value <= 0) throw new RangeError(`${name} must be positive`);75 return value;86 }97 10-function geometryInputs({ frameWidth, frameHeight, chromeHeight }) {11- const width = positiveFinite(frameWidth, 'frame width');12- const height = positiveFinite(frameHeight, 'frame height');13- const chrome = Number(chromeHeight);14- if (!Number.isFinite(chrome) || chrome < 0 || chrome > 512) {15- throw new RangeError('window chrome height is invalid');16- }17- return { ratio: width / height, chrome };18-}19-20-function roundedGeometry(width, height) {8+function roundedSurface(width, height) {219 return Object.freeze({22- width: Math.round(Math.min(MAX_WINDOW_DIMENSION, width)),23- height: Math.round(Math.min(MAX_WINDOW_DIMENSION, height)),10+ width: Math.round(Math.min(MAX_SURFACE_DIMENSION, width)),11+ height: Math.round(Math.min(MAX_SURFACE_DIMENSION, height)),2412 });2513 }2614 27-export function streamAspectKey(frameWidth, frameHeight) {15+export function fitStreamSurface({ frameWidth, frameHeight, availableWidth, availableHeight }) {2816 const width = positiveFinite(frameWidth, 'frame width');2917 const height = positiveFinite(frameHeight, 'frame height');30- if (!Number.isSafeInteger(width) || !Number.isSafeInteger(height)) {31- throw new RangeError('stream dimensions must be exact integers');32- }33- let a = width;34- let b = height;35- while (b !== 0) [a, b] = [b, a % b];36- return `${width / a}:${height / a}`;37-}38-39-export function fitInitialStreamWindow({40- frameWidth,41- frameHeight,42- chromeHeight,43- availableWidth,44- availableHeight,45- fill = 0.88,46-}) {47- const { ratio, chrome } = geometryInputs({ frameWidth, frameHeight, chromeHeight });48- const screenWidth = positiveFinite(availableWidth, 'available width');49- const screenHeight = positiveFinite(availableHeight, 'available height');50- if (!Number.isFinite(fill) || fill < 0.5 || fill > 1) throw new RangeError('window fill is invalid');51-52- const widthLimit = screenWidth * fill;53- const heightLimit = screenHeight * fill;54- const streamHeightLimit = Math.max(1, heightLimit - chrome);55- let width = Math.min(widthLimit, streamHeightLimit * ratio);56- const minimumWidth = Math.min(MIN_WINDOW_WIDTH, widthLimit, streamHeightLimit * ratio);57- width = Math.max(minimumWidth, width);58- return roundedGeometry(width, chrome + width / ratio);59-}60-61-export function constrainStreamWindowResize({62- frameWidth,63- frameHeight,64- chromeHeight,65- width,66- height,67- previousWidth,68- previousHeight,69-}) {70- const { ratio, chrome } = geometryInputs({ frameWidth, frameHeight, chromeHeight });71- const currentWidth = positiveFinite(width, 'window width');72- const currentHeight = positiveFinite(height, 'window height');73- const oldWidth = positiveFinite(previousWidth, 'previous window width');74- const oldHeight = positiveFinite(previousHeight, 'previous window height');75- const streamHeight = Math.max(1, currentHeight - chrome);76- const expectedHeight = chrome + currentWidth / ratio;77- if (Math.abs(expectedHeight - currentHeight) <= 1) return null;78-79- const widthDelta = Math.abs(currentWidth - oldWidth);80- const heightDelta = Math.abs(currentHeight - oldHeight);81- if (widthDelta >= heightDelta) {82- const adjustedWidth = Math.max(Math.min(MAX_WINDOW_DIMENSION, currentWidth), MIN_WINDOW_WIDTH);83- return roundedGeometry(adjustedWidth, chrome + adjustedWidth / ratio);84- }85-86- const adjustedStreamHeight = Math.max(87- Math.min(MAX_WINDOW_DIMENSION - chrome, streamHeight),88- MIN_STREAM_HEIGHT,89- );90- return roundedGeometry(adjustedStreamHeight * ratio, chrome + adjustedStreamHeight);91-}92-93-export function fitStreamSurface({ frameWidth, frameHeight, availableWidth, availableHeight }) {94- const { ratio } = geometryInputs({ frameWidth, frameHeight, chromeHeight: 0 });9518 const widthLimit = positiveFinite(availableWidth, 'available surface width');9619 const heightLimit = positiveFinite(availableHeight, 'available surface height');97- const width = Math.min(widthLimit, heightLimit * ratio);98- return roundedGeometry(width, width / ratio);20+ const fittedWidth = Math.min(widthLimit, heightLimit * (width / height));21+ return roundedSurface(fittedWidth, fittedWidth / (width / height));9922 }
M portal/window-geometry.test.mjs +30 -72
@@ -1,93 +1,51 @@
11 import assert from 'node:assert/strict';22 import test from 'node:test';33 4-import {5- constrainStreamWindowResize,6- fitInitialStreamWindow,7- fitStreamSurface,8- streamAspectKey,9-} from './window-geometry.mjs';4+import { fitStreamSurface } from './window-geometry.mjs';105 11-test('equivalent stream resolutions share one stable aspect identity', () => {12- assert.equal(streamAspectKey(1280, 800), '8:5');13- assert.equal(streamAspectKey(640, 400), '8:5');14- assert.notEqual(streamAspectKey(1280, 720), streamAspectKey(1280, 800));15-});16-17-test('initial stream window fits the display while preserving the drawable aspect', () => {18- const geometry = fitInitialStreamWindow({19- frameWidth: 1280,20- frameHeight: 800,21- chromeHeight: 64,22- availableWidth: 1440,23- availableHeight: 900,24- });25-26- assert.deepEqual(geometry, { width: 1165, height: 792 });27- assert.ok(Math.abs((geometry.height - 64) - geometry.width / 1.6) <= 1);28-});29-30-test('width-led resize corrects height and height-led resize corrects width', () => {31- assert.deepEqual(constrainStreamWindowResize({6+test('stream surface scales above native resolution without changing aspect ratio', () => {7+ assert.deepEqual(fitStreamSurface({328 frameWidth: 1280,339 frameHeight: 800,34- chromeHeight: 64,35- width: 1000,36- height: 700,37- previousWidth: 900,38- previousHeight: 700,39- }), { width: 1000, height: 689 });10+ availableWidth: 1920,11+ availableHeight: 1200,12+ }), { width: 1920, height: 1200 });4013 41- assert.deepEqual(constrainStreamWindowResize({14+ assert.deepEqual(fitStreamSurface({4215 frameWidth: 1280,4316 frameHeight: 800,44- chromeHeight: 64,45- width: 1000,46- height: 800,47- previousWidth: 1000,48- previousHeight: 700,49- }), { width: 1178, height: 800 });17+ availableWidth: 1920,18+ availableHeight: 1080,19+ }), { width: 1728, height: 1080 });5020 });5121 52-test('already-correct geometry is stable and malformed dimensions fail closed', () => {53- assert.equal(constrainStreamWindowResize({54- frameWidth: 16,55- frameHeight: 10,56- chromeHeight: 64,57- width: 1280,58- height: 864,59- previousWidth: 1200,60- previousHeight: 814,61- }), null);22+test('arbitrary portrait and ultrawide host resolutions letterbox inside user bounds', () => {23+ assert.deepEqual(fitStreamSurface({24+ frameWidth: 800,25+ frameHeight: 1280,26+ availableWidth: 1920,27+ availableHeight: 1080,28+ }), { width: 675, height: 1080 });6229 63- assert.throws(() => fitInitialStreamWindow({64- frameWidth: 0,65- frameHeight: 800,66- chromeHeight: 64,67- availableWidth: 1440,68- availableHeight: 900,69- }), /frame width/);70- assert.throws(() => fitInitialStreamWindow({71- frameWidth: 1280,72- frameHeight: 800,73- chromeHeight: 900,74- availableWidth: 1440,30+ assert.deepEqual(fitStreamSurface({31+ frameWidth: 3440,32+ frameHeight: 1440,33+ availableWidth: 1200,7534 availableHeight: 900,76- }), /chrome/);35+ }), { width: 1200, height: 502 });7736 });7837 79-test('stream surface scales above native resolution without changing aspect ratio', () => {80- assert.deepEqual(fitStreamSurface({81- frameWidth: 1280,38+test('malformed dimensions fail closed before changing the surface', () => {39+ assert.throws(() => fitStreamSurface({40+ frameWidth: 0,8241 frameHeight: 800,8342 availableWidth: 1920,84- availableHeight: 1200,85- }), { width: 1920, height: 1200 });86-87- assert.deepEqual(fitStreamSurface({43+ availableHeight: 1080,44+ }), /frame width/);45+ assert.throws(() => fitStreamSurface({8846 frameWidth: 1280,8947 frameHeight: 800,90- availableWidth: 1920,48+ availableWidth: Number.NaN,9149 availableHeight: 1080,92- }), { width: 1728, height: 1080 });50+ }), /available surface width/);9351 });
M portal/window-runtime.mjs +2 -128
@@ -1,46 +1,19 @@
1-import {2- constrainStreamWindowResize,3- fitInitialStreamWindow,4- fitStreamSurface,5- streamAspectKey,6-} from './window-geometry.mjs';7-8-export const STREAM_WINDOW_CORRECTION_DELAY_MS = 80;1+import { fitStreamSurface } from './window-geometry.mjs';92 103 export function createWindowRuntime({11- isConnected,124 getFormat,13- getGeneration,14- getChromeHeight,15- getScreenBounds,16- getWindowSize,175 getSurfaceBounds,186 setSurfaceSize,19- applyNativeGeometry,20- scheduleTimeout = globalThis.setTimeout,21- cancelTimeout = globalThis.clearTimeout,22- logger = console,237 } = {}) {248 const requiredCallbacks = {25- isConnected,269 getFormat,27- getGeneration,28- getChromeHeight,29- getScreenBounds,30- getWindowSize,3110 getSurfaceBounds,3211 setSurfaceSize,33- applyNativeGeometry,3412 };3513 for (const [name, callback] of Object.entries(requiredCallbacks)) {3614 if (typeof callback !== 'function') throw new TypeError(`${name} must be a function`);3715 }3816 39- let fittedStreamAspect = null;40- let pendingStreamFit = null;41- let lastObservedWindowSize = null;42- let streamWindowResizeTimer = null;43-4417 function sizeSurfaceToIncomingStream() {4518 const { width: frameWidth, height: frameHeight } = getFormat();4619 if (frameWidth < 1 || frameHeight < 1) return false;
@@ -56,104 +29,5 @@ export function createWindowRuntime({
5629 return true;5730 }5831 59- async function applyWindowGeometry(geometry, unmaximize) {60- try {61- const applied = await applyNativeGeometry(geometry, unmaximize);62- if (applied) lastObservedWindowSize = { ...geometry };63- return applied;64- } catch (error) {65- logger.warn('could not apply stream window geometry:', error);66- return false;67- }68- }69-70- async function fitWindowToIncomingStream() {71- const {72- width: frameWidth,73- height: frameHeight,74- epoch: activeVideoFormatEpoch,75- } = getFormat();76- if (!isConnected() || frameWidth < 1 || frameHeight < 1) return false;77- const aspect = streamAspectKey(frameWidth, frameHeight);78- if (fittedStreamAspect === aspect || pendingStreamFit?.aspect === aspect) return false;79- const request = { aspect, generation: getGeneration(), epoch: activeVideoFormatEpoch };80- pendingStreamFit = request;81- const screen = getScreenBounds();82- const geometry = fitInitialStreamWindow({83- frameWidth,84- frameHeight,85- chromeHeight: getChromeHeight(),86- availableWidth: screen.width,87- availableHeight: screen.height,88- });89- const applied = await applyWindowGeometry(geometry, true);90- const currentFormat = getFormat();91- const currentAspect = currentFormat.width > 0 && currentFormat.height > 092- ? streamAspectKey(currentFormat.width, currentFormat.height)93- : null;94- const current = pendingStreamFit === request95- && isConnected()96- && getGeneration() === request.generation97- && currentFormat.epoch === request.epoch98- && currentAspect === request.aspect;99- if (pendingStreamFit === request) pendingStreamFit = null;100- if (applied && current) fittedStreamAspect = request.aspect;101- return applied && current;102- }103-104- function scheduleAspectCorrection() {105- const observed = getWindowSize();106- const previous = lastObservedWindowSize ?? observed;107- lastObservedWindowSize = observed;108- if (streamWindowResizeTimer !== null) cancelTimeout(streamWindowResizeTimer);109- streamWindowResizeTimer = scheduleTimeout(() => {110- streamWindowResizeTimer = null;111- const { width: frameWidth, height: frameHeight } = getFormat();112- if (!isConnected() || frameWidth < 1 || frameHeight < 1) return;113- const current = getWindowSize();114- let geometry;115- try {116- geometry = constrainStreamWindowResize({117- frameWidth,118- frameHeight,119- chromeHeight: getChromeHeight(),120- width: current.width,121- height: current.height,122- previousWidth: previous.width,123- previousHeight: previous.height,124- });125- } catch (error) {126- logger.warn('could not constrain stream window geometry:', error);127- return;128- }129- if (geometry !== null) void applyWindowGeometry(geometry, false);130- }, STREAM_WINDOW_CORRECTION_DELAY_MS);131- }132-133- function reset() {134- fittedStreamAspect = null;135- pendingStreamFit = null;136- lastObservedWindowSize = null;137- if (streamWindowResizeTimer !== null) cancelTimeout(streamWindowResizeTimer);138- streamWindowResizeTimer = null;139- }140-141- function snapshot() {142- return Object.freeze({143- fittedStreamAspect,144- pendingStreamFit: pendingStreamFit === null ? null : Object.freeze({ ...pendingStreamFit }),145- lastObservedWindowSize: lastObservedWindowSize === null146- ? null : Object.freeze({ ...lastObservedWindowSize }),147- correctionScheduled: streamWindowResizeTimer !== null,148- });149- }150-151- return Object.freeze({152- applyWindowGeometry,153- fitWindowToIncomingStream,154- reset,155- scheduleAspectCorrection,156- sizeSurfaceToIncomingStream,157- snapshot,158- });32+ return Object.freeze({ sizeSurfaceToIncomingStream });15933 }
M portal/window-runtime.test.mjs +29 -222
@@ -2,89 +2,45 @@ import assert from 'node:assert/strict';
22 import { readFile } from 'node:fs/promises';33 import test from 'node:test';44 5-import {6- STREAM_WINDOW_CORRECTION_DELAY_MS,7- createWindowRuntime,8-} from './window-runtime.mjs';5+import { createWindowRuntime } from './window-runtime.mjs';96 107 const main = await readFile(new URL('./main.js', import.meta.url), 'utf8');11-12-function deferred() {13- let resolve;14- let reject;15- const promise = new Promise((resolvePromise, rejectPromise) => {16- resolve = resolvePromise;17- reject = rejectPromise;18- });19- return { promise, resolve, reject };20-}21-22-function newHarness({ applyNativeGeometry } = {}) {23- let connected = true;24- let generation = 7;8+const nativeMain = await readFile(9+ new URL('../src-tauri/src/main.rs', import.meta.url),10+ 'utf8',11+);12+const nativeState = await readFile(13+ new URL('../src-tauri/src/commands/state.rs', import.meta.url),14+ 'utf8',15+);16+const nativeCargo = await readFile(17+ new URL('../src-tauri/Cargo.toml', import.meta.url),18+ 'utf8',19+);20+21+function newHarness() {2522 let format = { width: 1280, height: 800, epoch: 3 };26- let chromeHeight = 64;27- let screenBounds = { width: 1440, height: 900 };28- let windowSize = { width: 1000, height: 700 };2923 let surfaceBounds = { width: 1920, height: 1080 };3024 const surfaceSizes = [];31- const nativeApplications = [];32- const warnings = [];33- const timers = new Map();34- const cancelled = [];35- let nextTimerId = 1;3625 const runtime = createWindowRuntime({37- isConnected: () => connected,3826 getFormat: () => format,39- getGeneration: () => generation,40- getChromeHeight: () => chromeHeight,41- getScreenBounds: () => screenBounds,42- getWindowSize: () => windowSize,4327 getSurfaceBounds: () => surfaceBounds,4428 setSurfaceSize: (geometry) => surfaceSizes.push(geometry),45- applyNativeGeometry: applyNativeGeometry ?? (async (geometry, unmaximize) => {46- nativeApplications.push({ geometry, unmaximize });47- return true;48- }),49- scheduleTimeout: (callback, delay) => {50- const id = nextTimerId;51- nextTimerId += 1;52- timers.set(id, { callback, delay });53- return id;54- },55- cancelTimeout: (id) => {56- cancelled.push(id);57- timers.delete(id);58- },59- logger: { warn: (...args) => warnings.push(args) },6029 });6130 return {6231 runtime,6332 surfaceSizes,64- nativeApplications,65- warnings,66- timers,67- cancelled,68- set connected(value) { connected = value; },69- set generation(value) { generation = value; },7033 set format(value) { format = value; },71- set chromeHeight(value) { chromeHeight = value; },72- set screenBounds(value) { screenBounds = value; },73- set windowSize(value) { windowSize = value; },7434 set surfaceBounds(value) { surfaceBounds = value; },7535 };7636 }7737 78-async function flushMicrotasks() {79- await Promise.resolve();80- await Promise.resolve();81-}82-83-test('surface sizing scales to available bounds without owning DOM style', () => {38+test('surface sizing scales to available bounds without changing native window geometry', () => {8439 const harness = newHarness();8540 8641 assert.equal(harness.runtime.sizeSurfaceToIncomingStream(), true);8742 assert.deepEqual(harness.surfaceSizes, [{ width: 1728, height: 1080 }]);43+ assert.deepEqual(Object.keys(harness.runtime), ['sizeSurfaceToIncomingStream']);8844 8945 harness.surfaceBounds = null;9046 assert.equal(harness.runtime.sizeSurfaceToIncomingStream(), false);
@@ -94,165 +50,16 @@ test('surface sizing scales to available bounds without owning DOM style', () =>
9450 assert.equal(harness.surfaceSizes.length, 1);9551 });9652 97-test('initial fit applies once per stable aspect and records the observed native result', async () => {98- const harness = newHarness();99-100- assert.equal(await harness.runtime.fitWindowToIncomingStream(), true);101- assert.deepEqual(harness.nativeApplications, [{102- geometry: { width: 1165, height: 792 },103- unmaximize: true,104- }]);105- assert.deepEqual(harness.runtime.snapshot(), {106- fittedStreamAspect: '8:5',107- pendingStreamFit: null,108- lastObservedWindowSize: { width: 1165, height: 792 },109- correctionScheduled: false,110- });111- assert.equal(await harness.runtime.fitWindowToIncomingStream(), false);112- assert.equal(harness.nativeApplications.length, 1);113-});114-115-test('same-aspect fits are single-flight while native sizing is pending', async () => {116- const native = deferred();117- let applications = 0;118- const harness = newHarness({119- applyNativeGeometry: async () => {120- applications += 1;121- return native.promise;122- },123- });124-125- const first = harness.runtime.fitWindowToIncomingStream();126- assert.equal(await harness.runtime.fitWindowToIncomingStream(), false);127- assert.equal(applications, 1);128- native.resolve(true);129- assert.equal(await first, true);130-});131-132-test('generation and format epoch protect against stale async fit completion', async () => {133- const native = deferred();134- const harness = newHarness({ applyNativeGeometry: async () => native.promise });135- const fitting = harness.runtime.fitWindowToIncomingStream();136- harness.generation = 8;137- harness.format = { width: 1280, height: 800, epoch: 4 };138- native.resolve(true);139-140- assert.equal(await fitting, false);141- assert.deepEqual(harness.runtime.snapshot(), {142- fittedStreamAspect: null,143- pendingStreamFit: null,144- lastObservedWindowSize: { width: 1165, height: 792 },145- correctionScheduled: false,146- });147-});148-149-test('an older completion cannot clear or commit a newer aspect request', async () => {150- const firstNative = deferred();151- const secondNative = deferred();152- let applications = 0;153- const harness = newHarness({154- applyNativeGeometry: async () => {155- applications += 1;156- return applications === 1 ? firstNative.promise : secondNative.promise;157- },158- });159- const first = harness.runtime.fitWindowToIncomingStream();160- harness.format = { width: 1280, height: 720, epoch: 4 };161- const second = harness.runtime.fitWindowToIncomingStream();162-163- firstNative.resolve(true);164- assert.equal(await first, false);165- assert.deepEqual(harness.runtime.snapshot().pendingStreamFit, {166- aspect: '16:9',167- generation: 7,168- epoch: 4,169- });170- secondNative.resolve(true);171- assert.equal(await second, true);172- assert.equal(harness.runtime.snapshot().fittedStreamAspect, '16:9');173-});174-175-test('native geometry errors fail closed and retain the exact warning boundary', async () => {176- const harness = newHarness({177- applyNativeGeometry: async () => { throw new Error('native rejected'); },178- });179-180- assert.equal(await harness.runtime.fitWindowToIncomingStream(), false);181- assert.equal(harness.warnings[0][0], 'could not apply stream window geometry:');182- assert.match(harness.warnings[0][1].message, /native rejected/);183-});184-185-test('resize correction debounces for 80ms and uses only the latest observed delta', async () => {186- const harness = newHarness();187- harness.runtime.scheduleAspectCorrection();188- const firstTimer = [...harness.timers.keys()][0];189- harness.windowSize = { width: 1100, height: 700 };190- harness.runtime.scheduleAspectCorrection();191-192- assert.deepEqual(harness.cancelled, [firstTimer]);193- assert.equal(harness.timers.size, 1);194- const [{ callback, delay }] = [...harness.timers.values()];195- assert.equal(delay, STREAM_WINDOW_CORRECTION_DELAY_MS);196- callback();197- await flushMicrotasks();198-199- assert.deepEqual(harness.nativeApplications, [{200- geometry: { width: 1100, height: 752 },201- unmaximize: false,202- }]);203- assert.equal(harness.runtime.snapshot().correctionScheduled, false);204-});205-206-test('already-correct and disconnected resize callbacks do not touch native geometry', async () => {207- const harness = newHarness();208- harness.windowSize = { width: 1280, height: 864 };209- harness.runtime.scheduleAspectCorrection();210- [...harness.timers.values()][0].callback();211- await flushMicrotasks();212- assert.equal(harness.nativeApplications.length, 0);213-214- harness.windowSize = { width: 1000, height: 700 };215- harness.runtime.scheduleAspectCorrection();216- harness.connected = false;217- [...harness.timers.values()][0].callback();218- await flushMicrotasks();219- assert.equal(harness.nativeApplications.length, 0);220-});221-222-test('invalid correction geometry is contained and reset cancels pending correction state', () => {223- const harness = newHarness();224- harness.chromeHeight = 900;225- harness.runtime.scheduleAspectCorrection();226- const timerId = [...harness.timers.keys()][0];227- harness.timers.get(timerId).callback();228- harness.timers.delete(timerId);229- assert.equal(harness.warnings[0][0], 'could not constrain stream window geometry:');230-231- harness.runtime.scheduleAspectCorrection();232- const pendingTimer = [...harness.timers.keys()][0];233- harness.runtime.reset();234- assert.ok(harness.cancelled.includes(pendingTimer));235- assert.deepEqual(harness.runtime.snapshot(), {236- fittedStreamAspect: null,237- pendingStreamFit: null,238- lastObservedWindowSize: null,239- correctionScheduled: false,240- });241-});242-243-test('window correction delay remains pinned to the existing behavior', () => {244- assert.equal(STREAM_WINDOW_CORRECTION_DELAY_MS, 80);245-});246-247-test('Portal injects DOM geometry boundaries and delegates orchestration state', () => {248- assert.match(main, /const windowRuntime = createWindowRuntime\(\{/);249- assert.match(main, /getSurfaceBounds: \(\) => \{/);250- assert.match(main, /setSurfaceSize: \(\{ width, height \}\) => \{/);251- assert.match(main, /applyNativeGeometry: \(geometry, unmaximize\) => invoke\('set_client_window_size'/);252- assert.match(main, /windowRuntime\.reset\(\)/);253- assert.match(main, /windowRuntime\.scheduleAspectCorrection\(\)/);254- assert.doesNotMatch(main, /let fittedStreamAspect =/);255- assert.doesNotMatch(main, /let streamWindowResizeTimer =/);256- assert.doesNotMatch(main, /function fitWindowToIncomingStream\(/);257- assert.doesNotMatch(main, /function scheduleStreamWindowAspectCorrection\(/);53+test('Portal persists the user window and never derives native geometry from a stream', () => {54+ assert.match(nativeCargo, /tauri-plugin-window-state = "2"/);55+ assert.match(56+ nativeMain,57+ /\.plugin\(tauri_plugin_window_state::Builder::default\(\)\.build\(\)\)/,58+ );59+ assert.doesNotMatch(nativeMain, /set_client_window_size/);60+ assert.doesNotMatch(nativeState, /set_client_window_size/);61+ assert.doesNotMatch(main, /set_client_window_size/);62+ assert.doesNotMatch(main, /fitWindowToIncomingStream/);63+ assert.doesNotMatch(main, /scheduleAspectCorrection/);64+ assert.match(main, /windowRuntime\.sizeSurfaceToIncomingStream\(\)/);25865 });
M src-tauri/Cargo.toml +1 -0
M src-tauri/src/commands/state.rs +0 -60
@@ -539,53 +539,6 @@ pub fn development_connection_mode(state: tauri::State<'_, AppState>) -> Develop
539539 }540540 }541541 542-fn validate_client_window_size(width: f64, height: f64) -> Result<(f64, f64), String> {543- const MIN_WIDTH: f64 = 480.0;544- const MIN_HEIGHT: f64 = 320.0;545- const MAX_DIMENSION: f64 = 8192.0;546- if !width.is_finite()547- || !height.is_finite()548- || width.fract() != 0.0549- || height.fract() != 0.0550- || !(MIN_WIDTH..=MAX_DIMENSION).contains(&width)551- || !(MIN_HEIGHT..=MAX_DIMENSION).contains(&height)552- {553- return Err("Client window size is outside the supported logical bounds".to_owned());554- }555- Ok((width, height))556-}557-558-#[tauri::command]559-pub fn set_client_window_size(560- window: tauri::WebviewWindow,561- logical_width: f64,562- logical_height: f64,563- unmaximize: bool,564-) -> Result<bool, String> {565- let (width, height) = validate_client_window_size(logical_width, logical_height)?;566- if window567- .is_fullscreen()568- .map_err(|error| format!("Could not inspect client fullscreen state: {error}"))?569- {570- return Ok(false);571- }572- if window573- .is_maximized()574- .map_err(|error| format!("Could not inspect client maximize state: {error}"))?575- {576- if !unmaximize {577- return Ok(false);578- }579- window580- .unmaximize()581- .map_err(|error| format!("Could not restore the client window: {error}"))?;582- }583- window584- .set_size(tauri::LogicalSize::new(width, height))585- .map_err(|error| format!("Could not set the client stream geometry: {error}"))?;586- Ok(true)587-}588-589542 #[tauri::command]590543 pub fn set_client_cursor_grab(window: tauri::WebviewWindow, grab: bool) -> Result<bool, String> {591544 if grab {
@@ -652,19 +605,6 @@ mod tests {
652605 assert!(!should_apply_native_cursor_association(false, false));653606 }654607 655- #[test]656- fn client_window_size_requires_bounded_integral_logical_dimensions() {657- assert_eq!(658- validate_client_window_size(1280.0, 864.0).unwrap(),659- (1280.0, 864.0)660- );661- assert!(validate_client_window_size(479.0, 864.0).is_err());662- assert!(validate_client_window_size(1280.0, 319.0).is_err());663- assert!(validate_client_window_size(8193.0, 864.0).is_err());664- assert!(validate_client_window_size(1280.5, 864.0).is_err());665- assert!(validate_client_window_size(f64::NAN, 864.0).is_err());666- }667-668608 #[test]669609 fn cursor_focus_reassertion_forces_tao_visibility_transition() {670610 let mut visibility = Vec::new();
M src-tauri/src/main.rs +4 -1