A game is easier to judge and to play without the editor around it. The Game tab and the game link now both have a button that puts the game view itself into fullscreen, not just the browser window. - The Game tab header has a fullscreen button on the right. It hides toolbar, panels, tab headers and console and keeps the zoom from the picker. A running game keeps running. - A corner button at the top right, or Esc, leaves fullscreen and puts the view back into the tab where it was. - The game link shows the same corner button. Over a running game it hides after 2 s without mouse movement and returns on the next move. - A click on the corner button never reaches the game. - Stop ends fullscreen. Stop switches back to the Scene tab, and any switch away from the Game tab ends fullscreen, so no empty screen is left behind. - src/viewport/fullscreen.ts wraps the Fullscreen API and holds the rule for when the button shows. FullscreenCorner.ts is the button used by both pages. Two SVG icons are new. - Editor.showGameFullscreen moves the GameViewport into the root while fullscreen is on. Tests: - ./check.sh: 195 files, 2815 tests, exit 0 (3 new unit tests). - e2e 60: 2 new checks for the corner button on the game link. - e2e 61 (new): 4 checks for fullscreen in the editor. - Full e2e run (local): ALL PASSED, 1580 checks. - Counter-checks fail as expected: rule without "no game running", no stopPropagation (60h), no end on tab switch (61c), no reparenting of the view (61a). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
79 lines
2.7 KiB
TypeScript
79 lines
2.7 KiB
TypeScript
import type { Container, FederatedPointerEvent } from 'pixi.js';
|
|
import { Button, requestRender, Theme, UI_ICON_SVG } from '../ui';
|
|
import {
|
|
CORNER_BUTTON_IDLE_SECONDS,
|
|
isFullscreen,
|
|
onFullscreenChange,
|
|
showsCornerButton,
|
|
toggleFullscreen,
|
|
} from './fullscreen';
|
|
|
|
/**
|
|
* The fullscreen button in the top right corner of a game on screen (#267): on the player page
|
|
* always, in the editor while its game view is in fullscreen. Over a running game it steps aside
|
|
* once the mouse rests (`showsCornerButton`).
|
|
*/
|
|
export class FullscreenCorner extends Button {
|
|
private active = true;
|
|
private running = false;
|
|
private lastMove = performance.now();
|
|
private hideTimer = 0;
|
|
private readonly stopChange: () => void;
|
|
|
|
constructor(host: Container) {
|
|
super('', {
|
|
width: Theme.controlHeight,
|
|
icon: isFullscreen() ? UI_ICON_SVG.fullscreenExit : UI_ICON_SVG.fullscreen,
|
|
onClick: () => void toggleFullscreen(),
|
|
});
|
|
this.focusable = false;
|
|
this.resize(Theme.controlHeight, Theme.controlHeight);
|
|
// The button lies on the game: a press on it must neither start the game nor reach it as a click.
|
|
const keep = (e: FederatedPointerEvent): void => e.stopPropagation();
|
|
this.on('pointerdown', keep);
|
|
this.on('pointerup', keep);
|
|
this.on('pointerover', () => this.refresh());
|
|
this.on('pointerout', () => this.refresh());
|
|
host.on('globalpointermove', () => {
|
|
this.lastMove = performance.now();
|
|
this.refresh();
|
|
});
|
|
this.stopChange = onFullscreenChange((on) =>
|
|
this.setIcon(on ? UI_ICON_SVG.fullscreenExit : UI_ICON_SVG.fullscreen),
|
|
);
|
|
}
|
|
|
|
override destroy(options?: Parameters<Button['destroy']>[0]): void {
|
|
this.stopChange();
|
|
window.clearTimeout(this.hideTimer);
|
|
super.destroy(options);
|
|
}
|
|
|
|
/** `active`: whether the host wants the button at all; `running`: whether a game is on screen. */
|
|
sync(active: boolean, running: boolean): void {
|
|
this.active = active;
|
|
this.running = running;
|
|
this.refresh();
|
|
}
|
|
|
|
/** Top right, a gap in from both edges of a host `w` wide. */
|
|
place(w: number): void {
|
|
this.position.set(Math.round(w - this.w - Theme.gap * 2), Theme.gap * 2);
|
|
}
|
|
|
|
private refresh(): void {
|
|
window.clearTimeout(this.hideTimer);
|
|
const idleSeconds = (performance.now() - this.lastMove) / 1000;
|
|
const show =
|
|
this.active && showsCornerButton({ running: this.running, idleSeconds, overButton: this.hovered });
|
|
if (show !== this.visible) {
|
|
this.visible = show;
|
|
requestRender();
|
|
}
|
|
if (show && this.running && !this.hovered) {
|
|
const left = CORNER_BUTTON_IDLE_SECONDS - idleSeconds;
|
|
this.hideTimer = window.setTimeout(() => this.refresh(), Math.max(0, left * 1000) + 20);
|
|
}
|
|
}
|
|
}
|