Files
brov-macbook/NotchBuddy/Sources/App/DesktopMochiLogic.swift
T
2026-10-06 19:34:32 +03:00

74 lines
3.1 KiB
Swift

import Foundation
import CoreGraphics
// MARK: - Alert state machine phase
/// Phase of the desktop Mochi lifecycle.
enum DesktopPhase: Equatable {
/// No panel on screen; `UserDefaults["mochiOnDesktop"]` is `false`.
case home
/// Panel animating from notch to saved desktop position.
case flyingOut
/// Panel live on the desktop — normal operating state.
case onDesktop
/// `pendingApproval`/`pendingQuestion` just went non-nil; panel animating toward notch.
case retracting
/// Alert cleared while retract animation was still running.
case alertResolvedDuringRetract
/// Retract complete; notch Mochi is showing the alert.
case atNotchForAlert
}
// MARK: - Pure geometry / logic (no AppKit — fully unit-testable)
/// Stateless helpers for `DesktopMochiController`.
enum DesktopMochiLogic {
static let panelSize: CGFloat = 120
static let sleepTimeout: TimeInterval = 120
static let sleepMouseDistance: CGFloat = 150
static let clampMargin: CGFloat = 24
static let bodyRadiusFraction: CGFloat = 0.24
/// Whether Mochi should enter sleeping state.
static func shouldSleep(lastAgentActiveInterval: TimeInterval,
mouseDistanceToPanelCenter: CGFloat) -> Bool {
lastAgentActiveInterval > sleepTimeout && mouseDistanceToPanelCenter >= sleepMouseDistance
}
/// Hit-test the circular body inside a square panel (AppKit y-up local coords).
static func isOverBody(localPoint: CGPoint, panelSize: CGFloat) -> Bool {
let cx = panelSize / 2
let cy = panelSize / 2
let r = panelSize * bodyRadiusFraction
let dx = localPoint.x - cx
let dy = localPoint.y - cy
return dx * dx + dy * dy <= r * r
}
/// Eye-tracking origin: panel center in screen-space with y-down from top of screen.
/// Matches the coordinate space of `AppState.mousePosition`.
static func lookOrigin(panelMinX: CGFloat,
panelMinY: CGFloat,
screenMinX: CGFloat,
screenHeight: CGFloat,
panelSize: CGFloat) -> CGPoint {
let cx = panelMinX + panelSize / 2
let cy = panelMinY + panelSize / 2
return CGPoint(x: cx - screenMinX, y: screenHeight - cy)
}
/// Whether Mochi should immediately retract after landing (alert was active during the flight).
static func shouldRetractOnLanding(alertActive: Bool) -> Bool { alertActive }
/// Clamp a panel origin so the panel stays inside `visibleFrame` with `margin` on each side.
static func clampOrigin(_ origin: CGPoint,
panelSize: CGFloat,
visibleFrame: CGRect,
margin: CGFloat) -> CGPoint {
CGPoint(
x: min(max(origin.x, visibleFrame.minX + margin), visibleFrame.maxX - panelSize - margin),
y: min(max(origin.y, visibleFrame.minY + margin), visibleFrame.maxY - panelSize - margin)
)
}
}