import Foundation import CoreGraphics /// Wo die schwebende Videokachel sitzt. /// /// Sie klebt an der linken oder rechten Seite und ist dort **frei in der /// Höhe** — nicht auf vier Ecken festgelegt. Vier Ecken sind schnell gebaut, /// aber im Weg: steht die Kachel unten rechts über einem Knopf, bleibt nur /// die Wahl zwischen vier Orten, von denen drei ebenso stören. /// /// 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, fällt erst auf dem Gerät auf — und dann als /// Gefühl, nicht als Fehler. public struct MiniPlayerPlacement: Sendable, Equatable { public enum Side: String, Sendable, CaseIterable, Codable { case leading, trailing } /// 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 = FlaroSpacing.regular) { self.bounds = bounds self.tile = tile self.inset = inset } private var halfWidth: CGFloat { tile.width / 2 } private var halfHeight: CGFloat { tile.height / 2 } private var minimumX: CGFloat { inset + halfWidth } private var maximumX: CGFloat { max(minimumX, bounds.width - inset - halfWidth) } private var minimumY: CGFloat { inset + halfHeight } private var maximumY: CGFloat { max(minimumY, bounds.height - inset - halfHeight) } /// An welche Seite eine losgelassene Kachel gehört. /// /// Entschieden wird nach dem **Mittelpunkt** der Kachel, nicht nach dem /// Finger: sonst springt eine Kachel, die man am Rand angefasst hat, auf /// die falsche Seite. public func nearestSide(toCenter center: CGPoint) -> Side { center.x < bounds.width / 2 ? .leading : .trailing } /// Wie weit unten die Kachel sitzt: 0 ganz oben, 1 ganz unten. /// /// Als Anteil statt als Punktwert, damit die Kachel beim Drehen des Geräts /// an derselben *relativen* Stelle bleibt, statt unten herauszufallen. public func verticalFraction(forCenter center: CGPoint) -> Double { let travel = maximumY - minimumY guard travel > 0 else { return 0 } return min(max(Double((center.y - minimumY) / travel), 0), 1) } public func center(side: Side, verticalFraction: Double) -> CGPoint { let fraction = min(max(verticalFraction, 0), 1) return CGPoint( x: side == .leading ? minimumX : maximumX, y: minimumY + (maximumY - minimumY) * CGFloat(fraction) ) } /// 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 { 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(minimumX, min(maximumX, center.x)), y: max(minimumY, min(maximumY, center.y)) ) } }