import Foundation import CoreGraphics /// Wo die schwebende Videokachel sitzt. /// /// Eigener Typ statt Rechnungen in der Ansicht, aus demselben Grund wie bei /// `GuideLayout`: hier geht lautlos schief, was man nicht sieht. Eine Kachel, /// die halb aus dem Bild ragt, oder eine, die beim Loslassen in die falsche /// Ecke springt, fällt erst auf dem Gerät auf — und dann als Gefühl, nicht /// als Fehler. public struct MiniPlayerPlacement: Sendable, Equatable { public enum Corner: String, Sendable, CaseIterable, Codable { case topLeading, topTrailing, bottomLeading, bottomTrailing public var isTop: Bool { self == .topLeading || self == .topTrailing } public var isLeading: Bool { self == .topLeading || self == .bottomLeading } } /// Der sichtbare Bereich, in dem die Kachel sich bewegen darf. public let bounds: CGSize public let tile: CGSize /// Abstand zum Rand. Eine Kachel, die den Rand berührt, sieht aus wie ein /// Fehler statt wie eine Entscheidung. public let inset: CGFloat public init(bounds: CGSize, tile: CGSize, inset: CGFloat = FlixySpacing.regular) { self.bounds = bounds self.tile = tile self.inset = inset } /// Die Ecke, in die eine losgelassene Kachel gehört. /// /// Entschieden wird nach dem **Mittelpunkt** der Kachel, nicht nach dem /// Finger: sonst springt eine Kachel, die man an der oberen Kante /// angefasst hat, in die falsche Hälfte. public func nearestCorner(toCenter center: CGPoint) -> Corner { let isLeading = center.x < bounds.width / 2 let isTop = center.y < bounds.height / 2 switch (isTop, isLeading) { case (true, true): return .topLeading case (true, false): return .topTrailing case (false, true): return .bottomLeading case (false, false): return .bottomTrailing } } /// Die linke obere Ecke der Kachel für eine Ecke des Bildschirms. public func origin(for corner: Corner) -> CGPoint { CGPoint( x: corner.isLeading ? inset : max(inset, bounds.width - tile.width - inset), y: corner.isTop ? inset : max(inset, bounds.height - tile.height - inset) ) } /// Der Mittelpunkt der Kachel in einer Ecke — damit rechnet SwiftUI. public func center(for corner: Corner) -> CGPoint { let start = origin(for: corner) return CGPoint(x: start.x + tile.width / 2, y: start.y + tile.height / 2) } /// Hält einen Mittelpunkt im sichtbaren Bereich. /// /// Während des Ziehens, damit die Kachel nicht hinter den Rand rutscht und /// dort unerreichbar wird — der Finger wäre dann schneller als das Ziel. public func clamped(center: CGPoint) -> CGPoint { let halfWidth = tile.width / 2 let halfHeight = tile.height / 2 let minX = inset + halfWidth let maxX = bounds.width - inset - halfWidth let minY = inset + halfHeight let maxY = bounds.height - inset - halfHeight return CGPoint( // `max(min…)` in dieser Reihenfolge, weil bei einem Bereich, der // schmaler ist als die Kachel, sonst der untere Anschlag verliert // und die Kachel rechts hinausragt. x: max(minX, min(maxX, center.x)), y: max(minY, min(maxY, center.y)) ) } }