MVP tauri frontend - Windows #1

Merged
manbo merged 8 commits from dev-tauri into master 2026-02-14 20:15:55 +08:00
4 changed files with 84 additions and 64 deletions
Showing only changes of commit f50243ab96 - Show all commits

View File

@@ -133,6 +133,7 @@ An issue touching Tauri runtime behaviors must satisfy all requirements before `
4. Change scale via slider and verify: 4. Change scale via slider and verify:
- runtime scale changes immediately - runtime scale changes immediately
- main overlay auto-fits without clipping - main overlay auto-fits without clipping
- persisted/runtime scale value reflects the effective fitted scale after monitor/window bounds
- value persists after restart - value persists after restart
5. Toggle `Visible` and verify: 5. Toggle `Visible` and verify:
- main overlay hide/show behavior - main overlay hide/show behavior

View File

@@ -59,25 +59,16 @@ function fittedWindowSize(
return { width, height }; return { width, height };
} }
function maxVisibleScale( function effectiveScaleForWindowSize(pack: UiSpritePack, width: number, height: number): number {
pack: UiSpritePack, const availableWidth = Math.max(width - WINDOW_PADDING, MIN_WINDOW_SIZE);
workArea: { width: number; height: number } const availableHeight = Math.max(height - WINDOW_PADDING, MIN_WINDOW_SIZE);
): number { const scaleByWidth = availableWidth / Math.max(pack.frame_width, 1);
const availableWidth = Math.max( const scaleByHeight = availableHeight / Math.max(pack.frame_height, 1);
workArea.width - WINDOW_PADDING - WINDOW_WORKAREA_MARGIN, const scale = Math.min(scaleByWidth, scaleByHeight);
MIN_WINDOW_SIZE return Math.max(SCALE_MIN, Math.min(scale, SCALE_MAX));
);
const availableHeight = Math.max(
workArea.height - WINDOW_PADDING - WINDOW_WORKAREA_MARGIN,
MIN_WINDOW_SIZE
);
const maxByWidth = availableWidth / Math.max(pack.frame_width, 1);
const maxByHeight = availableHeight / Math.max(pack.frame_height, 1);
const cap = Math.min(maxByWidth, maxByHeight);
return Math.max(SCALE_MIN, Math.min(cap, SCALE_MAX));
} }
async function fitWindowForScale(pack: UiSpritePack, scale: number): Promise<void> { async function fitWindowForScale(pack: UiSpritePack, 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(pack.frame_width, pack.frame_height, scale);
@@ -118,20 +109,24 @@ async function fitWindowForScale(pack: UiSpritePack, scale: number): Promise<voi
const widthChanged = Math.abs(targetWidth - innerSize.width) > SIZE_EPSILON; const widthChanged = Math.abs(targetWidth - innerSize.width) > SIZE_EPSILON;
const heightChanged = Math.abs(targetHeight - innerSize.height) > SIZE_EPSILON; const heightChanged = Math.abs(targetHeight - innerSize.height) > SIZE_EPSILON;
if (!widthChanged && !heightChanged) { if (widthChanged || heightChanged) {
return; await window.setSize(new PhysicalSize(targetWidth, targetHeight));
if (monitor !== null) {
const minX = Math.round(monitor.workArea.position.x);
const minY = Math.round(monitor.workArea.position.y);
const maxX = Math.round(
monitor.workArea.position.x + monitor.workArea.size.width - targetWidth
);
const maxY = Math.round(
monitor.workArea.position.y + monitor.workArea.size.height - targetHeight
);
targetX = maxX < minX ? minX : Math.min(Math.max(targetX, minX), maxX);
targetY = maxY < minY ? minY : Math.min(Math.max(targetY, minY), maxY);
}
await window.setPosition(new PhysicalPosition(Math.round(targetX), Math.round(targetY)));
} }
await window.setSize(new PhysicalSize(targetWidth, targetHeight)); return effectiveScaleForWindowSize(pack, targetWidth, targetHeight);
if (monitor !== null) {
const minX = Math.round(monitor.workArea.position.x);
const minY = Math.round(monitor.workArea.position.y);
const maxX = Math.round(monitor.workArea.position.x + monitor.workArea.size.width - targetWidth);
const maxY = Math.round(monitor.workArea.position.y + monitor.workArea.size.height - targetHeight);
targetX = maxX < minX ? minX : Math.min(Math.max(targetX, minX), maxX);
targetY = maxY < minY ? minY : Math.min(Math.max(targetY, minY), maxY);
}
await window.setPosition(new PhysicalPosition(Math.round(targetX), Math.round(targetY)));
} }
function MainOverlayWindow(): JSX.Element { function MainOverlayWindow(): JSX.Element {
@@ -142,7 +137,7 @@ function MainOverlayWindow(): JSX.Element {
const rendererRef = React.useRef<PixiPetRenderer | null>(null); const rendererRef = React.useRef<PixiPetRenderer | null>(null);
const activePackRef = React.useRef<UiSpritePack | null>(null); const activePackRef = React.useRef<UiSpritePack | null>(null);
const loadedPackKeyRef = React.useRef<string | null>(null); const loadedPackKeyRef = React.useRef<string | null>(null);
const scaleFitRef = React.useRef<number | null>(null); const effectiveScaleSyncRef = React.useRef<number | null>(null);
const loadingPackRef = React.useRef(false); const loadingPackRef = React.useRef(false);
const mountedRef = React.useRef(false); const mountedRef = React.useRef(false);
@@ -166,16 +161,34 @@ function MainOverlayWindow(): JSX.Element {
return true; return true;
}; };
const tryFitWindow = async (pack: UiSpritePack, scale: number): Promise<boolean> => { const tryFitWindow = async (pack: UiSpritePack, scale: number): Promise<number | null> => {
try { try {
await fitWindowForScale(pack, scale); return await fitWindowForScale(pack, scale);
scaleFitRef.current = scale; } catch (err) {
return true; if (mountedRef.current) {
setError(String(err));
}
return null;
}
};
const syncEffectiveScale = async (snapshotScale: number, effectiveScale: number): Promise<void> => {
if (Math.abs(snapshotScale - effectiveScale) < SCALE_EPSILON) {
return;
}
if (
effectiveScaleSyncRef.current !== null &&
Math.abs(effectiveScaleSyncRef.current - effectiveScale) < SCALE_EPSILON
) {
return;
}
effectiveScaleSyncRef.current = effectiveScale;
try {
await invokeSetScale(effectiveScale);
} catch (err) { } catch (err) {
if (mountedRef.current) { if (mountedRef.current) {
setError(String(err)); setError(String(err));
} }
return false;
} }
}; };
@@ -183,6 +196,12 @@ function MainOverlayWindow(): JSX.Element {
if (!mountedRef.current) { if (!mountedRef.current) {
return; return;
} }
if (
effectiveScaleSyncRef.current !== null &&
Math.abs(effectiveScaleSyncRef.current - value.scale) < SCALE_EPSILON
) {
effectiveScaleSyncRef.current = null;
}
setSnapshot(value); setSnapshot(value);
rendererRef.current?.applySnapshot(value); rendererRef.current?.applySnapshot(value);
@@ -196,8 +215,11 @@ function MainOverlayWindow(): JSX.Element {
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) {
await tryFitWindow(pack, value.scale); const effectiveScale = await tryFitWindow(pack, value.scale);
if (mountedRef.current) { if (effectiveScale !== null) {
await syncEffectiveScale(value.scale, effectiveScale);
}
if (mountedRef.current && effectiveScale !== null) {
setError(null); setError(null);
} }
} }
@@ -214,14 +236,11 @@ function MainOverlayWindow(): JSX.Element {
if (activePackRef.current === null) { if (activePackRef.current === null) {
return; return;
} }
if ( const effectiveScale = await tryFitWindow(activePackRef.current, value.scale);
scaleFitRef.current !== null && if (effectiveScale !== null) {
Math.abs(scaleFitRef.current - value.scale) < SCALE_EPSILON await syncEffectiveScale(value.scale, effectiveScale);
) {
return;
} }
const fitApplied = await tryFitWindow(activePackRef.current, value.scale); if (effectiveScale !== null && mountedRef.current) {
if (fitApplied && mountedRef.current) {
setError(null); setError(null);
} }
}; };
@@ -443,30 +462,13 @@ function SettingsWindow(): JSX.Element {
if (!Number.isFinite(value)) { if (!Number.isFinite(value)) {
return; return;
} }
let effectiveScale = value; const next = await withPending(() => invokeSetScale(value));
if (activePack !== null) {
try {
const monitor = await currentMonitor();
if (monitor !== null) {
effectiveScale = Math.min(
value,
maxVisibleScale(activePack, {
width: monitor.workArea.size.width,
height: monitor.workArea.size.height
})
);
}
} catch {
// Keep requested value if monitor query fails.
}
}
const next = await withPending(() => invokeSetScale(effectiveScale));
if (next === null) { if (next === null) {
return; return;
} }
setSettings((prev) => (prev === null ? prev : { ...prev, scale: next.scale })); setSettings((prev) => (prev === null ? prev : { ...prev, scale: next.scale }));
}, },
[activePack, withPending] [withPending]
); );
const onVisibleChange = React.useCallback( const onVisibleChange = React.useCallback(

View File

@@ -51,6 +51,8 @@ const HALO_HUE_MIN = 245;
const HALO_HUE_MAX = 355; const HALO_HUE_MAX = 355;
const HALO_SAT_MIN = 0.15; const HALO_SAT_MIN = 0.15;
const HALO_VAL_MIN = 0.04; const HALO_VAL_MIN = 0.04;
const RENDER_FIT_PADDING = 16;
const MIN_RENDER_SCALE = 0.01;
export class PixiPetRenderer { export class PixiPetRenderer {
private app: Application; private app: Application;
@@ -426,7 +428,6 @@ export class PixiPetRenderer {
this.frameElapsedMs = 0; this.frameElapsedMs = 0;
this.applyFrameTexture(this.currentClip.frames[0] ?? 0); this.applyFrameTexture(this.currentClip.frames[0] ?? 0);
} }
this.sprite.scale.set(snapshot.scale);
this.layoutSprite(); this.layoutSprite();
} }
@@ -457,6 +458,12 @@ export class PixiPetRenderer {
} }
private layoutSprite(): void { private layoutSprite(): void {
const availableWidth = Math.max(this.app.renderer.width - RENDER_FIT_PADDING, 1);
const availableHeight = Math.max(this.app.renderer.height - RENDER_FIT_PADDING, 1);
const fitScaleX = availableWidth / Math.max(this.pack.frame_width, 1);
const fitScaleY = availableHeight / Math.max(this.pack.frame_height, 1);
const fitScale = Math.max(Math.min(fitScaleX, fitScaleY), MIN_RENDER_SCALE);
this.sprite.scale.set(fitScale);
this.sprite.position.set(this.app.renderer.width / 2, this.app.renderer.height); this.sprite.position.set(this.app.renderer.width / 2, this.app.renderer.height);
} }

View File

@@ -74,6 +74,8 @@ and scaling becomes ineffective after the error.
practical visible bounds and appear clipped. practical visible bounds and appear clipped.
12. Ferris/demogorgon packaged backgrounds are gradient-magenta (not exact `#FF00FF`), requiring a 12. Ferris/demogorgon packaged backgrounds are gradient-magenta (not exact `#FF00FF`), requiring a
border-connected magenta-family mask instead of exact-key assumptions. border-connected magenta-family mask instead of exact-key assumptions.
13. Clipping persisted because sprite rendering scale followed snapshot/requested scale directly
instead of fitting to the actual post-clamp window size.
## Fix Plan ## Fix Plan
@@ -142,6 +144,12 @@ Implemented:
17. `frontend/tauri-ui/src/renderer/pixi_pet.ts` 17. `frontend/tauri-ui/src/renderer/pixi_pet.ts`
- added deterministic border-connected strong-magenta flood-fill cleanup pass so non-`#FF00FF` - added deterministic border-connected strong-magenta flood-fill cleanup pass so non-`#FF00FF`
gradient backgrounds are removed consistently in packaged ferris/demogorgon atlases. gradient backgrounds are removed consistently in packaged ferris/demogorgon atlases.
18. `frontend/tauri-ui/src/main.tsx`
- changed scale flow to window-driven fit semantics: scale request resizes/clamps main window and
then persists effective scale derived from applied window size.
19. `frontend/tauri-ui/src/renderer/pixi_pet.ts`
- renderer sprite scale is now derived from current canvas/window size each layout pass, removing
clipping caused by mismatch between requested scale and bounded window dimensions.
## Verification ## Verification
@@ -181,6 +189,8 @@ Implemented:
- `2026-02-14 00:00` - codex - `Fix Implemented` - added integer-safe physical setPosition and HSV magenta cleanup strategy. - `2026-02-14 00:00` - codex - `Fix Implemented` - added integer-safe physical setPosition and HSV magenta cleanup strategy.
- `2026-02-14 00:00` - reporter - `In Progress` - reported remaining default clipping and ferris magenta background persistence. - `2026-02-14 00:00` - reporter - `In Progress` - reported remaining default clipping and ferris magenta background persistence.
- `2026-02-14 00:00` - codex - `Fix Implemented` - tightened work-area scale guard and added border-connected strong-magenta cleanup pass. - `2026-02-14 00:00` - codex - `Fix Implemented` - tightened work-area scale guard and added border-connected strong-magenta cleanup pass.
- `2026-02-14 00:00` - reporter - `In Progress` - reported clipping still present on `default` and `demogorgon` after prior fixes.
- `2026-02-14 00:00` - codex - `Fix Implemented` - moved tauri scale to window-driven effective-fit persistence and renderer fit-to-window scaling.
## Closure ## Closure