//go:build darwin package overlay /* #cgo CFLAGS: -x objective-c #cgo LDFLAGS: -framework Cocoa -framework ApplicationServices #import #import #include #include #include // ---- Visual constants for the borderless macOS light-wave overlay ---- #define CAP_W 620.0f #define CAP_H 112.0f #define DRAG_W 280.0f #define DRAG_H 76.0f #define BAR_W 7.0f #define BAR_GAP 13.0f #define BAR_R 3.5f #define N_BARS 11 #define TXT_SZ 22.0f #define TXT_GAP 52.0f // ---- Shared state (written by Go, read on main thread) ---- typedef struct { float barHeights[N_BARS]; float barR, barG, barB; char text[128]; int status; float volumes[N_BARS]; float animTime; float fadeProgress; float fadeTarget; int posX, posY; int needsPosition; int hasPosition; int dragEnded; int dragX, dragY; } OverlayState; static OverlayState g_state = { .barHeights = {14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14}, .barR = 0, .barG = 0.478f, .barB = 1.0f, }; static float clamp01(float v) { if (v < 0) return 0; if (v > 1) return 1; return v; } static float mixf(float a, float b, float t) { return a + (b - a) * clamp01(t); } static void spectralColor(float p, float* r, float* g, float* b) { float c1[3] = {0.19f, 0.88f, 1.00f}; float c2[3] = {0.10f, 0.46f, 1.00f}; float c3[3] = {0.56f, 0.36f, 1.00f}; float c4[3] = {1.00f, 0.30f, 0.84f}; float c5[3] = {1.00f, 0.62f, 0.74f}; float* a = c1; float* z = c2; float t = 0; if (p < 0.25f) { a = c1; z = c2; t = p / 0.25f; } else if (p < 0.55f) { a = c2; z = c3; t = (p - 0.25f) / 0.30f; } else if (p < 0.80f) { a = c3; z = c4; t = (p - 0.55f) / 0.25f; } else { a = c4; z = c5; t = (p - 0.80f) / 0.20f; } *r = mixf(a[0], z[0], t); *g = mixf(a[1], z[1], t); *b = mixf(a[2], z[2], t); } static void applyStatusTint(float* r, float* g, float* b, float amount) { *r = mixf(*r, g_state.barR, amount); *g = mixf(*g, g_state.barG, amount); *b = mixf(*b, g_state.barB, amount); } static void drawEllipticalGlow(CGContextRef ctx, float cx, float cy, float rx, float ry, float r, float g, float b, float alpha) { CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); if (!colorSpace) return; CGFloat locations[] = {0.0f, 0.55f, 1.0f}; CGFloat components[] = { r, g, b, alpha, r, g, b, alpha * 0.28f, r, g, b, 0.0f, }; CGGradientRef gradient = CGGradientCreateWithColorComponents(colorSpace, components, locations, 3); if (!gradient) { CGColorSpaceRelease(colorSpace); return; } CGContextSaveGState(ctx); CGContextTranslateCTM(ctx, cx, cy); CGContextScaleCTM(ctx, rx, ry); CGContextDrawRadialGradient(ctx, gradient, CGPointMake(0, 0), 0.0f, CGPointMake(0, 0), 1.0f, 0); CGContextRestoreGState(ctx); CGGradientRelease(gradient); CGColorSpaceRelease(colorSpace); } static void drawWavePath(CGContextRef ctx, float startX, float endX, float centerY, float amplitude, float phase, float cycles, float r, float g, float b, float alpha, float lineWidth) { CGMutablePathRef path = CGPathCreateMutable(); if (!path) return; int steps = 180; for (int i = 0; i <= steps; i++) { float p = (float)i / (float)steps; float x = mixf(startX, endX, p); float edgeFade = sinf(p * (float)M_PI); float y = centerY + sinf(p * cycles * 2.0f * (float)M_PI + phase) * amplitude * edgeFade + sinf(p * (cycles * 0.55f) * 2.0f * (float)M_PI + phase * 1.7f) * amplitude * 0.36f * edgeFade; if (i == 0) { CGPathMoveToPoint(path, NULL, x, y); } else { CGPathAddLineToPoint(path, NULL, x, y); } } CGColorRef glow = CGColorCreateGenericRGB(r, g, b, alpha * 0.65f); CGContextSaveGState(ctx); CGContextSetLineCap(ctx, kCGLineCapRound); CGContextSetLineJoin(ctx, kCGLineJoinRound); if (glow) { CGContextSetShadowWithColor(ctx, CGSizeMake(0, 0), 9.0f, glow); } CGContextAddPath(ctx, path); CGContextSetRGBStrokeColor(ctx, r, g, b, alpha); CGContextSetLineWidth(ctx, lineWidth); CGContextStrokePath(ctx); CGContextRestoreGState(ctx); if (glow) { CGColorRelease(glow); } CGPathRelease(path); } static NSWindow* g_window = nil; static NSWindow* g_dragWindow = nil; static float desktopTopY(void) { NSScreen* mainScreen = [NSScreen mainScreen]; if (!mainScreen) return 0.0f; return mainScreen.frame.origin.y + mainScreen.frame.size.height; } static NSPoint visualOriginFromTopLeft(float x, float y) { return NSMakePoint(x, desktopTopY() - y - CAP_H); } static NSPoint topLeftFromVisualOrigin(NSPoint visualOrigin) { return NSMakePoint(visualOrigin.x, desktopTopY() - visualOrigin.y - CAP_H); } static NSPoint dragOriginForVisualOrigin(NSPoint visualOrigin) { return NSMakePoint( visualOrigin.x + (CAP_W - DRAG_W) * 0.5f, visualOrigin.y + (CAP_H - DRAG_H) * 0.5f ); } static void positionOverlayWindows(NSPoint visualOrigin) { if (g_window) { [g_window setFrameOrigin:visualOrigin]; } if (g_dragWindow) { [g_dragWindow setFrameOrigin:dragOriginForVisualOrigin(visualOrigin)]; } } static void storeDragPositionFromVisualOrigin(NSPoint visualOrigin) { NSPoint topLeft = topLeftFromVisualOrigin(visualOrigin); g_state.dragX = (int)topLeft.x; g_state.dragY = (int)topLeft.y; g_state.posX = g_state.dragX; g_state.posY = g_state.dragY; g_state.dragEnded = 1; } static BOOL rectLooksUsable(NSRect rect) { return isfinite(rect.origin.x) && isfinite(rect.origin.y) && isfinite(rect.size.width) && isfinite(rect.size.height) && rect.size.width > 1.0f && rect.size.height > 1.0f; } static CGFloat rectIntersectionArea(NSRect a, NSRect b) { NSRect intersection = NSIntersectionRect(a, b); if (NSIsEmptyRect(intersection)) return 0.0f; return intersection.size.width * intersection.size.height; } static BOOL rectIntersectsAnyScreen(NSRect rect) { if (!rectLooksUsable(rect)) return NO; for (NSScreen* screen in [NSScreen screens]) { if (rectIntersectionArea(rect, screen.frame) > 0.0f) return YES; } return NO; } static NSRect rectFromTopLeftCGRect(CGRect rect) { return NSMakeRect( rect.origin.x, desktopTopY() - rect.origin.y - rect.size.height, rect.size.width, rect.size.height ); } static NSRect rectFromTopLeftOrRawCGRect(CGRect rect) { NSRect converted = rectFromTopLeftCGRect(rect); if (rectIntersectsAnyScreen(converted)) return converted; NSRect raw = NSMakeRect(rect.origin.x, rect.origin.y, rect.size.width, rect.size.height); if (rectIntersectsAnyScreen(raw)) return raw; return converted; } static BOOL axCopyRectFromElement(AXUIElementRef element, NSRect* outRect) { if (!element || !outRect) return NO; CFTypeRef positionValue = NULL; CFTypeRef sizeValue = NULL; CGPoint position = CGPointZero; CGSize size = CGSizeZero; BOOL ok = NO; if (AXUIElementCopyAttributeValue(element, kAXPositionAttribute, &positionValue) == kAXErrorSuccess && AXUIElementCopyAttributeValue(element, kAXSizeAttribute, &sizeValue) == kAXErrorSuccess && positionValue && sizeValue && CFGetTypeID(positionValue) == AXValueGetTypeID() && CFGetTypeID(sizeValue) == AXValueGetTypeID() && AXValueGetValue((AXValueRef)positionValue, kAXValueCGPointType, &position) && AXValueGetValue((AXValueRef)sizeValue, kAXValueCGSizeType, &size)) { CGRect rect = CGRectMake(position.x, position.y, size.width, size.height); *outRect = rectFromTopLeftOrRawCGRect(rect); ok = rectLooksUsable(*outRect); } if (positionValue) CFRelease(positionValue); if (sizeValue) CFRelease(sizeValue); return ok; } static BOOL axCopyBoundsForSelectedText(AXUIElementRef element, NSRect* outRect) { if (!element || !outRect) return NO; CFTypeRef selectedRange = NULL; CFTypeRef boundsValue = NULL; CGRect bounds = CGRectZero; BOOL ok = NO; if (AXUIElementCopyAttributeValue(element, kAXSelectedTextRangeAttribute, &selectedRange) == kAXErrorSuccess && selectedRange && AXUIElementCopyParameterizedAttributeValue(element, kAXBoundsForRangeParameterizedAttribute, selectedRange, &boundsValue) == kAXErrorSuccess && boundsValue && CFGetTypeID(boundsValue) == AXValueGetTypeID() && AXValueGetValue((AXValueRef)boundsValue, kAXValueCGRectType, &bounds)) { *outRect = rectFromTopLeftOrRawCGRect(bounds); ok = rectLooksUsable(*outRect); } if (selectedRange) CFRelease(selectedRange); if (boundsValue) CFRelease(boundsValue); return ok; } static BOOL copyFocusedInputRect(NSRect* outRect) { if (!outRect) return NO; AXUIElementRef systemWide = AXUIElementCreateSystemWide(); if (!systemWide) return NO; CFTypeRef focusedValue = NULL; BOOL ok = NO; if (AXUIElementCopyAttributeValue(systemWide, kAXFocusedUIElementAttribute, &focusedValue) == kAXErrorSuccess && focusedValue && CFGetTypeID(focusedValue) == AXUIElementGetTypeID()) { AXUIElementRef focused = (AXUIElementRef)focusedValue; CFTypeRef windowValue = NULL; if (AXUIElementCopyAttributeValue(focused, kAXWindowAttribute, &windowValue) == kAXErrorSuccess && windowValue && CFGetTypeID(windowValue) == AXUIElementGetTypeID()) { ok = axCopyRectFromElement((AXUIElementRef)windowValue, outRect); } if (!ok) ok = axCopyBoundsForSelectedText(focused, outRect); if (!ok) ok = axCopyRectFromElement(focused, outRect); if (windowValue) CFRelease(windowValue); } if (focusedValue) CFRelease(focusedValue); CFRelease(systemWide); return ok; } static BOOL copyFrontmostWindowRect(NSRect* outRect) { if (!outRect) return NO; NSRunningApplication* app = [[NSWorkspace sharedWorkspace] frontmostApplication]; if (!app) return NO; pid_t pid = [app processIdentifier]; CFArrayRef windowInfo = CGWindowListCopyWindowInfo( kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements, kCGNullWindowID ); if (!windowInfo) return NO; BOOL ok = NO; NSArray* windows = (NSArray*)windowInfo; for (NSDictionary* info in windows) { NSNumber* ownerPID = [info objectForKey:(id)kCGWindowOwnerPID]; if (!ownerPID || [ownerPID intValue] != pid) continue; NSNumber* layer = [info objectForKey:(id)kCGWindowLayer]; if (layer && [layer intValue] != 0) continue; NSNumber* alpha = [info objectForKey:(id)kCGWindowAlpha]; if (alpha && [alpha floatValue] <= 0.0f) continue; CFDictionaryRef boundsDict = (CFDictionaryRef)[info objectForKey:(id)kCGWindowBounds]; CGRect bounds = CGRectZero; if (!boundsDict || !CGRectMakeWithDictionaryRepresentation(boundsDict, &bounds)) continue; if (bounds.size.width < 80.0f || bounds.size.height < 40.0f) continue; *outRect = rectFromTopLeftOrRawCGRect(bounds); ok = rectLooksUsable(*outRect); if (ok) break; } CFRelease(windowInfo); return ok; } static NSScreen* screenForPoint(NSPoint point) { for (NSScreen* candidate in [NSScreen screens]) { if (NSPointInRect(point, candidate.frame)) return candidate; } return nil; } static NSScreen* screenForRect(NSRect rect) { if (!rectLooksUsable(rect)) return nil; NSScreen* screen = screenForPoint(NSMakePoint(NSMidX(rect), NSMidY(rect))); if (screen) return screen; NSScreen* best = nil; CGFloat bestArea = 0.0f; for (NSScreen* candidate in [NSScreen screens]) { CGFloat area = rectIntersectionArea(rect, candidate.frame); if (!best || area > bestArea) { best = candidate; bestArea = area; } } return best; } static CGFloat clampFloat(CGFloat value, CGFloat minValue, CGFloat maxValue) { if (maxValue < minValue) return minValue; if (value < minValue) return minValue; if (value > maxValue) return maxValue; return value; } static NSPoint overlayOriginForTarget(NSScreen* screen, NSRect targetRect, BOOL hasTarget) { NSRect wa = screen.visibleFrame; CGFloat targetMidX = hasTarget ? NSMidX(targetRect) : NSMidX(wa); CGFloat x = targetMidX - CAP_W * 0.5f; if (CAP_W < wa.size.width) { x = clampFloat(x, wa.origin.x, wa.origin.x + wa.size.width - CAP_W); } else { x = wa.origin.x + (wa.size.width - CAP_W) * 0.5f; } CGFloat y = wa.origin.y + 100.0f; if (hasTarget && targetRect.size.height > CAP_H + 80.0f) { CGFloat inset = fmin(120.0f, fmax(32.0f, targetRect.size.height * 0.18f)); y = targetRect.origin.y + inset; } if (CAP_H < wa.size.height) { y = clampFloat(y, wa.origin.y, wa.origin.y + wa.size.height - CAP_H); } else { y = wa.origin.y + (wa.size.height - CAP_H) * 0.5f; } return NSMakePoint((int)x, (int)y); } // ---- Custom NSView for CG drawing ---- @interface PrivateVoiceOverlayView : NSView @end @implementation PrivateVoiceOverlayView - (void)drawRect:(NSRect)dirtyRect { CGContextRef ctx = [[NSGraphicsContext currentContext] CGContext]; float w = self.bounds.size.width; float h = self.bounds.size.height; CGContextClearRect(ctx, self.bounds); // Measure text float textW = 0; NSString* text = nil; NSDictionary* textAttrs = nil; if (g_state.text[0] != '\0') { text = [NSString stringWithUTF8String:g_state.text]; textAttrs = @{ NSFontAttributeName: [NSFont systemFontOfSize:TXT_SZ weight:NSFontWeightRegular], NSForegroundColorAttributeName: [NSColor colorWithRed:1 green:1 blue:1 alpha:0.82f] }; textW = [text sizeWithAttributes:textAttrs].width; } // Borderless light field: no stroke, no hard capsule edge. float midY = h * 0.5f; float t = g_state.animTime; CGContextSetBlendMode(ctx, kCGBlendModePlusLighter); drawEllipticalGlow(ctx, w * 0.34f, midY - 3, w * 0.34f, h * 0.58f, 0.13f, 0.72f, 1.0f, 0.34f); drawEllipticalGlow(ctx, w * 0.52f, midY + 2, w * 0.31f, h * 0.48f, 0.46f, 0.32f, 1.0f, 0.25f); drawEllipticalGlow(ctx, w * 0.70f, midY - 2, w * 0.30f, h * 0.56f, 1.0f, 0.30f, 0.80f, 0.30f); drawEllipticalGlow(ctx, w * 0.50f, midY, w * 0.52f, h * 0.32f, 1.0f, 1.0f, 1.0f, 0.09f); float barsW = N_BARS * BAR_W + (N_BARS - 1) * BAR_GAP; float totalW = barsW; if (textW > 0) totalW += TXT_GAP + textW; float startX = (w - totalW) / 2; if (startX < 86) startX = 86; float barsEnd = startX + barsW; float textX = barsEnd + TXT_GAP; float waveStart = 54; float waveEnd = textW > 0 ? textX + textW + 34 : w - 54; if (waveEnd > w - 36) waveEnd = w - 36; // Fine horizontal light waves flowing through the transparent field. drawWavePath(ctx, waveStart, waveEnd, midY - 8.0f, 12.0f, t * 1.15f, 2.45f, 0.67f, 0.91f, 1.00f, 0.45f, 1.05f); drawWavePath(ctx, waveStart + 18, waveEnd - 8, midY + 5.0f, 9.0f, -t * 1.00f + 1.9f, 2.05f, 1.00f, 0.80f, 0.98f, 0.38f, 0.95f); drawWavePath(ctx, waveStart + 10, waveEnd - 24, midY + 14.0f, 6.5f, t * 0.82f + 3.1f, 2.75f, 0.92f, 1.00f, 1.00f, 0.24f, 0.75f); drawWavePath(ctx, waveStart + 32, waveEnd - 28, midY - 19.0f, 7.5f, -t * 0.72f + 0.7f, 2.00f, 0.92f, 0.76f, 1.00f, 0.22f, 0.70f); // Draw bars static const float profiles[N_BARS] = {0.20f, 0.34f, 0.58f, 0.83f, 1.12f, 1.34f, 1.05f, 0.82f, 0.60f, 0.40f, 0.24f}; for (int i = 0; i < N_BARS; i++) { float bx = startX + i * (BAR_W + BAR_GAP); float bh = g_state.barHeights[i] * profiles[i]; if (bh < 7) bh = 7; if (bh > 66) bh = 66; float by = (h - bh) / 2; CGRect barRect = CGRectMake(bx, by, BAR_W, bh); CGPathRef barPath = CGPathCreateWithRoundedRect(barRect, BAR_R, BAR_R, NULL); if (!barPath) continue; float p = (float)i / (float)(N_BARS - 1); float r, g, b; spectralColor(p, &r, &g, &b); applyStatusTint(&r, &g, &b, g_state.status == 3 || g_state.status == 4 ? 0.06f : 0.16f); CGColorRef glow = CGColorCreateGenericRGB(r, g, b, 0.82f); CGContextSaveGState(ctx); if (glow) { CGContextSetShadowWithColor(ctx, CGSizeMake(0, 0), 14.0f, glow); } CGContextAddPath(ctx, barPath); CGContextSetRGBFillColor(ctx, r, g, b, 0.88f); CGContextFillPath(ctx); CGContextRestoreGState(ctx); CGContextAddPath(ctx, barPath); CGContextSetRGBStrokeColor(ctx, 1, 1, 1, 0.52f); CGContextSetLineWidth(ctx, 1.15f); CGContextStrokePath(ctx); if (glow) { CGColorRelease(glow); } CGPathRelease(barPath); } // Small luminous beads echo the reference artwork without creating a border. for (int i = 0; i < 7; i++) { float p = (float)i / 6.0f; float cx = mixf(startX - 46, barsEnd + 46, p); float phase = t * 2.1f + p * 8.0f; float radius = 2.0f + (sinf(phase) * 0.5f + 0.5f) * 2.0f; float r, g, b; spectralColor(p, &r, &g, &b); applyStatusTint(&r, &g, &b, g_state.status == 3 || g_state.status == 4 ? 0.06f : 0.16f); CGColorRef glow = CGColorCreateGenericRGB(r, g, b, 0.68f); CGContextSaveGState(ctx); if (glow) { CGContextSetShadowWithColor(ctx, CGSizeMake(0, 0), 8.0f, glow); } CGContextSetRGBFillColor(ctx, r, g, b, 0.72f); CGContextFillEllipseInRect(ctx, CGRectMake(cx - radius, midY - radius, radius * 2, radius * 2)); CGContextRestoreGState(ctx); if (glow) { CGColorRelease(glow); } } // Draw text if (text && textAttrs) { NSSize sz = [text sizeWithAttributes:textAttrs]; float ty = (h - sz.height) / 2; float tr = 0.63f, tg = 0.88f, tb = 1.0f; applyStatusTint(&tr, &tg, &tb, g_state.status == 3 || g_state.status == 4 ? 0.06f : 0.16f); CGColorRef textGlow = CGColorCreateGenericRGB(tr, tg, tb, 0.58f); CGContextSaveGState(ctx); if (textGlow) { CGContextSetShadowWithColor(ctx, CGSizeMake(0, 0), 10.0f, textGlow); } [text drawAtPoint:NSMakePoint(textX, ty) withAttributes:textAttrs]; CGContextRestoreGState(ctx); if (textGlow) { CGColorRelease(textGlow); } } CGContextSetBlendMode(ctx, kCGBlendModeNormal); } @end @interface PrivateVoiceDragView : NSView { NSPoint _dragStartMouse; NSPoint _dragStartOrigin; BOOL _draggingWindow; } @end @implementation PrivateVoiceDragView - (BOOL)acceptsFirstMouse:(NSEvent*)event { return YES; } - (void)mouseDown:(NSEvent*)event { _dragStartMouse = [NSEvent mouseLocation]; _dragStartOrigin = g_window ? g_window.frame.origin : self.window.frame.origin; _draggingWindow = YES; } - (void)mouseDragged:(NSEvent*)event { if (!_draggingWindow || !self.window) return; NSPoint currentMouse = [NSEvent mouseLocation]; NSPoint nextOrigin = NSMakePoint( _dragStartOrigin.x + currentMouse.x - _dragStartMouse.x, _dragStartOrigin.y + currentMouse.y - _dragStartMouse.y ); positionOverlayWindows(nextOrigin); } - (void)mouseUp:(NSEvent*)event { if (_draggingWindow && g_window) { storeDragPositionFromVisualOrigin(g_window.frame.origin); } _draggingWindow = NO; } @end // ---- Custom NSPanel for non-activating overlay windows ---- @interface PrivateVoiceOverlayWindow : NSPanel @end @implementation PrivateVoiceOverlayWindow - (BOOL)canBecomeKeyWindow { return NO; } - (BOOL)canBecomeMainWindow { return NO; } @end // ---- Timer target ---- static PrivateVoiceOverlayView* g_view = nil; static NSTimer* g_timer = nil; static void applyOverlayWindowPolicy(NSWindow* window) { if (!window) return; [window setLevel:CGWindowLevelForKey(kCGStatusWindowLevelKey)]; [window setCollectionBehavior: NSWindowCollectionBehaviorCanJoinAllSpaces | NSWindowCollectionBehaviorStationary | NSWindowCollectionBehaviorFullScreenAuxiliary | NSWindowCollectionBehaviorTransient | NSWindowCollectionBehaviorIgnoresCycle]; [window setCanHide:NO]; if ([window isKindOfClass:[NSPanel class]]) { [(NSPanel*)window setHidesOnDeactivate:NO]; } } static void animateBars(void) { g_state.animTime += 0.033f; float t = g_state.animTime; switch (g_state.status) { case 1: // loading for (int i = 0; i < N_BARS; i++) { float pulse = sinf(t * 2 + i * 0.3f) * 0.5f + 0.5f; g_state.barHeights[i] = 10 + pulse * 20; } break; case 2: // ready case 6: // done for (int i = 0; i < N_BARS; i++) g_state.barHeights[i] = g_state.barHeights[i] * 0.85f + 14 * 0.15f; break; case 3: // recording case 4: { // freetalking int hasReal = 0; for (int i = 0; i < N_BARS; i++) if (g_state.volumes[i] > 0.01f) { hasReal = 1; break; } for (int i = 0; i < N_BARS; i++) { float target; if (hasReal) { target = 8 + g_state.volumes[i] * 32; } else { float wave = sinf(t * 4 + i * 0.8f) * 0.5f + 0.5f; float rnd = sinf(t * 7 + i * 1.5f) * 0.3f; target = 10 + (wave + rnd) * 25; } g_state.barHeights[i] = g_state.barHeights[i] * 0.6f + target * 0.4f; } break; } case 5: // processing for (int i = 0; i < N_BARS; i++) { float wave = sinf(t * 3 + i * 0.6f) * 0.5f + 0.5f; g_state.barHeights[i] = 12 + wave * 18; } break; default: for (int i = 0; i < N_BARS; i++) g_state.barHeights[i] = g_state.barHeights[i] * 0.85f + 14 * 0.15f; break; } } @interface OverlayTimerTarget : NSObject - (void)tick:(NSTimer*)timer; @end @implementation OverlayTimerTarget - (void)tick:(NSTimer*)timer { if (!g_window) return; // Fade if (g_state.fadeProgress < g_state.fadeTarget) { g_state.fadeProgress += 0.16f; if (g_state.fadeProgress > 1.0f) g_state.fadeProgress = 1.0f; } else if (g_state.fadeProgress > g_state.fadeTarget) { g_state.fadeProgress -= 0.22f; if (g_state.fadeProgress < 0) g_state.fadeProgress = 0; } float eased = g_state.fadeProgress * (2 - g_state.fadeProgress); [g_window setAlphaValue:eased]; if (g_state.fadeProgress <= 0 && g_state.fadeTarget == 0) { [g_window orderOut:nil]; if (g_dragWindow) [g_dragWindow orderOut:nil]; return; } if (g_state.fadeTarget > 0) { if (![g_window isVisible]) { applyOverlayWindowPolicy(g_window); [g_window orderFrontRegardless]; } if (g_dragWindow && ![g_dragWindow isVisible]) { applyOverlayWindowPolicy(g_dragWindow); [g_dragWindow orderFrontRegardless]; } } // Reposition if (g_state.needsPosition) { g_state.needsPosition = 0; positionOverlayWindows(visualOriginFromTopLeft(g_state.posX, g_state.posY)); } animateBars(); [g_view setNeedsDisplay:YES]; } @end static OverlayTimerTarget* g_timerTarget = nil; // ---- C API for Go ---- static void doCreateOverlay(void* ctx) { (void)ctx; PrivateVoiceOverlayWindow* window = [[PrivateVoiceOverlayWindow alloc] initWithContentRect:NSMakeRect(0, 0, CAP_W, CAP_H) styleMask:NSWindowStyleMaskBorderless | NSWindowStyleMaskNonactivatingPanel backing:NSBackingStoreBuffered defer:NO]; [window setOpaque:NO]; [window setBackgroundColor:[NSColor clearColor]]; applyOverlayWindowPolicy(window); [window setIgnoresMouseEvents:YES]; [window setMovableByWindowBackground:NO]; [window setHasShadow:NO]; [window setAlphaValue:0]; PrivateVoiceOverlayView* view = [[PrivateVoiceOverlayView alloc] initWithFrame:NSMakeRect(0, 0, CAP_W, CAP_H)]; [window setContentView:view]; g_window = window; g_view = view; PrivateVoiceOverlayWindow* dragWindow = [[PrivateVoiceOverlayWindow alloc] initWithContentRect:NSMakeRect(0, 0, DRAG_W, DRAG_H) styleMask:NSWindowStyleMaskBorderless | NSWindowStyleMaskNonactivatingPanel backing:NSBackingStoreBuffered defer:NO]; [dragWindow setOpaque:NO]; [dragWindow setBackgroundColor:[NSColor clearColor]]; applyOverlayWindowPolicy(dragWindow); [dragWindow setIgnoresMouseEvents:NO]; [dragWindow setMovableByWindowBackground:NO]; [dragWindow setHasShadow:NO]; [dragWindow setAlphaValue:1.0]; PrivateVoiceDragView* dragView = [[PrivateVoiceDragView alloc] initWithFrame:NSMakeRect(0, 0, DRAG_W, DRAG_H)]; [dragWindow setContentView:dragView]; g_dragWindow = dragWindow; g_timerTarget = [[OverlayTimerTarget alloc] init]; g_timer = [NSTimer timerWithTimeInterval:0.033 target:g_timerTarget selector:@selector(tick:) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:g_timer forMode:NSRunLoopCommonModes]; } static void overlayCreate(void) { dispatch_async_f(dispatch_get_main_queue(), NULL, doCreateOverlay); } static void doAutoPosition(void* ctx) { (void)ctx; if (!g_window) return; NSRect targetRect = NSZeroRect; BOOL hasTarget = copyFocusedInputRect(&targetRect); if (!hasTarget) hasTarget = copyFrontmostWindowRect(&targetRect); NSScreen* screen = hasTarget ? screenForRect(targetRect) : nil; if (!screen) { screen = screenForPoint([NSEvent mouseLocation]); } if (!screen) screen = [NSScreen mainScreen]; if (!screen) return; NSPoint origin = overlayOriginForTarget(screen, targetRect, hasTarget); positionOverlayWindows(origin); NSPoint topLeft = topLeftFromVisualOrigin(origin); g_state.posX = (int)topLeft.x; g_state.posY = (int)topLeft.y; g_state.hasPosition = 1; } static void doBringOverlayForward(void* ctx) { (void)ctx; if (!g_window) return; applyOverlayWindowPolicy(g_window); [g_window orderFrontRegardless]; if (g_dragWindow) { applyOverlayWindowPolicy(g_dragWindow); [g_dragWindow orderFrontRegardless]; } } static void overlayShow(void) { g_state.fadeTarget = 1.0f; if (!g_state.hasPosition) { dispatch_async_f(dispatch_get_main_queue(), NULL, doAutoPosition); } dispatch_async_f(dispatch_get_main_queue(), NULL, doBringOverlayForward); } static void overlayAutoPosition(void) { g_state.hasPosition = 0; dispatch_async_f(dispatch_get_main_queue(), NULL, doAutoPosition); } static void doHideDragOverlay(void* ctx) { (void)ctx; if (g_state.fadeTarget == 0.0f && g_dragWindow) { [g_dragWindow orderOut:nil]; } } static void overlayHide(void) { g_state.fadeTarget = 0.0f; dispatch_async_f(dispatch_get_main_queue(), NULL, doHideDragOverlay); } static void overlaySetStatus(int status, const char* text, float r, float g, float b) { g_state.status = status; g_state.barR = r; g_state.barG = g; g_state.barB = b; if (text) { strncpy(g_state.text, text, sizeof(g_state.text) - 1); g_state.text[sizeof(g_state.text) - 1] = '\0'; } else { g_state.text[0] = '\0'; } } static void overlaySetVolume(float vol) { for (int i = 0; i < N_BARS - 1; i++) g_state.volumes[i] = g_state.volumes[i + 1]; g_state.volumes[N_BARS - 1] = vol; } static void overlaySetPosition(int x, int y) { g_state.posX = x; g_state.posY = y; g_state.hasPosition = 1; g_state.needsPosition = 1; } static void overlayGetPosition(int* x, int* y) { *x = g_state.posX; *y = g_state.posY; } static int overlayCheckDrag(int* x, int* y) { if (g_state.dragEnded) { g_state.dragEnded = 0; *x = g_state.dragX; *y = g_state.dragY; return 1; } return 0; } static void doCloseOverlay(void* ctx) { (void)ctx; if (g_timer) { [g_timer invalidate]; g_timer = nil; } if (g_window) { [g_window close]; g_window = nil; } if (g_dragWindow) { [g_dragWindow close]; g_dragWindow = nil; } g_view = nil; g_timerTarget = nil; } static void overlayClose(void) { dispatch_async_f(dispatch_get_main_queue(), NULL, doCloseOverlay); } */ import "C" import ( "time" "unsafe" ) const ( darwinOverlayWidth = 620 darwinOverlayHeight = 112 ) // darwinOverlay uses a native NSWindow + Core Graphics to render // the floating indicator, matching the Windows GDI+ implementation. type darwinOverlay struct { dragCb func(int, int) created bool done chan struct{} } func New() Overlay { return &darwinOverlay{ done: make(chan struct{}), } } func (o *darwinOverlay) ensureCreated() { if !o.created { C.overlayCreate() o.created = true time.Sleep(50 * time.Millisecond) go o.pollDrag() } } func (o *darwinOverlay) Show() { o.ensureCreated() C.overlayShow() } func (o *darwinOverlay) Hide() { C.overlayHide() } func (o *darwinOverlay) SetStatus(status Status, text string) { r, g, b := statusColorRGB(status) cText := C.CString(text) defer C.free(unsafe.Pointer(cText)) C.overlaySetStatus(statusToInt(status), cText, C.float(r), C.float(g), C.float(b)) } func (o *darwinOverlay) SetVolume(vol float64) { C.overlaySetVolume(C.float(vol)) } func (o *darwinOverlay) SetPosition(x, y int) { C.overlaySetPosition(C.int(x), C.int(y)) } func (o *darwinOverlay) AutoPosition() { o.ensureCreated() C.overlayAutoPosition() } func (o *darwinOverlay) GetPosition() (int, int) { var x, y C.int C.overlayGetPosition(&x, &y) return int(x), int(y) } func (o *darwinOverlay) Size() (int, int) { return darwinOverlayWidth, darwinOverlayHeight } func (o *darwinOverlay) OnDragged(fn func(int, int)) { o.dragCb = fn } func (o *darwinOverlay) Close() { select { case <-o.done: default: close(o.done) } C.overlayClose() } func (o *darwinOverlay) pollDrag() { ticker := time.NewTicker(100 * time.Millisecond) defer ticker.Stop() for { select { case <-o.done: return case <-ticker.C: var x, y C.int if C.overlayCheckDrag(&x, &y) != 0 { if o.dragCb != nil { o.dragCb(int(x), int(y)) } } } } } func statusToInt(s Status) C.int { switch s { case StatusLoading: return 1 case StatusReady: return 2 case StatusRecording: return 3 case StatusFreetalking: return 4 case StatusProcessing: return 5 case StatusDone: return 6 case StatusCancelled: return 7 case StatusNoVoice: return 8 case StatusNoContent: return 9 case StatusError: return 10 default: return 0 } } func statusColorRGB(s Status) (float32, float32, float32) { switch s { case StatusLoading, StatusFreetalking: return 0, 0.478, 1.0 // #007AFF case StatusReady, StatusDone: return 0.204, 0.780, 0.349 // #34C759 case StatusRecording, StatusError: return 1.0, 0.231, 0.188 // #FF3B30 case StatusProcessing, StatusCancelled, StatusNoVoice, StatusNoContent: return 1.0, 0.584, 0.0 // #FF9500 default: return 0, 0.478, 1.0 } }