perf: throttle voice overlay updates
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user