MVP tauri frontend - Windows #1
@@ -142,6 +142,9 @@ Bevy window-management verification workflow: `docs/BEVY_WINDOW_VERIFICATION.md`
|
|||||||
- at large scale values (>= 1.8), full sprite remains visible without clipping
|
- at large scale values (>= 1.8), full sprite remains visible without clipping
|
||||||
13. Verify packaged tauri frontend freshness:
|
13. Verify packaged tauri frontend freshness:
|
||||||
- confirm package run reflects latest `frontend/tauri-ui` changes (no stale embedded UI bundle)
|
- confirm package run reflects latest `frontend/tauri-ui` changes (no stale embedded UI bundle)
|
||||||
|
14. Verify packaged tauri overlay edge and scale consistency:
|
||||||
|
- place overlay over dark background and confirm no white strip/background bleed on left edge
|
||||||
|
- for same slider value, confirm main-window size is consistent across `default`/`ferris`/`demogorgon`
|
||||||
|
|
||||||
### Packaged Mode (Required Once Tauri Packaging Exists)
|
### Packaged Mode (Required Once Tauri Packaging Exists)
|
||||||
|
|
||||||
|
|||||||
@@ -122,6 +122,9 @@ An issue touching Tauri runtime behaviors must satisfy all requirements before `
|
|||||||
API mismatch)
|
API mismatch)
|
||||||
- verify window resize uses consistent coordinate units (no accumulated drift over 20 scale changes)
|
- verify window resize uses consistent coordinate units (no accumulated drift over 20 scale changes)
|
||||||
- no runtime command/type error from position updates (e.g. `set_position` expects integer coords)
|
- no runtime command/type error from position updates (e.g. `set_position` expects integer coords)
|
||||||
|
- at the same slider scale value, main window size is consistent across packs (`default`, `ferris`,
|
||||||
|
`demogorgon`) within 1px rounding tolerance
|
||||||
|
- no white strip/background bleed is visible along any overlay window edge on dark desktop background
|
||||||
|
|
||||||
## Settings Window Checklist
|
## Settings Window Checklist
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ const SIZE_EPSILON = 0.5;
|
|||||||
const SCALE_EPSILON = 0.0001;
|
const SCALE_EPSILON = 0.0001;
|
||||||
const SCALE_MIN = 0.5;
|
const SCALE_MIN = 0.5;
|
||||||
const SCALE_MAX = 3.0;
|
const SCALE_MAX = 3.0;
|
||||||
|
const LOGICAL_BASE_FRAME_WIDTH = 512;
|
||||||
|
const LOGICAL_BASE_FRAME_HEIGHT = 512;
|
||||||
|
|
||||||
async function invokeSetSpritePack(packIdOrPath: string): Promise<UiSnapshot> {
|
async function invokeSetSpritePack(packIdOrPath: string): Promise<UiSnapshot> {
|
||||||
return invoke<UiSnapshot>("set_sprite_pack", { packIdOrPath });
|
return invoke<UiSnapshot>("set_sprite_pack", { packIdOrPath });
|
||||||
@@ -49,29 +51,31 @@ async function invokeSetAlwaysOnTop(alwaysOnTop: boolean): Promise<UiSnapshot> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function fittedWindowSize(
|
function fittedWindowSize(
|
||||||
frameWidth: number,
|
|
||||||
frameHeight: number,
|
|
||||||
scale: number
|
scale: number
|
||||||
): { width: number; height: number } {
|
): { width: number; height: number } {
|
||||||
const safeScale = Number.isFinite(scale) && scale > 0 ? scale : 1;
|
const safeScale = Number.isFinite(scale) && scale > 0 ? scale : 1;
|
||||||
const width = Math.round(Math.max(frameWidth * safeScale + WINDOW_PADDING, MIN_WINDOW_SIZE));
|
const width = Math.round(
|
||||||
const height = Math.round(Math.max(frameHeight * safeScale + WINDOW_PADDING, MIN_WINDOW_SIZE));
|
Math.max(LOGICAL_BASE_FRAME_WIDTH * safeScale + WINDOW_PADDING, MIN_WINDOW_SIZE)
|
||||||
|
);
|
||||||
|
const height = Math.round(
|
||||||
|
Math.max(LOGICAL_BASE_FRAME_HEIGHT * safeScale + WINDOW_PADDING, MIN_WINDOW_SIZE)
|
||||||
|
);
|
||||||
return { width, height };
|
return { width, height };
|
||||||
}
|
}
|
||||||
|
|
||||||
function effectiveScaleForWindowSize(pack: UiSpritePack, width: number, height: number): number {
|
function effectiveScaleForWindowSize(width: number, height: number): number {
|
||||||
const availableWidth = Math.max(width - WINDOW_PADDING, MIN_WINDOW_SIZE);
|
const availableWidth = Math.max(width - WINDOW_PADDING, MIN_WINDOW_SIZE);
|
||||||
const availableHeight = Math.max(height - WINDOW_PADDING, MIN_WINDOW_SIZE);
|
const availableHeight = Math.max(height - WINDOW_PADDING, MIN_WINDOW_SIZE);
|
||||||
const scaleByWidth = availableWidth / Math.max(pack.frame_width, 1);
|
const scaleByWidth = availableWidth / LOGICAL_BASE_FRAME_WIDTH;
|
||||||
const scaleByHeight = availableHeight / Math.max(pack.frame_height, 1);
|
const scaleByHeight = availableHeight / LOGICAL_BASE_FRAME_HEIGHT;
|
||||||
const scale = Math.min(scaleByWidth, scaleByHeight);
|
const scale = Math.min(scaleByWidth, scaleByHeight);
|
||||||
return Math.max(SCALE_MIN, Math.min(scale, SCALE_MAX));
|
return Math.max(SCALE_MIN, Math.min(scale, SCALE_MAX));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fitWindowForScale(pack: UiSpritePack, scale: number): Promise<number> {
|
async function fitWindowForScale(scale: number): Promise<number> {
|
||||||
const window = getCurrentWindow();
|
const window = getCurrentWindow();
|
||||||
const [outerPosition, innerSize] = await Promise.all([window.outerPosition(), window.innerSize()]);
|
const [outerPosition, innerSize] = await Promise.all([window.outerPosition(), window.innerSize()]);
|
||||||
const target = fittedWindowSize(pack.frame_width, pack.frame_height, scale);
|
const target = fittedWindowSize(scale);
|
||||||
const centerX = outerPosition.x + innerSize.width / 2;
|
const centerX = outerPosition.x + innerSize.width / 2;
|
||||||
const centerY = outerPosition.y + innerSize.height / 2;
|
const centerY = outerPosition.y + innerSize.height / 2;
|
||||||
let targetWidth = target.width;
|
let targetWidth = target.width;
|
||||||
@@ -126,7 +130,7 @@ async function fitWindowForScale(pack: UiSpritePack, scale: number): Promise<num
|
|||||||
await window.setPosition(new PhysicalPosition(Math.round(targetX), Math.round(targetY)));
|
await window.setPosition(new PhysicalPosition(Math.round(targetX), Math.round(targetY)));
|
||||||
}
|
}
|
||||||
|
|
||||||
return effectiveScaleForWindowSize(pack, targetWidth, targetHeight);
|
return effectiveScaleForWindowSize(targetWidth, targetHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MainOverlayWindow(): JSX.Element {
|
function MainOverlayWindow(): JSX.Element {
|
||||||
@@ -161,9 +165,9 @@ function MainOverlayWindow(): JSX.Element {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const tryFitWindow = async (pack: UiSpritePack, scale: number): Promise<number | null> => {
|
const tryFitWindow = async (scale: number): Promise<number | null> => {
|
||||||
try {
|
try {
|
||||||
return await fitWindowForScale(pack, scale);
|
return await fitWindowForScale(scale);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (mountedRef.current) {
|
if (mountedRef.current) {
|
||||||
setError(String(err));
|
setError(String(err));
|
||||||
@@ -212,13 +216,13 @@ function MainOverlayWindow(): JSX.Element {
|
|||||||
loadingPackRef.current = true;
|
loadingPackRef.current = true;
|
||||||
let reloaded = false;
|
let reloaded = false;
|
||||||
try {
|
try {
|
||||||
const pack = await invoke<UiSpritePack>("load_active_sprite_pack");
|
const pack = await invoke<UiSpritePack>("load_active_sprite_pack");
|
||||||
reloaded = await recreateRenderer(pack, value);
|
reloaded = await recreateRenderer(pack, value);
|
||||||
if (reloaded) {
|
if (reloaded) {
|
||||||
const effectiveScale = await tryFitWindow(pack, value.scale);
|
const effectiveScale = await tryFitWindow(value.scale);
|
||||||
if (effectiveScale !== null) {
|
if (effectiveScale !== null) {
|
||||||
await syncEffectiveScale(value.scale, effectiveScale);
|
await syncEffectiveScale(value.scale, effectiveScale);
|
||||||
}
|
}
|
||||||
if (mountedRef.current && effectiveScale !== null) {
|
if (mountedRef.current && effectiveScale !== null) {
|
||||||
setError(null);
|
setError(null);
|
||||||
}
|
}
|
||||||
@@ -236,7 +240,7 @@ function MainOverlayWindow(): JSX.Element {
|
|||||||
if (activePackRef.current === null) {
|
if (activePackRef.current === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const effectiveScale = await tryFitWindow(activePackRef.current, value.scale);
|
const effectiveScale = await tryFitWindow(value.scale);
|
||||||
if (effectiveScale !== null) {
|
if (effectiveScale !== null) {
|
||||||
await syncEffectiveScale(value.scale, effectiveScale);
|
await syncEffectiveScale(value.scale, effectiveScale);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,23 @@
|
|||||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: #e2e8f0;
|
color: #e2e8f0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
.app {
|
.app {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
|||||||
92
issues/issue5.md
Normal file
92
issues/issue5.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
## Title
|
||||||
|
|
||||||
|
Tauri overlay shows left-edge white strip and same scale yields different window sizes by pack.
|
||||||
|
|
||||||
|
## Severity
|
||||||
|
|
||||||
|
P2
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
|
||||||
|
- OS: Windows
|
||||||
|
- App version/build: packaged release (`sprimo-tauri.exe`)
|
||||||
|
- Evidence screenshots:
|
||||||
|
- `issues/screenshots/issue5.png`
|
||||||
|
- `issues/screenshots/issue5-b.png`
|
||||||
|
- `issues/screenshots/issue5-c.png`
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Two regressions were observed in packaged Tauri runtime:
|
||||||
|
|
||||||
|
1. A visible white strip appears on the overlay left edge on dark backgrounds.
|
||||||
|
2. At the same slider scale, main overlay window size differs by sprite pack.
|
||||||
|
|
||||||
|
## Reproduction Steps
|
||||||
|
|
||||||
|
1. Run packaged `sprimo-tauri.exe`.
|
||||||
|
2. Place overlay over a dark/black background.
|
||||||
|
3. Observe left window edge.
|
||||||
|
4. Open settings and set the same scale value on `default`, `ferris`, and `demogorgon`.
|
||||||
|
5. Compare main window footprint.
|
||||||
|
|
||||||
|
## Expected Result
|
||||||
|
|
||||||
|
- No white strip or white background bleed on transparent overlay edges.
|
||||||
|
- Same scale value produces the same main overlay window size regardless of pack.
|
||||||
|
|
||||||
|
## Actual Result
|
||||||
|
|
||||||
|
- Left edge can show a white strip.
|
||||||
|
- Window size differs across packs at same scale.
|
||||||
|
|
||||||
|
## Root Cause Analysis
|
||||||
|
|
||||||
|
1. Transparency chain was incomplete in CSS:
|
||||||
|
- `body` was transparent, but `html`/`#root` were not explicitly transparent/full-size, allowing
|
||||||
|
white background bleed at edges in transparent frameless window mode.
|
||||||
|
2. Scale-to-window mapping depended on per-pack frame size:
|
||||||
|
- window sizing used pack `frame_width/frame_height`, so identical scale values produced different
|
||||||
|
target sizes across packs.
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
1. `frontend/tauri-ui/src/main.tsx`
|
||||||
|
- introduced canonical scale basis `512x512` for window sizing semantics.
|
||||||
|
- changed `fittedWindowSize` and `effectiveScaleForWindowSize` to use canonical dimensions.
|
||||||
|
- removed pack-dependent sizing from `fitWindowForScale`; pack remains used for rendering/splitting.
|
||||||
|
2. `frontend/tauri-ui/src/styles.css`
|
||||||
|
- made `html`, `body`, and `#root` explicit full-size transparent surfaces to avoid white bleed.
|
||||||
|
3. `docs/TAURI_RUNTIME_TESTING.md`
|
||||||
|
- added explicit checks for same-scale cross-pack window-size consistency and no edge white strip.
|
||||||
|
4. `docs/RELEASE_TESTING.md`
|
||||||
|
- added packaged verification steps for white-edge bleed and cross-pack same-scale size consistency.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
### Commands Run
|
||||||
|
|
||||||
|
- [x] `npm --prefix frontend/tauri-ui run build`
|
||||||
|
- [x] `cargo check -p sprimo-tauri`
|
||||||
|
- [ ] `just build-release-tauri`
|
||||||
|
- [ ] `just package-win-tauri`
|
||||||
|
- [ ] `just smoke-win-tauri`
|
||||||
|
|
||||||
|
### Visual Checklist
|
||||||
|
|
||||||
|
- [x] Before screenshot(s): `issues/screenshots/issue5.png`
|
||||||
|
- [x] Before screenshot(s): `issues/screenshots/issue5-b.png`
|
||||||
|
- [x] Before screenshot(s): `issues/screenshots/issue5-c.png`
|
||||||
|
- [ ] After screenshot(s): `issues/screenshots/issue5-after-YYYYMMDD-HHMMSS.png`
|
||||||
|
|
||||||
|
### Result
|
||||||
|
|
||||||
|
- Status: `Fix Implemented`
|
||||||
|
- Notes: packaged runtime verification pending.
|
||||||
|
|
||||||
|
## Status History
|
||||||
|
|
||||||
|
- `2026-02-14 00:00` - reporter - `Reported` - left white strip + same-scale size inconsistency reported.
|
||||||
|
- `2026-02-14 00:00` - codex - `Triaged` - identified transparency chain and scale-basis coupling root causes.
|
||||||
|
- `2026-02-14 00:00` - codex - `Fix Implemented` - switched to canonical scale basis and full-surface transparency.
|
||||||
|
|
||||||
Reference in New Issue
Block a user