ui: match titlebar chrome and prefer upward caption menus

This commit is contained in:
2026-10-12 23:23:33 +02:00
parent 7d24ce609e
commit 2398370c23
2 changed files with 80 additions and 28 deletions
+78 -26
View File
@@ -1305,6 +1305,73 @@ private struct VideoSurface: NSViewRepresentable {
func updateNSView(_ nsView: NSView, context: Context) {}
}
@MainActor
private struct CaptionSelector: NSViewRepresentable {
let model: PlayerModel
func makeNSView(context: Context) -> Selector { Selector(model: model) }
func updateNSView(_ view: Selector, context: Context) { view.updateTracks() }
final class Selector: NSPopUpButton {
private let model: PlayerModel
private var displayedTracks: [ResolvedSubtitle] = []
private var displayedSelection: Int?
private let captionItem = NSMenuItem(title: "", action: nil, keyEquivalent: "")
init(model: PlayerModel) {
self.model = model
super.init(frame: NSRect(x: 0, y: 0, width: 24, height: 24), pullsDown: true)
usesItemFromMenu = false
preferredEdge = .maxY
isBordered = false
imagePosition = .imageOnly
imageScaling = .scaleProportionallyUpOrDown
(cell as? NSPopUpButtonCell)?.arrowPosition = .noArrow
let symbol = NSImage(systemSymbolName: "captions.bubble", accessibilityDescription: "Subtitles")?
.withSymbolConfiguration(.init(pointSize: 22, weight: .regular))
symbol?.size = NSSize(width: 24, height: 24)
captionItem.image = symbol
contentTintColor = SunsetPalette.text
toolTip = "Subtitles"
setAccessibilityLabel("Subtitles")
updateTracks()
}
required init?(coder: NSCoder) { nil }
func updateTracks() {
guard menu == nil || displayedSelection != model.selectedSubtitle ||
displayedTracks.count != model.subtitleTracks.count ||
zip(displayedTracks, model.subtitleTracks).contains(where: {
$0.label != $1.label || $0.language != $1.language
}) else { return }
displayedTracks = model.subtitleTracks
displayedSelection = model.selectedSubtitle
let tracks = NSMenu()
tracks.autoenablesItems = false
func add(_ label: String, index: Int?) {
let item = NSMenuItem(title: label, action: #selector(selectTrack(_:)), keyEquivalent: "")
item.target = self
item.tag = index ?? -1
item.state = model.selectedSubtitle == index ? .on : .off
tracks.addItem(item)
}
add("Off", index: nil)
for (index, track) in model.subtitleTracks.enumerated() {
add(track.label + " (" + track.language + ")", index: index)
}
menu = tracks
(cell as? NSPopUpButtonCell)?.menuItem = captionItem
isEnabled = !model.subtitleTracks.isEmpty
}
@objc private func selectTrack(_ item: NSMenuItem) {
model.selectSubtitle(item.tag < 0 ? nil : item.tag)
model.revealControls()
}
}
}
@MainActor
private struct CompactDragSurface: NSViewRepresentable {
let model: PlayerModel
@@ -1452,10 +1519,10 @@ private struct PlayerContent: View {
.help(model.fileName ?? "terebi")
}
.padding(.horizontal, 8)
.padding(.vertical, 4)
.frame(height: 24)
.background(PlayerGlass(cornerRadius: 8).clipShape(RoundedRectangle(cornerRadius: 8)).allowsHitTesting(false))
Spacer(minLength: 12)
titleControls
titleControls(compact: false)
}
.padding(.leading, model.isFullScreen ? 16 : 86)
.padding(.trailing, 12)
@@ -1467,7 +1534,7 @@ private struct PlayerContent: View {
.accessibilityHidden(!model.controlsVisible)
}
private var titleControls: some View {
private func titleControls(compact: Bool) -> some View {
HStack(spacing: 4) {
iconButton(model.isOnTop ? "pin.fill" : "pin",
model.isOnTop ? "Stop Keeping Window on Top" : "Keep Window on Top", action: model.toggleOnTop)
@@ -1477,30 +1544,14 @@ private struct PlayerContent: View {
actionsMenu
}
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(PlayerGlass(cornerRadius: 8).clipShape(RoundedRectangle(cornerRadius: 8)).allowsHitTesting(false))
.frame(height: compact ? 28 : 24)
.background(PlayerGlass(cornerRadius: compact ? 10 : 8)
.clipShape(RoundedRectangle(cornerRadius: compact ? 10 : 8)).allowsHitTesting(false))
}
private var captionsMenu: some View {
Menu {
Button("Off") { model.selectSubtitle(nil) }
ForEach(model.subtitleTracks.indices, id: \.self) { index in
Button {
model.selectSubtitle(index)
} label: {
Label(model.subtitleTracks[index].label + " (" + model.subtitleTracks[index].language + ")",
systemImage: model.selectedSubtitle == index ? "checkmark" : "text.bubble")
}
}
} label: {
Image(systemName: "captions.bubble").font(.system(size: 17)).frame(width: 24, height: 24)
}
.menuStyle(.borderlessButton)
.menuIndicator(.hidden)
.fixedSize()
.disabled(model.subtitleTracks.isEmpty)
.accessibilityLabel("Subtitles")
.help("Subtitles")
CaptionSelector(model: model)
.frame(width: 24, height: 24)
}
private var actionsMenu: some View {
@@ -1817,11 +1868,12 @@ private struct PlayerContent: View {
.font(.system(size: 11, weight: .medium))
.accessibilityElement(children: .combine)
.help((model.fileName ?? "terebi") + (progress.map { " · \($0)" } ?? ""))
.padding(8)
.padding(.horizontal, 8)
.frame(height: 28)
.background(PlayerGlass(cornerRadius: 10).clipShape(RoundedRectangle(cornerRadius: 10)).allowsHitTesting(false))
.allowsHitTesting(false)
Spacer(minLength: 0)
titleControls
titleControls(compact: true)
}
.padding(8)
.foregroundStyle(Color(SunsetPalette.text))