ui: blend titlebars into player background

This commit is contained in:
2026-10-12 13:04:29 +02:00
parent e6897332a5
commit 0ed35b4a58
2 changed files with 5 additions and 5 deletions
+3 -1
View File
@@ -184,12 +184,14 @@ Terebi reports facts, not watched status. A consumer such as Tubular decides com
## Playback UI ## Playback UI
The layout follows [IINA's video-first window](https://iina.io/img/screenshot.png) and the supplied screenshots: a 32-point frosted titlebar with native traffic lights and file icon, plus a dark frosted 440 × 68-point floating controller. AppKit materials blur the video beneath the chrome; muted icons, an orange volume track, a narrow seek thumb, and zero-padded time labels follow the reference. Shared vertical padding and an optical offset for symbol/slider insets balance the controller's visible top and bottom margins in both play and pause states. The controller starts centered with a four-percent bottom inset and keeps its relative position when the window changes size. The layout follows [IINA's video-first window](https://iina.io/img/screenshot.png) and the supplied screenshots: a 32-point transparent titlebar with native traffic lights and file icon, plus a dark frosted 440 × 68-point floating controller. AppKit materials blur the video beneath the controller; muted icons, an orange volume track, a narrow seek thumb, and zero-padded time labels follow the reference. Shared vertical padding and an optical offset for symbol/slider insets balance the controller's visible top and bottom margins in both play and pause states. The controller starts centered with a four-percent bottom inset and keeps its relative position when the window changes size.
The empty player uses the same HUD-glass material, 12-point rounded corners, and muted icon/text palette in a centered welcome card. File and YouTube opening actions share a consistently sized row; the card fits the 400 × 240-point minimum window, and the full window still accepts media-file and YouTube-link drops. The empty player uses the same HUD-glass material, 12-point rounded corners, and muted icon/text palette in a centered welcome card. File and YouTube opening actions share a consistently sized row; the card fits the 400 × 240-point minimum window, and the full window still accepts media-file and YouTube-link drops.
With no media open, the full window uses native behind-window HUD glass to frost the desktop and windows beneath it. The idle Metal surface stays attached but hidden; opening media restores a solid black background and letterboxing, and closing it restores the frosted background. macOS controls the material’s appearance, including Reduce Transparency. With no media open, the full window uses native behind-window HUD glass to frost the desktop and windows beneath it. The idle Metal surface stays attached but hidden; opening media restores a solid black background and letterboxing, and closing it restores the frosted background. macOS controls the material’s appearance, including Reduce Transparency.
Normal and compact titlebars have no separate glass strip or separator: content extends beneath AppKit’s transparent titlebar using `fullSizeContentView`, `titlebarAppearsTransparent`, and `titlebarSeparatorStyle = .none`. The title and pin sit directly over the existing background or video, with native traffic lights and window dragging preserved.
Opening video fits the window to its display aspect ratio, including non-square pixels, and constrains subsequent window resizing. Fullscreen retains the normal letterboxed renderer; audio-only playback and close release the aspect constraint. Extreme ratios that cannot fit both the screen and usable controls keep a freely resizable letterboxed window. Opening video fits the window to its display aspect ratio, including non-square pixels, and constrains subsequent window resizing. Fullscreen retains the normal letterboxed renderer; audio-only playback and close release the aspect constraint. Extreme ratios that cannot fit both the screen and usable controls keep a freely resizable letterboxed window.
**Compact mode** uses the same window, Metal view, and playback session—not a second window or macOS system PiP. Use the PiP-shaped button in the normal playback controls, **⌃⌘P**, or **Playback Actions → Enter Compact Mode**. The compact-mode button replaces the fullscreen button; fullscreen remains available through its menu, keyboard shortcuts, and double-clicking normal-mode video. Compact mode starts at the current screen's top-right corner with a 16-point inset and a preferred width of 320 points, preserving display aspect where usable screen bounds and the 160 × 90-point minimum allow; extreme ratios retain letterboxing. Compact mode floats above ordinary windows, joins all Spaces, and is configured to join eligible other-app fullscreen spaces and Stage Manager sets. Small hover controls provide play/pause, restore, and Playback Actions; keyboard playback controls remain available. **Compact mode** uses the same window, Metal view, and playback session—not a second window or macOS system PiP. Use the PiP-shaped button in the normal playback controls, **⌃⌘P**, or **Playback Actions → Enter Compact Mode**. The compact-mode button replaces the fullscreen button; fullscreen remains available through its menu, keyboard shortcuts, and double-clicking normal-mode video. Compact mode starts at the current screen's top-right corner with a 16-point inset and a preferred width of 320 points, preserving display aspect where usable screen bounds and the 160 × 90-point minimum allow; extreme ratios retain letterboxing. Compact mode floats above ordinary windows, joins all Spaces, and is configured to join eligible other-app fullscreen spaces and Stage Manager sets. Small hover controls provide play/pause, restore, and Playback Actions; keyboard playback controls remain available.
+2 -4
View File
@@ -998,7 +998,6 @@ final class PlayerModel: ObservableObject {
@MainActor @MainActor
private struct PlayerGlass: NSViewRepresentable { private struct PlayerGlass: NSViewRepresentable {
var titlebar = false
var blendingMode: NSVisualEffectView.BlendingMode = .withinWindow var blendingMode: NSVisualEffectView.BlendingMode = .withinWindow
func makeNSView(context: Context) -> NSVisualEffectView { func makeNSView(context: Context) -> NSVisualEffectView {
@@ -1006,7 +1005,7 @@ private struct PlayerGlass: NSViewRepresentable {
view.blendingMode = blendingMode view.blendingMode = blendingMode
view.state = .active view.state = .active
view.appearance = NSAppearance(named: .vibrantDark) view.appearance = NSAppearance(named: .vibrantDark)
view.material = titlebar ? .titlebar : .hudWindow view.material = .hudWindow
return view return view
} }
@@ -1245,7 +1244,6 @@ private struct PlayerContent: View {
.padding(.trailing, 12) .padding(.trailing, 12)
.frame(height: 32) .frame(height: 32)
.foregroundStyle(.white.opacity(0.6)) .foregroundStyle(.white.opacity(0.6))
.background(PlayerGlass(titlebar: true).allowsHitTesting(false))
.onHover(perform: model.setTitleHovered) .onHover(perform: model.setTitleHovered)
.opacity(model.controlsVisible ? 1 : 0) .opacity(model.controlsVisible ? 1 : 0)
.allowsHitTesting(model.controlsVisible) .allowsHitTesting(model.controlsVisible)
@@ -1500,7 +1498,6 @@ private struct PlayerContent: View {
.padding(8) .padding(8)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.foregroundStyle(.white.opacity(0.75)) .foregroundStyle(.white.opacity(0.75))
.background(PlayerGlass(titlebar: true))
.opacity(model.controlsVisible ? 1 : 0) .opacity(model.controlsVisible ? 1 : 0)
.allowsHitTesting(false) .allowsHitTesting(false)
.accessibilityHidden(!model.controlsVisible) .accessibilityHidden(!model.controlsVisible)
@@ -1617,6 +1614,7 @@ private struct WindowChrome: NSViewRepresentable {
window.styleMask.insert(.fullSizeContentView) window.styleMask.insert(.fullSizeContentView)
window.titleVisibility = .hidden window.titleVisibility = .hidden
window.titlebarAppearsTransparent = true window.titlebarAppearsTransparent = true
window.titlebarSeparatorStyle = .none
window.isOpaque = false window.isOpaque = false
window.backgroundColor = .clear window.backgroundColor = .clear
window.acceptsMouseMovedEvents = true window.acceptsMouseMovedEvents = true