Swiftfin/Swiftfin tvOS/Objects/FocusGuide.swift
Ethan Pippin b038f05848
Update 2024 headers (#951)
* update header

* the rest after upgrading
2024-01-13 15:13:44 -07:00

150 lines
4.2 KiB
Swift

//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
//
import SwiftUI
struct FocusGuideModifier: ViewModifier {
@FocusState
var focusDirection: FocusDirection?
@EnvironmentObject
var focusGuide: FocusGuide
let focusConstructor: FocusConstructor
let onContentFocus: (() -> Void)?
let debug = false
func body(content: Content) -> some View {
VStack(spacing: 0) {
Color(debug ? .red : .clear)
.frame(height: 1)
.if(focusConstructor.topTarget != nil, transform: { boundary in
boundary.focusable()
})
.focused($focusDirection, equals: .top)
HStack(spacing: 0) {
Color(debug ? .red : .clear)
.frame(width: 1)
.if(focusConstructor.leftTarget != nil, transform: { boundary in
boundary.focusable()
})
.focused($focusDirection, equals: .left)
content
.focused($focusDirection, equals: .content)
Color(debug ? .red : .clear)
.frame(width: 1)
.if(focusConstructor.rightTarget != nil, transform: { boundary in
boundary.focusable()
})
.focused($focusDirection, equals: .right)
}
Color(debug ? .red : .clear)
.frame(height: 1)
.if(focusConstructor.bottomTarget != nil, transform: { boundary in
boundary.focusable()
})
.focused($focusDirection, equals: .bottom)
}
.onChange(of: focusDirection) { focusDirection in
guard let focusDirection = focusDirection else { return }
switch focusDirection {
case .top:
focusGuide.transition(to: focusConstructor.topTarget!)
case .bottom:
focusGuide.transition(to: focusConstructor.bottomTarget!)
case .left:
focusGuide.transition(to: focusConstructor.leftTarget!)
case .right:
focusGuide.transition(to: focusConstructor.rightTarget!)
case .content: ()
}
}
.onChange(of: focusGuide.focusedTag) { newTag in
if newTag == focusConstructor.tag {
if let onContentFocus = onContentFocus {
onContentFocus()
} else {
focusDirection = .content
}
}
}
}
}
extension View {
func focusGuide(
_ focusGuide: FocusGuide,
tag: String,
onContentFocus: (() -> Void)? = nil,
top: String? = nil,
bottom: String? = nil,
left: String? = nil,
right: String? = nil
) -> some View {
let focusConstructor = FocusConstructor(
tag: tag,
topTarget: top,
bottomTarget: bottom,
leftTarget: left,
rightTarget: right
)
return modifier(FocusGuideModifier(focusConstructor: focusConstructor, onContentFocus: onContentFocus))
.environmentObject(focusGuide)
}
}
enum FocusDirection: String {
case top
case bottom
case content
case left
case right
}
struct FocusConstructor {
let tag: String
let topTarget: String?
let bottomTarget: String?
let leftTarget: String?
let rightTarget: String?
init(
tag: String,
topTarget: String?,
bottomTarget: String?,
leftTarget: String?,
rightTarget: String?
) {
self.tag = tag
self.topTarget = topTarget
self.bottomTarget = bottomTarget
self.leftTarget = leftTarget
self.rightTarget = rightTarget
}
}
class FocusGuide: ObservableObject {
@Published
private(set) var focusedTag: String?
private(set) var lastFocusedTag: String?
func transition(to tag: String?) {
lastFocusedTag = focusedTag
focusedTag = tag
}
}