perf: throttle voice overlay updates

This commit is contained in:
Peter Steinberger
2025-12-24 13:51:09 +01:00
parent 88d20c5419
commit 5ba90db049

View File

@@ -40,6 +40,7 @@ final class VoiceWakeOverlayController {
private var autoSendToken: UUID? private var autoSendToken: UUID?
private var activeToken: UUID? private var activeToken: UUID?
private var activeSource: Source? private var activeSource: Source?
private var lastLevelUpdate: TimeInterval = 0
private let width: CGFloat = 360 private let width: CGFloat = 360
private let padding: CGFloat = 10 private let padding: CGFloat = 10
@@ -49,6 +50,7 @@ final class VoiceWakeOverlayController {
private let maxHeight: CGFloat = 400 private let maxHeight: CGFloat = 400
private let minHeight: CGFloat = 48 private let minHeight: CGFloat = 48
let closeOverflow: CGFloat = 10 let closeOverflow: CGFloat = 10
private let levelUpdateInterval: TimeInterval = 1.0 / 12.0
init(enableUI: Bool = true) { init(enableUI: Bool = true) {
self.enableUI = enableUI self.enableUI = enableUI
@@ -78,6 +80,7 @@ final class VoiceWakeOverlayController {
self.model.isEditing = false self.model.isEditing = false
self.model.attributed = attributed ?? self.makeAttributed(from: transcript) self.model.attributed = attributed ?? self.makeAttributed(from: transcript)
self.model.level = 0 self.model.level = 0
self.lastLevelUpdate = 0
self.present() self.present()
self.updateWindowFrame(animate: true) self.updateWindowFrame(animate: true)
return token return token
@@ -218,6 +221,7 @@ final class VoiceWakeOverlayController {
if !self.enableUI { if !self.enableUI {
self.model.isVisible = false self.model.isVisible = false
self.model.level = 0 self.model.level = 0
self.lastLevelUpdate = 0
self.activeToken = nil self.activeToken = nil
self.activeSource = nil self.activeSource = nil
return return
@@ -245,6 +249,7 @@ final class VoiceWakeOverlayController {
window.orderOut(nil) window.orderOut(nil)
self.model.isVisible = false self.model.isVisible = false
self.model.level = 0 self.model.level = 0
self.lastLevelUpdate = 0
self.activeToken = nil self.activeToken = nil
self.activeSource = nil self.activeSource = nil
if outcome == .empty { if outcome == .empty {
@@ -260,6 +265,12 @@ final class VoiceWakeOverlayController {
func updateLevel(token: UUID, _ level: Double) { func updateLevel(token: UUID, _ level: Double) {
guard self.guardToken(token, context: "level") else { return } guard self.guardToken(token, context: "level") else { return }
guard self.model.isVisible else { return }
let now = ProcessInfo.processInfo.systemUptime
if level != 0, now - self.lastLevelUpdate < self.levelUpdateInterval {
return
}
self.lastLevelUpdate = now
self.model.level = max(0, min(1, level)) self.model.level = max(0, min(1, level))
} }
@@ -506,6 +517,7 @@ struct VoiceWakeOverlayView: View {
self.textFocused = true self.textFocused = true
}) })
.frame(maxWidth: .infinity, minHeight: 32, maxHeight: .infinity, alignment: .topLeading) .frame(maxWidth: .infinity, minHeight: 32, maxHeight: .infinity, alignment: .topLeading)
.focusable(false)
.id("display") .id("display")
} }
@@ -549,10 +561,8 @@ struct VoiceWakeOverlayView: View {
.padding(.horizontal, 10) .padding(.horizontal, 10)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.background { .background {
let shape = RoundedRectangle(cornerRadius: 12, style: .continuous) OverlayBackground()
VisualEffectView(material: .hudWindow, blendingMode: .behindWindow) .equatable()
.clipShape(shape)
.overlay(shape.strokeBorder(Color.white.opacity(0.16), lineWidth: 1))
} }
.shadow(color: Color.black.opacity(0.22), radius: 14, x: 0, y: -2) .shadow(color: Color.black.opacity(0.22), radius: 14, x: 0, y: -2)
.onHover { self.isHovering = $0 } .onHover { self.isHovering = $0 }
@@ -567,20 +577,38 @@ struct VoiceWakeOverlayView: View {
.padding(.leading, self.controller.closeOverflow) .padding(.leading, self.controller.closeOverflow)
.padding(.trailing, self.controller.closeOverflow) .padding(.trailing, self.controller.closeOverflow)
.padding(.bottom, self.controller.closeOverflow) .padding(.bottom, self.controller.closeOverflow)
.onAppear { self.textFocused = false } .onAppear {
.onChange(of: self.controller.model.text) { _, _ in self.updateFocusState(visible: self.controller.model.isVisible, editing: self.controller.model.isEditing)
self.textFocused = self.controller.model.isEditing
} }
.onChange(of: self.controller.model.isVisible) { _, visible in .onChange(of: self.controller.model.isVisible) { _, visible in
if visible { self.textFocused = self.controller.model.isEditing } self.updateFocusState(visible: visible, editing: self.controller.model.isEditing)
} }
.onChange(of: self.controller.model.isEditing) { _, editing in .onChange(of: self.controller.model.isEditing) { _, editing in
self.textFocused = editing self.updateFocusState(visible: self.controller.model.isVisible, editing: editing)
} }
.onChange(of: self.controller.model.attributed) { _, _ in .onChange(of: self.controller.model.attributed) { _, _ in
self.controller.updateWindowFrame(animate: true) self.controller.updateWindowFrame(animate: true)
} }
} }
private func updateFocusState(visible: Bool, editing: Bool) {
let shouldFocus = visible && editing
guard self.textFocused != shouldFocus else { return }
self.textFocused = shouldFocus
}
}
private struct OverlayBackground: View {
var body: some View {
let shape = RoundedRectangle(cornerRadius: 12, style: .continuous)
VisualEffectView(material: .hudWindow, blendingMode: .behindWindow)
.clipShape(shape)
.overlay(shape.strokeBorder(Color.white.opacity(0.16), lineWidth: 1))
}
}
extension OverlayBackground: @MainActor Equatable {
static func == (lhs: Self, rhs: Self) -> Bool { true }
} }
struct TranscriptTextView: NSViewRepresentable { struct TranscriptTextView: NSViewRepresentable {