ui: frost playback titles and hide chrome on pointer exit

This commit is contained in:
2026-10-12 13:30:29 +02:00
parent 0ed35b4a58
commit 93acc1436a
3 changed files with 79 additions and 21 deletions
+53 -19
View File
@@ -59,6 +59,8 @@ final class PlayerModel: ObservableObject {
private var timer: Timer?
private weak var window: NSWindow?
private var hideDeadline: ContinuousClock.Instant?
private var pointerInsideWindow = true
private var keyboardNavigation = false
private var controlsHovered = false
private var titleHovered = false
private var controlsDragging = false
@@ -596,6 +598,18 @@ final class PlayerModel: ObservableObject {
return true
}
func setPointerInsideWindow(_ inside: Bool) {
pointerInsideWindow = inside
keyboardNavigation = false
if inside {
revealControls()
} else {
controlsHovered = false
titleHovered = false
hideControls(hideCursor: false)
}
}
func setControlsHovered(_ hovered: Bool) {
guard controlsHovered != hovered else { return }
controlsHovered = hovered
@@ -617,10 +631,11 @@ final class PlayerModel: ObservableObject {
func hideControls(hideCursor: Bool = true) {
guard shouldHideControls else { return }
hideDeadline = nil
keyboardNavigation = false
withAnimation(NSWorkspace.shared.accessibilityDisplayShouldReduceMotion ? nil : .easeIn(duration: 0.25)) {
controlsVisible = false
}
NSCursor.setHiddenUntilMouseMoves(hideCursor && windowActive && !isCompact)
NSCursor.setHiddenUntilMouseMoves(hideCursor && pointerInsideWindow && windowActive && !isCompact)
}
func beginSeeking() {
@@ -630,9 +645,14 @@ final class PlayerModel: ObservableObject {
revealControls()
}
func revealControls() {
func revealControls(forKeyboardNavigation: Bool = false) {
hideDeadline = nil
NSCursor.setHiddenUntilMouseMoves(false)
if forKeyboardNavigation { keyboardNavigation = true }
if !keyboardNavigation && !pointerInsideWindow && shouldHideControls {
hideControls(hideCursor: false)
return
}
if !controlsVisible {
withAnimation(NSWorkspace.shared.accessibilityDisplayShouldReduceMotion ? nil : .easeOut(duration: 0.25)) {
controlsVisible = true
@@ -643,9 +663,11 @@ final class PlayerModel: ObservableObject {
}
private var shouldHideControls: Bool {
canTogglePlayback && state.has_video && fileName != nil &&
!isSeeking && !transition.seekPending && !controlsHovered && !titleHovered && !controlsDragging &&
(windowActive || isCompact) && errorText == nil &&
((canTogglePlayback && fileName != nil && (state.has_video || !pointerInsideWindow)) ||
(!pointerInsideWindow && isBusy)) &&
!isSeeking && !transition.seekPending && !controlsDragging &&
(!pointerInsideWindow || (!controlsHovered && !titleHovered)) &&
(windowActive || isCompact || !pointerInsideWindow) && errorText == nil &&
!NSWorkspace.shared.isVoiceOverEnabled
}
@@ -709,7 +731,7 @@ final class PlayerModel: ObservableObject {
seekPosition = min(max(0, next.position.isFinite ? next.position : 0), duration)
}
fitVideoWindow()
if statusChanged { revealControls() }
if statusChanged || (!pointerInsideWindow && !keyboardNavigation && controlsVisible) { revealControls() }
if let hideDeadline, ContinuousClock.now >= hideDeadline { hideControls() }
updateAutomaticQuality()
if let restored = observation.restored {
@@ -1224,16 +1246,21 @@ private struct PlayerContent: View {
private var titlebar: some View {
HStack(spacing: 4) {
if let icon = model.fileIcon {
Image(nsImage: icon).resizable().frame(width: 16, height: 16)
} else {
Image(systemName: "play.rectangle.fill").font(.system(size: 14))
HStack(spacing: 4) {
if let icon = model.fileIcon {
Image(nsImage: icon).resizable().frame(width: 16, height: 16)
} else {
Image(systemName: "play.rectangle.fill").font(.system(size: 14))
}
Text(model.fileName ?? "terebi")
.font(.system(size: 13, weight: .semibold))
.lineLimit(1)
.truncationMode(.middle)
.help(model.fileName ?? "terebi")
}
Text(model.fileName ?? "terebi")
.font(.system(size: 13, weight: .semibold))
.lineLimit(1)
.truncationMode(.middle)
.help(model.fileName ?? "terebi")
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(PlayerGlass().clipShape(RoundedRectangle(cornerRadius: 8)).allowsHitTesting(false))
Spacer(minLength: 12)
iconButton(model.isOnTop ? "pin.fill" : "pin",
model.isOnTop ? "Stop Keeping Window on Top" : "Keep Window on Top", action: model.toggleOnTop)
@@ -1495,8 +1522,12 @@ private struct PlayerContent: View {
Text(model.fileName ?? "terebi")
.font(.system(size: 11, weight: .medium))
.lineLimit(1)
.truncationMode(.middle)
.help(model.fileName ?? "terebi")
.padding(8)
.background(PlayerGlass().clipShape(RoundedRectangle(cornerRadius: 10)).allowsHitTesting(false))
.frame(maxWidth: .infinity, alignment: .trailing)
.padding(8)
.frame(maxWidth: .infinity)
.foregroundStyle(.white.opacity(0.75))
.opacity(model.controlsVisible ? 1 : 0)
.allowsHitTesting(false)
@@ -1511,6 +1542,7 @@ private struct PlayerContent: View {
.accessibilityHidden(!model.isCompact)
titlebar
.frame(width: geometry.size.width)
.animation(compactChromeAnimation) { content in
content.opacity(model.isCompact ? 0 : 1)
}
@@ -1566,8 +1598,8 @@ private struct PlayerContent: View {
.onContinuousHover { phase in
switch phase {
case .active:
if model.isCompact { model.revealControls() }
case .ended: model.hideControls(hideCursor: false)
model.setPointerInsideWindow(true)
case .ended: model.setPointerInsideWindow(false)
}
}
}
@@ -1646,7 +1678,9 @@ private struct WindowChrome: NSViewRepresentable {
}
guard event.window === window else { return false }
if event.type == .leftMouseDown { self.dragOrigin = window.frame.origin }
if event.type == .keyDown && event.keyCode == 48 { self.model.revealControls() }
if event.type == .keyDown && event.keyCode == 48 {
self.model.revealControls(forKeyboardNavigation: true)
}
if event.type == .mouseMoved || event.type == .leftMouseDown {
self.model.revealControls()
} else if event.type == .keyDown {