Skip to content

fix: keep the macOS window responsive after it is reopened from the Dock - #38

Merged
acking-you merged 2 commits into
mainfrom
fix/macos-idle-frozen-ui
Oct 27, 2026
Merged

acking-you merged 2 commits into
mainfrom
fix/macos-idle-frozen-ui

Conversation

@acking-you

Copy link
Copy Markdown
Contributor

Problem

On macOS the window could stop reacting while the proxy kept working: clicking a tab, a button or a dialog changed nothing on screen.

Hiding the window to the tray set WindowStateService.visible = false, and only the tray's own "Show" path set it back. Clicking the Dock icon (or launching the app again) reopens the window natively in AppDelegate.applicationShouldHandleReopen, so the flag stayed false for the rest of the session. AppRenderActivity wrapped the whole navigator in TickerMode(enabled: … && shown), so every ticker in the app stayed muted. Clicks were handled, but animated changes never ran: an AnimatedSwitcher page switch kept the old page on top, dialogs stayed transparent behind a live barrier, and the theme snapshot never faded. The window looked frozen.

The same gate also froze the UI when the window was visible but had lost main-window status while the app stayed active, because it treated window blur as "hidden".

Separately, the Dock reopen ran setIsVisible(true) + makeKeyAndOrderFront on every window of the app (status item and menu windows included) and then returned true, so AppKit's default reopen ran on top. A minimize-to-tray window, which is miniaturized and ordered out, was never explicitly deminiaturized. A late minimize event could also hide the window again right after it came back.

Change

  • Remove the app-wide gate and the Dart visibility flag. AppRenderActivity and WindowStateService.visible/show()/hide() are deleted, and callers use windowManager directly. The galaxy, the only continuous animation, already mutes itself from the engine lifecycle. Every desktop embedder derives that lifecycle from the native window (hidden or minimized → hidden, unfocused → inactive), and a hidden engine draws no frames at all. The remaining animations (power-button halo, launcher scan shimmer) are finite. Navigation, dialogs and the theme snapshot are therefore never muted.
  • Dock reopen restores only the main window. It deminiaturizes the window if needed, orders it front, activates the app and returns false so AppKit doesn't act on the same window again.
  • Minimize to tray hides only while the window is still minimized, so a minimize event that arrives after the Dock has restored the window no longer snaps it away.
  • Tests and docs/galaxy-performance.md updated. The window-event-driven tests for the deleted gate are removed, and lifecycle-state coverage stays.

Verification

  • flutter analyze: no new issues. dart format: clean. flutter test: all passed.

  • home_render_lifecycle_test now switches pages while the app is inactive and checks that the switch completes.

  • AppDelegate.swift typechecks against the 3.47.7 FlutterMacOS framework.

  • Real macOS check. Throwaway debug builds of origin/main and of this branch, each with its own bundle id, ran a harness inside the navigator. It hid the window through the app's real close path, sent the Dock's reopen event with open -a, then started a 300 ms AnimationController:

    Scenario origin/main this branch
    Close to tray, reopen from Dock ticker=false, animation 0.00 after 0.9 s ticker=true, animation 1.00
    Window loses main status while the app stays active ticker=false, animation 0.00 ticker=true, animation 1.00

    While the window was hidden, both builds reported lifecycle hidden, so idle rendering is unchanged.

  • Not verified live: a real minimize. On the test machine miniaturize began but never completed, even for a bare AppKit window, so the deminiaturize branch of the Dock reopen and the late-minimize guard are covered by review and typecheck only.

🤖 Generated with Claude Code

acking-you and others added 2 commits October 26, 2026 22:10
Hiding the window to the tray set a Dart-side visibility flag that only the
tray's own show path cleared. The Dock icon and a second launch show the window
natively, so the flag stayed false and the app-wide TickerMode gate muted every
animation: page switches, dialogs and the theme snapshot never completed, and
the window looked frozen while the proxy kept running.

Remove the gate and the flag. The galaxy, the only continuous animation,
already mutes itself from the engine lifecycle, which every desktop embedder
derives from the native window. Other animations are finite and must never be
muted.

The Dock reopen also restores only the main window, deminiaturizing it when
minimize-to-tray left it miniaturized, and no longer lets AppKit's default
reopen run on top. A minimize event that arrives after the window is back no
longer hides it again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on check

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@acking-you
acking-you merged commit eecf3f6 into main Oct 27, 2026
3 checks passed
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