Skip to content

feat: add "Windowed fullscreen" toggle for borderless on-Space mode#190

Open
koto9x wants to merge 2 commits into
aviwad:mainfrom
koto9x:feat/windowed-fullscreen
Open

feat: add "Windowed fullscreen" toggle for borderless on-Space mode#190
koto9x wants to merge 2 commits into
aviwad:mainfrom
koto9x:feat/windowed-fullscreen

Conversation

@koto9x

@koto9x koto9x commented May 26, 2026

Copy link
Copy Markdown

Summary

Adds a user-toggleable Windowed fullscreen mode to the Fullscreen view. When enabled, the fullscreen window opens as a movable/resizable borderless-style window pinned to the current Space's visible frame, instead of macOS native fullscreen — which animates to a new Space and hides the menubar and Dock.

Default is off, so existing behavior is preserved 1:1 for current users.

Why

Native fullscreen forcing a new Space breaks common multi-window workflows — lyrics-while-coding, lyrics-while-browsing, lyrics on a second display while the primary stays free. "Windowed fullscreen" is the same idiom most games offer: visually fills the screen, but stays on the current Space and can be moved/resized like any other window.

What changed

UserDefaultStorage.swift — adds useWindowedFullscreen: Bool = false (macOS-only, persisted via the existing @ObservableUserDefault pattern).

LyricFever.swift — the Fullscreen Window's onAppear now branches on the flag:

  • Off (default, unchanged): collectionBehavior = .fullScreenPrimary + the existing toggleFullScreen(true) path.
  • On: borderless styleMask (.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView) with titlebarAppearsTransparent + titleVisibility = .hidden, collectionBehavior = [.managed], setFrame(screen.visibleFrame, display: true), isMovableByWindowBackground = true. If macOS restored prior native-fullscreen state, exits it first via toggleFullScreen(nil) so the styleMask change can actually land; sets isRestorable = false so subsequent launches start clean.

.windowFullScreenBehavior is now bound to the flag (.disabled when on, .enabled when off), so the green traffic-light button can't pull a windowed session back into native fullscreen.

FullscreenView.swift — new toggle in the existing in-fullscreen settings popover (next to Blur surrounding lyrics and Animate on startup):

Toggle(\"Windowed fullscreen (stays on current Space)\", isOn: \$viewmodel.userDefaultStorage.useWindowedFullscreen)

Drive-by: responsive album art

The album-art frame in FullscreenView was hard-coded at 550pt / 700pt. That looks fine at 1920×1080 native fullscreen, but in windowed mode the user can resize the window and the fixed-size art overflows the lyrics column. Changed to derive from the GeometryReader container:

let columnWidth = viewmodel.canDisplayLyrics ? containerSize.width * 0.5 : containerSize.width
let artSide = max(120, min(columnWidth * 0.85, containerSize.height * 0.55))

Min 120pt so it doesn't disappear at tiny sizes. On a typical native-fullscreen display the resulting size matches the previous hard-codes within ~5%, so the existing experience is effectively unchanged.

Test plan

  • Builds against main (xcodebuild Debug, -skipMacroValidation).
  • Default off: Fullscreen opens via native fullscreen as before, animates to a new Space, green button toggles, Esc blocked.
  • Toggle on, reopen Fullscreen: opens as a borderless window at visible-frame size on the current Space. Can drag from the window background; can resize from edges/corners; menubar and Dock remain visible.
  • Album art resizes smoothly when the windowed-fullscreen window is dragged smaller/larger; lyrics column no longer overflows.
  • Toggling the setting back off and reopening Fullscreen returns to native-fullscreen behavior.

Notes

  • The Apple Music workaround / MediaController(bundleIdentifier:) change on main is independent of this PR — same compile state as base.
  • No new dependencies. No localization strings added beyond the one toggle label (happy to wire into Localizable.xcstrings if preferred).

koto9x added 2 commits May 26, 2026 19:16
Adds a per-user preference that, when enabled, opens the Fullscreen window as a
movable/resizable borderless-style window pinned to the current Space's visible
frame instead of macOS native fullscreen (which animates to a new Space and
hides the menubar and Dock).

- New user default `useWindowedFullscreen` (defaults to false → existing native
  fullscreen behavior is unchanged).
- New "Windowed fullscreen (stays on current Space)" toggle in the in-fullscreen
  settings popover, alongside the existing Blur/Animate toggles.
- LyricFever.swift Fullscreen Window onAppear branches on the flag:
  - off: existing `collectionBehavior = .fullScreenPrimary` + `toggleFullScreen`
  - on: borderless styleMask (titled/closable/miniaturizable/resizable +
        fullSizeContentView, transparent + hidden titlebar), `.managed`
        collectionBehavior, `setFrame(screen.visibleFrame)`. Defensively exits
        any restored native-fullscreen state first and disables window
        restoration so the styleMask change isn't fought by the system.
- `.windowFullScreenBehavior` is bound to the flag so the green traffic-light
  button doesn't pull a windowed session back into native fullscreen.
- Drive-by: album art in FullscreenView is now sized from the GeometryReader
  container (max(120, min(columnWidth*0.85, height*0.55))) instead of the
  hard-coded 550/700 frame. This is what makes the windowed mode usable when
  resized; native-fullscreen sizing on a typical display is effectively
  unchanged.

Motivation: native fullscreen forcing a new Space breaks multi-window workflows
(e.g. lyrics-while-coding, lyrics-while-browsing). Windowed-fullscreen behaves
like the gaming "windowed fullscreen" idiom — fills the screen visually, stays
on the current Space, can be moved/resized.
…ative fullscreen

Two follow-up polish changes on top of the windowed-fullscreen feature:

1. `Cmd+Ctrl+F` (the canonical macOS toggle-fullscreen shortcut) now toggles
   native fullscreen on the Lyric Fever Fullscreen window. Because the app is
   `LSUIElement = true` and has no standard View menu, the OS shortcut has
   nothing to fire by default; we wire it explicitly in the existing
   `NSEvent.addLocalMonitorForEvents` block. This is the escape hatch that
   makes "Windowed fullscreen" non-trapping — a user can be in windowed mode by
   default and still opt into native fullscreen when they want it (e.g. on a
   second display), and back out again.

2. Esc-blocking is now conditional: blocked only while the window is NOT in
   native fullscreen. When the user has entered native fullscreen (via
   Cmd+Ctrl+F or otherwise), Esc passes through so the platform-standard exit
   gesture works. In windowed mode the original "sticky" behavior is preserved.

Also flips:
- `.windowFullScreenBehavior` is now unconditionally `.enabled` (was conditional
  on the user default). Native fullscreen is always a reachable mode.
- `collectionBehavior` in the windowed-fullscreen branch is `.fullScreenPrimary`
  (was `[.managed]`), which is what allows the new Cmd+Ctrl+F path to actually
  enter native fullscreen from a windowed-mode session.

Default-off invariant is unchanged: when `useWindowedFullscreen` is false the
window opens via the existing `.fullScreenPrimary` + `toggleFullScreen(true)`
path exactly as before.
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