cat / goq
clone

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

fix(portal): preserve user window geometry

Dami committed c95ad23
Persist native window state across relaunches with the official Tauri plugin and keep incoming stream aspect handling confined to the letterboxed canvas.
commit
c95ad23b355f0e3f526db37c321d36551519a8e0
parent
f41e3896d871edfe59c7f0a13c2f41658800268e
author
Dami <dami.etoile@gmail.com>
9 files changed, +90 -588
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 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