- feat: global controller navigation
- shorten initial host detection
This commit is contained in:
1 parent
6460036661
commit
9a634da3ed
66 files changed
+6572
-1659
No files matched your search
@@ -2,7 +2,7 @@
|
||||
// RadialMenuOverlayView.swift
|
||||
// VoidLink
|
||||
//
|
||||
// Created by Codex on 2026/7/1.
|
||||
// Created by True砖家 on 2026/7/1.
|
||||
// Copyright © 2026 True砖家 on Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
@@ -11,60 +11,91 @@ import UIKit
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
final class RadialMenuOverlayView: UIView {
|
||||
func getNavigationElements() -> [ControllerNavigationElement] {
|
||||
var elements: [ControllerNavigationElement] = []
|
||||
elements.append(ControllerNavigationElement(control: ControllerNavigator.radialMenuButtonPosition == .right ? .leftStick : .rightStick, action: "focusNavigation"))
|
||||
return elements
|
||||
}
|
||||
|
||||
private enum Metrics {
|
||||
static let diameter: CGFloat = 260
|
||||
static let bottomInset: CGFloat = 92
|
||||
static let diameter: CGFloat = PublicUtils.isIPhone ? 200 : 260
|
||||
static let releaseDistanceEpsilon: CGFloat = 0.015
|
||||
}
|
||||
|
||||
private let selectionState = RadialMenuSelectionState()
|
||||
private var hostingController: UIHostingController<RadialMenuView>?
|
||||
private var themeObserver: NSObjectProtocol?
|
||||
private var previousJoystickDistance: CGFloat?
|
||||
private var isJoystickReturningToCenter = false
|
||||
private var releaseLatchedSelectedIndex: Int?
|
||||
|
||||
static var menuSectors: [RadialMenuSector] = []
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
setupView()
|
||||
observeThemeChanges()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setupView()
|
||||
observeThemeChanges()
|
||||
}
|
||||
|
||||
deinit {
|
||||
removeThemeObserver()
|
||||
}
|
||||
|
||||
static func presentInKeyWindow() -> RadialMenuOverlayView? {
|
||||
guard let window = keyWindow() else { return nil }
|
||||
|
||||
let overlayView = RadialMenuOverlayView()
|
||||
overlayView.translatesAutoresizingMaskIntoConstraints = false
|
||||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
window.addSubview(overlayView)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
overlayView.widthAnchor.constraint(equalToConstant: Metrics.diameter),
|
||||
overlayView.heightAnchor.constraint(equalToConstant: Metrics.diameter),
|
||||
overlayView.centerXAnchor.constraint(equalTo: window.centerXAnchor),
|
||||
overlayView.bottomAnchor.constraint(equalTo: window.safeAreaLayoutGuide.bottomAnchor, constant: -Metrics.bottomInset)
|
||||
overlayView.centerYAnchor.constraint(equalTo: window.centerYAnchor)
|
||||
])
|
||||
|
||||
CATransaction.commit()
|
||||
return overlayView
|
||||
}
|
||||
|
||||
func updateSelection(xOffset: Float, yOffset: Float) -> RadialMenuItem? {
|
||||
func updateSelection(xOffset: CGFloat, yOffset: CGFloat) -> RadialMenuItem? {
|
||||
guard Thread.isMainThread else {
|
||||
return DispatchQueue.main.sync {
|
||||
self.updateSelection(xOffset: xOffset, yOffset: yOffset)
|
||||
}
|
||||
}
|
||||
|
||||
let previousSelectedIndex = selectionState.selectedIndex
|
||||
latchSelectionIfNeeded(
|
||||
distance: Self.normalizedDistance(xOffset: xOffset, yOffset: yOffset),
|
||||
selectedIndex: previousSelectedIndex
|
||||
)
|
||||
|
||||
selectionState.updateSelection(xOffset: xOffset, yOffset: yOffset)
|
||||
|
||||
guard previousSelectedIndex != nil, selectionState.selectedIndex == nil else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let previousSelectedIndex,
|
||||
Self.menuSectors.indices.contains(previousSelectedIndex) else {
|
||||
let actionSelectedIndex = releaseLatchedSelectedIndex ?? previousSelectedIndex
|
||||
resetReleaseLatch()
|
||||
|
||||
guard let actionSelectedIndex,
|
||||
Self.menuSectors.indices.contains(actionSelectedIndex) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return Self.menuSectors[previousSelectedIndex].item
|
||||
return Self.menuSectors[actionSelectedIndex].item
|
||||
}
|
||||
|
||||
func dismiss() {
|
||||
removeThemeObserver()
|
||||
hostingController?.willMove(toParent: nil)
|
||||
hostingController?.view.removeFromSuperview()
|
||||
hostingController?.removeFromParent()
|
||||
@@ -77,11 +108,7 @@ final class RadialMenuOverlayView: UIView {
|
||||
isUserInteractionEnabled = false
|
||||
clipsToBounds = false
|
||||
|
||||
let radialMenuView = RadialMenuView(
|
||||
sectors: Self.menuSectors,
|
||||
isTouchSelectionEnabled: false,
|
||||
selectionState: selectionState
|
||||
)
|
||||
let radialMenuView = makeRadialMenuView()
|
||||
|
||||
let hostingController = UIHostingController(rootView: radialMenuView)
|
||||
hostingController.view.backgroundColor = .clear
|
||||
@@ -97,9 +124,80 @@ final class RadialMenuOverlayView: UIView {
|
||||
hostingController.view.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
hostingController.view.trailingAnchor.constraint(equalTo: trailingAnchor)
|
||||
])
|
||||
|
||||
GamepadNavigationIllustrationHud.updateNavigationElements(self.getNavigationElements())
|
||||
}
|
||||
|
||||
private func observeThemeChanges() {
|
||||
themeObserver = NotificationCenter.default.addObserver(
|
||||
forName: Notification.Name(ThemeManager.ThemeDidChangeNotification),
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { [weak self] _ in
|
||||
self?.refreshTheme()
|
||||
}
|
||||
}
|
||||
|
||||
private func removeThemeObserver() {
|
||||
if let themeObserver {
|
||||
NotificationCenter.default.removeObserver(themeObserver)
|
||||
self.themeObserver = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshTheme() {
|
||||
hostingController?.rootView = makeRadialMenuView()
|
||||
}
|
||||
|
||||
private func latchSelectionIfNeeded(distance: CGFloat, selectedIndex: Int?) {
|
||||
defer { previousJoystickDistance = distance }
|
||||
|
||||
guard let previousJoystickDistance else {
|
||||
return
|
||||
}
|
||||
|
||||
if distance + Metrics.releaseDistanceEpsilon < previousJoystickDistance {
|
||||
if !isJoystickReturningToCenter {
|
||||
releaseLatchedSelectedIndex = selectedIndex
|
||||
isJoystickReturningToCenter = true
|
||||
}
|
||||
} else if distance > previousJoystickDistance + Metrics.releaseDistanceEpsilon {
|
||||
resetReleaseLatch(keepingPreviousDistance: true)
|
||||
}
|
||||
}
|
||||
|
||||
private func resetReleaseLatch(keepingPreviousDistance: Bool = false) {
|
||||
if !keepingPreviousDistance {
|
||||
previousJoystickDistance = nil
|
||||
}
|
||||
isJoystickReturningToCenter = false
|
||||
releaseLatchedSelectedIndex = nil
|
||||
}
|
||||
|
||||
private func makeRadialMenuView() -> RadialMenuView {
|
||||
RadialMenuView(
|
||||
sectors: Self.menuSectors,
|
||||
isTouchSelectionEnabled: false,
|
||||
style: RadialMenuStyle.themed(
|
||||
for: ThemeManager.userInterfaceStyle(),
|
||||
accentColor: ThemeManager.appPrimaryColor
|
||||
),
|
||||
selectionState: selectionState
|
||||
)
|
||||
}
|
||||
|
||||
private static func keyWindow() -> UIWindow? {
|
||||
UIApplication.shared.windows.first(where: { $0.isKeyWindow }) ?? UIApplication.shared.windows.first
|
||||
}
|
||||
|
||||
private static func normalizedDistance(xOffset: CGFloat, yOffset: CGFloat) -> CGFloat {
|
||||
let x = max(min(xOffset, 1), -1)
|
||||
let y = max(min(yOffset, 1), -1)
|
||||
return hypot(x, y)
|
||||
}
|
||||
|
||||
func navigateByController(forward: Bool) {}
|
||||
func navigateByController(downward: Bool) {}
|
||||
func uiWidgetActionForControllerNavigator(forward: Bool, from navigation: ControllerNavigationElement) {}
|
||||
func uiButtonActionForControllerNavigator(pressed: Bool, from navigation: ControllerNavigationElement) {}
|
||||
}
|
||||
Reference in new issue
Block a user