Skip to content

feat: OnClose, a handler for the window's close request - #17

Merged
LucasAntunesdeAlmeida merged 1 commit into
masterfrom
feat/on-close
Sep 29, 2026
Merged

LucasAntunesdeAlmeida merged 1 commit into
masterfrom
feat/on-close

Conversation

@LucasAntunesdeAlmeida

Copy link
Copy Markdown
Owner

What

g.OnClose(fn func()): run fn once when the player closes the window (X button, Alt+F4, the taskbar's Close, a launcher such as Steam asking the game to exit), then end the game like Quit. Until now a close request ended the game at once, with no chance to save progress or bank a run in progress.

Design choices

  • Built on ebiten.SetWindowClosingHandled(true) + ebiten.IsWindowBeingClosed(). OnClose sets the flag (OnClose(nil) gives the close button back to the window); the runner checks for a close request at the start of Update.
  • The handler runs on the game loop, between two frames, like any other callback, so it can read and save game state safely. Then the next frame never runs: Update returns ebiten.Termination, Run returns and saves the COLLIDER_RECORD GIF, the same path as Quit.
  • Once: the handler is cleared before it runs.
  • No veto. Launchers and the OS expect a close request to end the game; an "are you sure?" on close is not what a small API needs. A game that wants one can still offer its own QUIT entry.
  • Not on Quit (the game's own decision), not headless (no window: Step and COLLIDER_AGENT=mcp never see it), and a no-op in a browser: a tab closes its page and ebiten reports no close request there, so games save as they go (localStorage writes are immediate). Autopilot and mcp-window are real windows: the handler runs.
  • windowBeingClosed / setWindowClosingHandled are swappable variables so tests fake the window.

Proof

  • close_test.go: the handler runs once, its Save lands, no frame runs after it, later Updates stay Termination; Quit does not run it; OnClose(nil) restores the default; headless never runs it.
  • examples/catcher: closing the window mid-round keeps a new best (g.OnClose saves it); its README explains the pattern.
  • Real window check on Windows: a probe program with OnClose writing a file, closed by WM_CLOSE (Process.CloseMainWindow): the handler ran after 135 frames, then Run returned and the process exited.
  • README: cheat-sheet row, Catcher row, contract item 8.
  • Local gate: gofmt, vet, js/wasm vet + build, staticcheck, go test.

Closing the window (its X button, Alt+F4, the taskbar, a launcher such
as Steam asking the game to exit) ended the game at once, so a game had
no chance to save progress or bank a run in progress.

g.OnClose(fn) takes over the close request (ebiten
SetWindowClosingHandled): when the window is asked to close, fn runs
once on the game loop, between two frames, then the game ends the way
Quit does (the next Update returns ebiten.Termination, Run returns and
saves the COLLIDER_RECORD GIF). No frame runs after the handler. There
is no veto: launchers and the OS expect a close request to end the
game, and a confirm dialog on close is not what a small API needs.

fn never runs on Quit (the game's own decision), headless (no window),
or in a browser (a tab closes its page; ebiten reports no close request
there, so it is a no-op: save as you go). OnClose(nil) restores the
default. The window hooks are swappable variables so the tests fake a
close request through runner.Update.

The catcher example keeps a record in the making when the window
closes mid-round; README cheat-sheet row and contract item 8 updated.
@LucasAntunesdeAlmeida
LucasAntunesdeAlmeida merged commit a550289 into master Sep 29, 2026
2 checks passed
@LucasAntunesdeAlmeida
LucasAntunesdeAlmeida deleted the feat/on-close branch September 29, 2026 14:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant