fix: address swiftlint warnings
This commit is contained in:
@@ -257,54 +257,37 @@ enum CritterIconRenderer {
|
|||||||
private struct Canvas {
|
private struct Canvas {
|
||||||
let w: CGFloat
|
let w: CGFloat
|
||||||
let h: CGFloat
|
let h: CGFloat
|
||||||
|
let stepX: CGFloat
|
||||||
|
let stepY: CGFloat
|
||||||
let snapX: (CGFloat) -> CGFloat
|
let snapX: (CGFloat) -> CGFloat
|
||||||
let snapY: (CGFloat) -> CGFloat
|
let snapY: (CGFloat) -> CGFloat
|
||||||
let context: CGContext
|
let context: CGContext
|
||||||
}
|
}
|
||||||
|
|
||||||
static func makeIcon(
|
private struct Geometry {
|
||||||
blink: CGFloat,
|
let bodyRect: CGRect
|
||||||
legWiggle: CGFloat = 0,
|
let bodyCorner: CGFloat
|
||||||
earWiggle: CGFloat = 0,
|
let leftEarRect: CGRect
|
||||||
earScale: CGFloat = 1,
|
let rightEarRect: CGRect
|
||||||
earHoles: Bool = false,
|
let earCorner: CGFloat
|
||||||
eyesClosedLines: Bool = false,
|
let earW: CGFloat
|
||||||
badge: Badge? = nil) -> NSImage
|
let earH: CGFloat
|
||||||
{
|
let legW: CGFloat
|
||||||
// Force a 36×36px backing store (2× for the 18pt logical canvas) so the menu bar icon stays crisp on Retina.
|
let legH: CGFloat
|
||||||
let pixelsWide = 36
|
let legSpacing: CGFloat
|
||||||
let pixelsHigh = 36
|
let legStartX: CGFloat
|
||||||
guard let rep = NSBitmapImageRep(
|
let legYBase: CGFloat
|
||||||
bitmapDataPlanes: nil,
|
let legLift: CGFloat
|
||||||
pixelsWide: pixelsWide,
|
let legHeightScale: CGFloat
|
||||||
pixelsHigh: pixelsHigh,
|
let eyeW: CGFloat
|
||||||
bitsPerSample: 8,
|
let eyeY: CGFloat
|
||||||
samplesPerPixel: 4,
|
let eyeOffset: CGFloat
|
||||||
hasAlpha: true,
|
|
||||||
isPlanar: false,
|
|
||||||
colorSpaceName: .deviceRGB,
|
|
||||||
bitmapFormat: [],
|
|
||||||
bytesPerRow: 0,
|
|
||||||
bitsPerPixel: 0)
|
|
||||||
else {
|
|
||||||
return NSImage(size: self.size)
|
|
||||||
}
|
|
||||||
rep.size = self.size
|
|
||||||
|
|
||||||
NSGraphicsContext.saveGraphicsState()
|
init(canvas: Canvas, legWiggle: CGFloat, earWiggle: CGFloat, earScale: CGFloat) {
|
||||||
if let context = NSGraphicsContext(bitmapImageRep: rep) {
|
let w = canvas.w
|
||||||
NSGraphicsContext.current = context
|
let h = canvas.h
|
||||||
context.imageInterpolation = .none
|
let snapX = canvas.snapX
|
||||||
context.cgContext.setShouldAntialias(false)
|
let snapY = canvas.snapY
|
||||||
defer { NSGraphicsContext.restoreGraphicsState() }
|
|
||||||
|
|
||||||
let stepX = self.size.width / max(CGFloat(rep.pixelsWide), 1)
|
|
||||||
let stepY = self.size.height / max(CGFloat(rep.pixelsHigh), 1)
|
|
||||||
let snapX: (CGFloat) -> CGFloat = { ($0 / stepX).rounded() * stepX }
|
|
||||||
let snapY: (CGFloat) -> CGFloat = { ($0 / stepY).rounded() * stepY }
|
|
||||||
|
|
||||||
let w = snapX(size.width)
|
|
||||||
let h = snapY(size.height)
|
|
||||||
|
|
||||||
let bodyW = snapX(w * 0.78)
|
let bodyW = snapX(w * 0.78)
|
||||||
let bodyH = snapY(h * 0.58)
|
let bodyH = snapY(h * 0.58)
|
||||||
@@ -333,133 +316,77 @@ enum CritterIconRenderer {
|
|||||||
let legStartX = snapX((w - legsWidth) / 2)
|
let legStartX = snapX((w - legsWidth) / 2)
|
||||||
let legLift = snapY(legH * 0.35 * legWiggle)
|
let legLift = snapY(legH * 0.35 * legWiggle)
|
||||||
let legYBase = snapY(bodyY - legH + h * 0.05)
|
let legYBase = snapY(bodyY - legH + h * 0.05)
|
||||||
|
let legHeightScale = 1 - 0.12 * legWiggle
|
||||||
|
|
||||||
let eyeW = snapX(bodyW * 0.2)
|
let eyeW = snapX(bodyW * 0.2)
|
||||||
let eyeY = snapY(bodyY + bodyH * 0.56)
|
let eyeY = snapY(bodyY + bodyH * 0.56)
|
||||||
let eyeOffset = snapX(bodyW * 0.24)
|
let eyeOffset = snapX(bodyW * 0.24)
|
||||||
|
|
||||||
context.cgContext.setFillColor(NSColor.labelColor.cgColor)
|
self.bodyRect = CGRect(x: bodyX, y: bodyY, width: bodyW, height: bodyH)
|
||||||
|
self.bodyCorner = bodyCorner
|
||||||
|
self.leftEarRect = leftEarRect
|
||||||
|
self.rightEarRect = rightEarRect
|
||||||
|
self.earCorner = earCorner
|
||||||
|
self.earW = earW
|
||||||
|
self.earH = earH
|
||||||
|
self.legW = legW
|
||||||
|
self.legH = legH
|
||||||
|
self.legSpacing = legSpacing
|
||||||
|
self.legStartX = legStartX
|
||||||
|
self.legYBase = legYBase
|
||||||
|
self.legLift = legLift
|
||||||
|
self.legHeightScale = legHeightScale
|
||||||
|
self.eyeW = eyeW
|
||||||
|
self.eyeY = eyeY
|
||||||
|
self.eyeOffset = eyeOffset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
context.cgContext.addPath(CGPath(
|
private struct FaceOptions {
|
||||||
roundedRect: CGRect(x: bodyX, y: bodyY, width: bodyW, height: bodyH),
|
let blink: CGFloat
|
||||||
cornerWidth: bodyCorner,
|
let earHoles: Bool
|
||||||
cornerHeight: bodyCorner,
|
let earScale: CGFloat
|
||||||
transform: nil))
|
let eyesClosedLines: Bool
|
||||||
context.cgContext.addPath(CGPath(
|
}
|
||||||
roundedRect: leftEarRect,
|
|
||||||
cornerWidth: earCorner,
|
|
||||||
cornerHeight: earCorner,
|
|
||||||
transform: nil))
|
|
||||||
context.cgContext.addPath(CGPath(
|
|
||||||
roundedRect: rightEarRect,
|
|
||||||
cornerWidth: earCorner,
|
|
||||||
cornerHeight: earCorner,
|
|
||||||
transform: nil))
|
|
||||||
for i in 0..<4 {
|
|
||||||
let x = legStartX + CGFloat(i) * (legW + legSpacing)
|
|
||||||
let lift = (i % 2 == 0 ? legLift : -legLift)
|
|
||||||
let rect = CGRect(
|
|
||||||
x: x,
|
|
||||||
y: legYBase + lift,
|
|
||||||
width: legW,
|
|
||||||
height: legH * (1 - 0.12 * legWiggle))
|
|
||||||
context.cgContext.addPath(CGPath(
|
|
||||||
roundedRect: rect,
|
|
||||||
cornerWidth: legW * 0.34,
|
|
||||||
cornerHeight: legW * 0.34,
|
|
||||||
transform: nil))
|
|
||||||
}
|
|
||||||
context.cgContext.fillPath()
|
|
||||||
|
|
||||||
context.cgContext.saveGState()
|
static func makeIcon(
|
||||||
context.cgContext.setBlendMode(CGBlendMode.clear)
|
blink: CGFloat,
|
||||||
|
legWiggle: CGFloat = 0,
|
||||||
let leftCenter = CGPoint(x: snapX(w / 2 - eyeOffset), y: snapY(eyeY))
|
earWiggle: CGFloat = 0,
|
||||||
let rightCenter = CGPoint(x: snapX(w / 2 + eyeOffset), y: snapY(eyeY))
|
earScale: CGFloat = 1,
|
||||||
|
earHoles: Bool = false,
|
||||||
if earHoles || earScale > 1.05 {
|
eyesClosedLines: Bool = false,
|
||||||
let holeW = snapX(earW * 0.6)
|
badge: Badge? = nil) -> NSImage
|
||||||
let holeH = snapY(earH * 0.46)
|
{
|
||||||
let holeCorner = snapX(holeW * 0.34)
|
guard let rep = self.makeBitmapRep() else {
|
||||||
let leftHoleRect = CGRect(
|
|
||||||
x: snapX(leftEarRect.midX - holeW / 2),
|
|
||||||
y: snapY(leftEarRect.midY - holeH / 2 + earH * 0.04),
|
|
||||||
width: holeW,
|
|
||||||
height: holeH)
|
|
||||||
let rightHoleRect = CGRect(
|
|
||||||
x: snapX(rightEarRect.midX - holeW / 2),
|
|
||||||
y: snapY(rightEarRect.midY - holeH / 2 + earH * 0.04),
|
|
||||||
width: holeW,
|
|
||||||
height: holeH)
|
|
||||||
|
|
||||||
context.cgContext.addPath(CGPath(
|
|
||||||
roundedRect: leftHoleRect,
|
|
||||||
cornerWidth: holeCorner,
|
|
||||||
cornerHeight: holeCorner,
|
|
||||||
transform: nil))
|
|
||||||
context.cgContext.addPath(CGPath(
|
|
||||||
roundedRect: rightHoleRect,
|
|
||||||
cornerWidth: holeCorner,
|
|
||||||
cornerHeight: holeCorner,
|
|
||||||
transform: nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
if eyesClosedLines {
|
|
||||||
let lineW = snapX(eyeW * 0.95)
|
|
||||||
let lineH = snapY(max(stepY * 2, bodyH * 0.06))
|
|
||||||
let corner = snapX(lineH * 0.6)
|
|
||||||
let leftRect = CGRect(
|
|
||||||
x: snapX(leftCenter.x - lineW / 2),
|
|
||||||
y: snapY(leftCenter.y - lineH / 2),
|
|
||||||
width: lineW,
|
|
||||||
height: lineH)
|
|
||||||
let rightRect = CGRect(
|
|
||||||
x: snapX(rightCenter.x - lineW / 2),
|
|
||||||
y: snapY(rightCenter.y - lineH / 2),
|
|
||||||
width: lineW,
|
|
||||||
height: lineH)
|
|
||||||
context.cgContext.addPath(CGPath(
|
|
||||||
roundedRect: leftRect,
|
|
||||||
cornerWidth: corner,
|
|
||||||
cornerHeight: corner,
|
|
||||||
transform: nil))
|
|
||||||
context.cgContext.addPath(CGPath(
|
|
||||||
roundedRect: rightRect,
|
|
||||||
cornerWidth: corner,
|
|
||||||
cornerHeight: corner,
|
|
||||||
transform: nil))
|
|
||||||
} else {
|
|
||||||
let eyeOpen = max(0.05, 1 - blink)
|
|
||||||
let eyeH = snapY(bodyH * 0.26 * eyeOpen)
|
|
||||||
|
|
||||||
let left = CGMutablePath()
|
|
||||||
left.move(to: CGPoint(x: snapX(leftCenter.x - eyeW / 2), y: snapY(leftCenter.y - eyeH)))
|
|
||||||
left.addLine(to: CGPoint(x: snapX(leftCenter.x + eyeW / 2), y: snapY(leftCenter.y)))
|
|
||||||
left.addLine(to: CGPoint(x: snapX(leftCenter.x - eyeW / 2), y: snapY(leftCenter.y + eyeH)))
|
|
||||||
left.closeSubpath()
|
|
||||||
|
|
||||||
let right = CGMutablePath()
|
|
||||||
right.move(to: CGPoint(x: snapX(rightCenter.x + eyeW / 2), y: snapY(rightCenter.y - eyeH)))
|
|
||||||
right.addLine(to: CGPoint(x: snapX(rightCenter.x - eyeW / 2), y: snapY(rightCenter.y)))
|
|
||||||
right.addLine(to: CGPoint(x: snapX(rightCenter.x + eyeW / 2), y: snapY(rightCenter.y + eyeH)))
|
|
||||||
right.closeSubpath()
|
|
||||||
|
|
||||||
context.cgContext.addPath(left)
|
|
||||||
context.cgContext.addPath(right)
|
|
||||||
}
|
|
||||||
|
|
||||||
context.cgContext.fillPath()
|
|
||||||
context.cgContext.restoreGState()
|
|
||||||
|
|
||||||
if let badge {
|
|
||||||
self.drawBadge(
|
|
||||||
badge,
|
|
||||||
canvas: Canvas(w: w, h: h, snapX: snapX, snapY: snapY, context: context.cgContext))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
NSGraphicsContext.restoreGraphicsState()
|
|
||||||
return NSImage(size: self.size)
|
return NSImage(size: self.size)
|
||||||
}
|
}
|
||||||
|
rep.size = self.size
|
||||||
|
|
||||||
|
NSGraphicsContext.saveGraphicsState()
|
||||||
|
defer { NSGraphicsContext.restoreGraphicsState() }
|
||||||
|
|
||||||
|
guard let context = NSGraphicsContext(bitmapImageRep: rep) else {
|
||||||
|
return NSImage(size: self.size)
|
||||||
|
}
|
||||||
|
NSGraphicsContext.current = context
|
||||||
|
context.imageInterpolation = .none
|
||||||
|
context.cgContext.setShouldAntialias(false)
|
||||||
|
|
||||||
|
let canvas = self.makeCanvas(for: rep, context: context)
|
||||||
|
let geometry = Geometry(canvas: canvas, legWiggle: legWiggle, earWiggle: earWiggle, earScale: earScale)
|
||||||
|
|
||||||
|
self.drawBody(in: canvas, geometry: geometry)
|
||||||
|
let face = FaceOptions(
|
||||||
|
blink: blink,
|
||||||
|
earHoles: earHoles,
|
||||||
|
earScale: earScale,
|
||||||
|
eyesClosedLines: eyesClosedLines)
|
||||||
|
self.drawFace(in: canvas, geometry: geometry, options: face)
|
||||||
|
|
||||||
|
if let badge {
|
||||||
|
self.drawBadge(badge, canvas: canvas)
|
||||||
|
}
|
||||||
|
|
||||||
let image = NSImage(size: size)
|
let image = NSImage(size: size)
|
||||||
image.addRepresentation(rep)
|
image.addRepresentation(rep)
|
||||||
@@ -467,6 +394,181 @@ enum CritterIconRenderer {
|
|||||||
return image
|
return image
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func makeBitmapRep() -> NSBitmapImageRep? {
|
||||||
|
// Force a 36×36px backing store (2× for the 18pt logical canvas) so the menu bar icon stays crisp on Retina.
|
||||||
|
let pixelsWide = 36
|
||||||
|
let pixelsHigh = 36
|
||||||
|
return NSBitmapImageRep(
|
||||||
|
bitmapDataPlanes: nil,
|
||||||
|
pixelsWide: pixelsWide,
|
||||||
|
pixelsHigh: pixelsHigh,
|
||||||
|
bitsPerSample: 8,
|
||||||
|
samplesPerPixel: 4,
|
||||||
|
hasAlpha: true,
|
||||||
|
isPlanar: false,
|
||||||
|
colorSpaceName: .deviceRGB,
|
||||||
|
bitmapFormat: [],
|
||||||
|
bytesPerRow: 0,
|
||||||
|
bitsPerPixel: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func makeCanvas(for rep: NSBitmapImageRep, context: NSGraphicsContext) -> Canvas {
|
||||||
|
let stepX = self.size.width / max(CGFloat(rep.pixelsWide), 1)
|
||||||
|
let stepY = self.size.height / max(CGFloat(rep.pixelsHigh), 1)
|
||||||
|
let snapX: (CGFloat) -> CGFloat = { ($0 / stepX).rounded() * stepX }
|
||||||
|
let snapY: (CGFloat) -> CGFloat = { ($0 / stepY).rounded() * stepY }
|
||||||
|
|
||||||
|
let w = snapX(size.width)
|
||||||
|
let h = snapY(size.height)
|
||||||
|
|
||||||
|
return Canvas(
|
||||||
|
w: w,
|
||||||
|
h: h,
|
||||||
|
stepX: stepX,
|
||||||
|
stepY: stepY,
|
||||||
|
snapX: snapX,
|
||||||
|
snapY: snapY,
|
||||||
|
context: context.cgContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func drawBody(in canvas: Canvas, geometry: Geometry) {
|
||||||
|
canvas.context.setFillColor(NSColor.labelColor.cgColor)
|
||||||
|
|
||||||
|
canvas.context.addPath(CGPath(
|
||||||
|
roundedRect: geometry.bodyRect,
|
||||||
|
cornerWidth: geometry.bodyCorner,
|
||||||
|
cornerHeight: geometry.bodyCorner,
|
||||||
|
transform: nil))
|
||||||
|
canvas.context.addPath(CGPath(
|
||||||
|
roundedRect: geometry.leftEarRect,
|
||||||
|
cornerWidth: geometry.earCorner,
|
||||||
|
cornerHeight: geometry.earCorner,
|
||||||
|
transform: nil))
|
||||||
|
canvas.context.addPath(CGPath(
|
||||||
|
roundedRect: geometry.rightEarRect,
|
||||||
|
cornerWidth: geometry.earCorner,
|
||||||
|
cornerHeight: geometry.earCorner,
|
||||||
|
transform: nil))
|
||||||
|
|
||||||
|
for i in 0..<4 {
|
||||||
|
let x = geometry.legStartX + CGFloat(i) * (geometry.legW + geometry.legSpacing)
|
||||||
|
let lift = i % 2 == 0 ? geometry.legLift : -geometry.legLift
|
||||||
|
let rect = CGRect(
|
||||||
|
x: x,
|
||||||
|
y: geometry.legYBase + lift,
|
||||||
|
width: geometry.legW,
|
||||||
|
height: geometry.legH * geometry.legHeightScale)
|
||||||
|
canvas.context.addPath(CGPath(
|
||||||
|
roundedRect: rect,
|
||||||
|
cornerWidth: geometry.legW * 0.34,
|
||||||
|
cornerHeight: geometry.legW * 0.34,
|
||||||
|
transform: nil))
|
||||||
|
}
|
||||||
|
canvas.context.fillPath()
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func drawFace(
|
||||||
|
in canvas: Canvas,
|
||||||
|
geometry: Geometry,
|
||||||
|
options: FaceOptions)
|
||||||
|
{
|
||||||
|
canvas.context.saveGState()
|
||||||
|
canvas.context.setBlendMode(.clear)
|
||||||
|
|
||||||
|
let leftCenter = CGPoint(
|
||||||
|
x: canvas.snapX(canvas.w / 2 - geometry.eyeOffset),
|
||||||
|
y: canvas.snapY(geometry.eyeY))
|
||||||
|
let rightCenter = CGPoint(
|
||||||
|
x: canvas.snapX(canvas.w / 2 + geometry.eyeOffset),
|
||||||
|
y: canvas.snapY(geometry.eyeY))
|
||||||
|
|
||||||
|
if options.earHoles || options.earScale > 1.05 {
|
||||||
|
let holeW = canvas.snapX(geometry.earW * 0.6)
|
||||||
|
let holeH = canvas.snapY(geometry.earH * 0.46)
|
||||||
|
let holeCorner = canvas.snapX(holeW * 0.34)
|
||||||
|
let leftHoleRect = CGRect(
|
||||||
|
x: canvas.snapX(geometry.leftEarRect.midX - holeW / 2),
|
||||||
|
y: canvas.snapY(geometry.leftEarRect.midY - holeH / 2 + geometry.earH * 0.04),
|
||||||
|
width: holeW,
|
||||||
|
height: holeH)
|
||||||
|
let rightHoleRect = CGRect(
|
||||||
|
x: canvas.snapX(geometry.rightEarRect.midX - holeW / 2),
|
||||||
|
y: canvas.snapY(geometry.rightEarRect.midY - holeH / 2 + geometry.earH * 0.04),
|
||||||
|
width: holeW,
|
||||||
|
height: holeH)
|
||||||
|
|
||||||
|
canvas.context.addPath(CGPath(
|
||||||
|
roundedRect: leftHoleRect,
|
||||||
|
cornerWidth: holeCorner,
|
||||||
|
cornerHeight: holeCorner,
|
||||||
|
transform: nil))
|
||||||
|
canvas.context.addPath(CGPath(
|
||||||
|
roundedRect: rightHoleRect,
|
||||||
|
cornerWidth: holeCorner,
|
||||||
|
cornerHeight: holeCorner,
|
||||||
|
transform: nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.eyesClosedLines {
|
||||||
|
let lineW = canvas.snapX(geometry.eyeW * 0.95)
|
||||||
|
let lineH = canvas.snapY(max(canvas.stepY * 2, geometry.bodyRect.height * 0.06))
|
||||||
|
let corner = canvas.snapX(lineH * 0.6)
|
||||||
|
let leftRect = CGRect(
|
||||||
|
x: canvas.snapX(leftCenter.x - lineW / 2),
|
||||||
|
y: canvas.snapY(leftCenter.y - lineH / 2),
|
||||||
|
width: lineW,
|
||||||
|
height: lineH)
|
||||||
|
let rightRect = CGRect(
|
||||||
|
x: canvas.snapX(rightCenter.x - lineW / 2),
|
||||||
|
y: canvas.snapY(rightCenter.y - lineH / 2),
|
||||||
|
width: lineW,
|
||||||
|
height: lineH)
|
||||||
|
canvas.context.addPath(CGPath(
|
||||||
|
roundedRect: leftRect,
|
||||||
|
cornerWidth: corner,
|
||||||
|
cornerHeight: corner,
|
||||||
|
transform: nil))
|
||||||
|
canvas.context.addPath(CGPath(
|
||||||
|
roundedRect: rightRect,
|
||||||
|
cornerWidth: corner,
|
||||||
|
cornerHeight: corner,
|
||||||
|
transform: nil))
|
||||||
|
} else {
|
||||||
|
let eyeOpen = max(0.05, 1 - options.blink)
|
||||||
|
let eyeH = canvas.snapY(geometry.bodyRect.height * 0.26 * eyeOpen)
|
||||||
|
|
||||||
|
let left = CGMutablePath()
|
||||||
|
left.move(to: CGPoint(
|
||||||
|
x: canvas.snapX(leftCenter.x - geometry.eyeW / 2),
|
||||||
|
y: canvas.snapY(leftCenter.y - eyeH)))
|
||||||
|
left.addLine(to: CGPoint(
|
||||||
|
x: canvas.snapX(leftCenter.x + geometry.eyeW / 2),
|
||||||
|
y: canvas.snapY(leftCenter.y)))
|
||||||
|
left.addLine(to: CGPoint(
|
||||||
|
x: canvas.snapX(leftCenter.x - geometry.eyeW / 2),
|
||||||
|
y: canvas.snapY(leftCenter.y + eyeH)))
|
||||||
|
left.closeSubpath()
|
||||||
|
|
||||||
|
let right = CGMutablePath()
|
||||||
|
right.move(to: CGPoint(
|
||||||
|
x: canvas.snapX(rightCenter.x + geometry.eyeW / 2),
|
||||||
|
y: canvas.snapY(rightCenter.y - eyeH)))
|
||||||
|
right.addLine(to: CGPoint(
|
||||||
|
x: canvas.snapX(rightCenter.x - geometry.eyeW / 2),
|
||||||
|
y: canvas.snapY(rightCenter.y)))
|
||||||
|
right.addLine(to: CGPoint(
|
||||||
|
x: canvas.snapX(rightCenter.x + geometry.eyeW / 2),
|
||||||
|
y: canvas.snapY(rightCenter.y + eyeH)))
|
||||||
|
right.closeSubpath()
|
||||||
|
|
||||||
|
canvas.context.addPath(left)
|
||||||
|
canvas.context.addPath(right)
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.context.fillPath()
|
||||||
|
canvas.context.restoreGState()
|
||||||
|
}
|
||||||
|
|
||||||
private static func drawBadge(_ badge: Badge, canvas: Canvas) {
|
private static func drawBadge(_ badge: Badge, canvas: Canvas) {
|
||||||
let strength: CGFloat = switch badge.prominence {
|
let strength: CGFloat = switch badge.prominence {
|
||||||
case .primary: 1.0
|
case .primary: 1.0
|
||||||
|
|||||||
@@ -72,6 +72,14 @@ actor VoiceWakeRuntime {
|
|||||||
let sendChime: VoiceWakeChime
|
let sendChime: VoiceWakeChime
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct RecognitionUpdate {
|
||||||
|
let transcript: String?
|
||||||
|
let segments: [WakeWordSegment]
|
||||||
|
let isFinal: Bool
|
||||||
|
let error: Error?
|
||||||
|
let generation: Int
|
||||||
|
}
|
||||||
|
|
||||||
func refresh(state: AppState) async {
|
func refresh(state: AppState) async {
|
||||||
let snapshot = await MainActor.run { () -> (Bool, RuntimeConfig) in
|
let snapshot = await MainActor.run { () -> (Bool, RuntimeConfig) in
|
||||||
let enabled = state.swabbleEnabled
|
let enabled = state.swabbleEnabled
|
||||||
@@ -154,13 +162,13 @@ actor VoiceWakeRuntime {
|
|||||||
.map { WakeWordSpeechSegments.from(transcription: result.bestTranscription, transcript: $0) }
|
.map { WakeWordSpeechSegments.from(transcription: result.bestTranscription, transcript: $0) }
|
||||||
} ?? []
|
} ?? []
|
||||||
let isFinal = result?.isFinal ?? false
|
let isFinal = result?.isFinal ?? false
|
||||||
Task { await self.handleRecognition(
|
let update = RecognitionUpdate(
|
||||||
transcript: transcript,
|
transcript: transcript,
|
||||||
segments: segments,
|
segments: segments,
|
||||||
isFinal: isFinal,
|
isFinal: isFinal,
|
||||||
error: error,
|
error: error,
|
||||||
config: config,
|
generation: generation)
|
||||||
generation: generation) }
|
Task { await self.handleRecognition(update, config: config) }
|
||||||
}
|
}
|
||||||
|
|
||||||
self.logger.info("voicewake runtime started")
|
self.logger.info("voicewake runtime started")
|
||||||
@@ -212,22 +220,15 @@ actor VoiceWakeRuntime {
|
|||||||
self.recognizer = SFSpeechRecognizer(locale: locale)
|
self.recognizer = SFSpeechRecognizer(locale: locale)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleRecognition(
|
private func handleRecognition(_ update: RecognitionUpdate, config: RuntimeConfig) async {
|
||||||
transcript: String?,
|
if update.generation != self.recognitionGeneration {
|
||||||
segments: [WakeWordSegment],
|
|
||||||
isFinal: Bool,
|
|
||||||
error: Error?,
|
|
||||||
config: RuntimeConfig,
|
|
||||||
generation: Int) async
|
|
||||||
{
|
|
||||||
if generation != self.recognitionGeneration {
|
|
||||||
return // stale callback from a superseded recognizer session
|
return // stale callback from a superseded recognizer session
|
||||||
}
|
}
|
||||||
if let error {
|
if let error = update.error {
|
||||||
self.logger.debug("voicewake recognition error: \(error.localizedDescription, privacy: .public)")
|
self.logger.debug("voicewake recognition error: \(error.localizedDescription, privacy: .public)")
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let transcript else { return }
|
guard let transcript = update.transcript else { return }
|
||||||
|
|
||||||
let now = Date()
|
let now = Date()
|
||||||
if !transcript.isEmpty {
|
if !transcript.isEmpty {
|
||||||
@@ -240,7 +241,7 @@ actor VoiceWakeRuntime {
|
|||||||
triggers: config.triggers)
|
triggers: config.triggers)
|
||||||
self.capturedTranscript = trimmed
|
self.capturedTranscript = trimmed
|
||||||
self.updateHeardBeyondTrigger(withTrimmed: trimmed)
|
self.updateHeardBeyondTrigger(withTrimmed: trimmed)
|
||||||
if isFinal {
|
if update.isFinal {
|
||||||
self.committedTranscript = trimmed
|
self.committedTranscript = trimmed
|
||||||
self.volatileTranscript = ""
|
self.volatileTranscript = ""
|
||||||
} else {
|
} else {
|
||||||
@@ -250,7 +251,7 @@ actor VoiceWakeRuntime {
|
|||||||
let attributed = Self.makeAttributed(
|
let attributed = Self.makeAttributed(
|
||||||
committed: self.committedTranscript,
|
committed: self.committedTranscript,
|
||||||
volatile: self.volatileTranscript,
|
volatile: self.volatileTranscript,
|
||||||
isFinal: isFinal)
|
isFinal: update.isFinal)
|
||||||
let snapshot = self.committedTranscript + self.volatileTranscript
|
let snapshot = self.committedTranscript + self.volatileTranscript
|
||||||
if let token = self.overlayToken {
|
if let token = self.overlayToken {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
@@ -266,7 +267,7 @@ actor VoiceWakeRuntime {
|
|||||||
if self.isCapturing { return }
|
if self.isCapturing { return }
|
||||||
|
|
||||||
let gateConfig = WakeWordGateConfig(triggers: config.triggers)
|
let gateConfig = WakeWordGateConfig(triggers: config.triggers)
|
||||||
if let match = WakeWordGate.match(transcript: transcript, segments: segments, config: gateConfig) {
|
if let match = WakeWordGate.match(transcript: transcript, segments: update.segments, config: gateConfig) {
|
||||||
if let cooldown = cooldownUntil, now < cooldown {
|
if let cooldown = cooldownUntil, now < cooldown {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user