498f407585
Introduce optional Apple account-backed credits with scoped gateway access while preserving local and BYOK paths. Refresh assistant behavior, tests, privacy disclosures, docs, and the website for the 2.0 experience.
125 lines
4.4 KiB
Swift
125 lines
4.4 KiB
Swift
// GlobeInputModeButton.swift
|
||
// OSGKeyboard · Keyboard Extension
|
||
//
|
||
// System "next keyboard" (🌐) control. Registering
|
||
// `handleInputModeList(from:with:)` for all touch events lets UIKit provide
|
||
// native tap-to-advance and long-press input-mode selection without retaining
|
||
// a transient `UIEvent`. SwiftUI draws the shared native key chrome while a
|
||
// transparent `UIButton` owns touch delivery.
|
||
|
||
import OSGKeyboardShared
|
||
import SwiftUI
|
||
import UIKit
|
||
|
||
struct GlobeInputModeButton: UIViewRepresentable {
|
||
@ObservedObject var state: KeyboardState
|
||
@Binding var isPressed: Bool
|
||
|
||
func makeUIView(context: Context) -> GlobeInputModeButtonView {
|
||
let view = GlobeInputModeButtonView()
|
||
view.onHighlightChanged = { isPressed = $0 }
|
||
view.setInputModeController(state.inputModeController)
|
||
return view
|
||
}
|
||
|
||
func updateUIView(_ uiView: GlobeInputModeButtonView, context: Context) {
|
||
uiView.onHighlightChanged = { isPressed = $0 }
|
||
uiView.setInputModeController(state.inputModeController)
|
||
}
|
||
|
||
static func dismantleUIView(_ uiView: GlobeInputModeButtonView, coordinator: Void) {
|
||
uiView.onHighlightChanged = nil
|
||
uiView.setInputModeController(nil)
|
||
}
|
||
}
|
||
|
||
/// SwiftUI wrapper that frames the UIKit globe button and wires it to the
|
||
/// shared `KeyboardState` action hooks. Used on both iPad voice and typing
|
||
/// surfaces; iPhone relies on the system-provided switch below the keyboard.
|
||
/// Default 44×30 remains available for previews; bottom action rows pass an
|
||
/// explicit width / height so the key shares their complete geometry.
|
||
struct SystemGlobeKey: View {
|
||
@Environment(\.colorScheme) private var colorScheme
|
||
@Environment(\.themePalette) private var palette
|
||
@ObservedObject var state: KeyboardState
|
||
@State private var isPressed = false
|
||
|
||
var width: CGFloat = 44
|
||
var height: CGFloat = 30
|
||
|
||
var body: some View {
|
||
ZStack {
|
||
NativeKeyboardKeySurface(
|
||
isPressed: isPressed,
|
||
fill: NativeKeyboardKeyColors.fill(for: colorScheme),
|
||
pressedFill: NativeKeyboardKeyColors.pressedFill(for: colorScheme),
|
||
border: palette.divider,
|
||
cornerRadius: KeyboardChromeLayout.actionKeyCornerRadius
|
||
) {
|
||
Image(systemName: "globe")
|
||
.font(.system(size: 17, weight: .medium))
|
||
.foregroundStyle(NativeKeyboardKeyColors.text(for: colorScheme))
|
||
.accessibilityHidden(true)
|
||
}
|
||
|
||
GlobeInputModeButton(state: state, isPressed: $isPressed)
|
||
.accessibilityLabel(ExtL10n.text("keyboard.nextKeyboardA11y"))
|
||
.accessibilityHint(ExtL10n.text("keyboard.nextKeyboardA11yHint"))
|
||
}
|
||
.frame(width: width, height: height)
|
||
}
|
||
}
|
||
|
||
final class GlobeInputModeButtonView: UIButton {
|
||
var onHighlightChanged: ((Bool) -> Void)?
|
||
|
||
/// UIKit controls do not retain action targets, but keeping this explicitly
|
||
/// weak documents and enforces the keyboard controller ownership boundary.
|
||
private weak var inputModeController: UIInputViewController?
|
||
private let inputModeAction = #selector(UIInputViewController.handleInputModeList(from:with:))
|
||
|
||
override var isHighlighted: Bool {
|
||
didSet {
|
||
guard oldValue != isHighlighted else { return }
|
||
onHighlightChanged?(isHighlighted)
|
||
}
|
||
}
|
||
|
||
override init(frame: CGRect) {
|
||
super.init(frame: frame)
|
||
configure()
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
private func configure() {
|
||
// SwiftUI renders the icon, fill, border, shadow, and pressed state.
|
||
// This UIKit layer stays transparent and handles only system gestures.
|
||
backgroundColor = .clear
|
||
isExclusiveTouch = true
|
||
accessibilityTraits = .keyboardKey
|
||
}
|
||
|
||
func setInputModeController(_ controller: UIInputViewController?) {
|
||
guard inputModeController !== controller else { return }
|
||
if let inputModeController {
|
||
removeTarget(
|
||
inputModeController,
|
||
action: inputModeAction,
|
||
for: .allTouchEvents
|
||
)
|
||
}
|
||
inputModeController = controller
|
||
if let controller {
|
||
addTarget(
|
||
controller,
|
||
action: inputModeAction,
|
||
for: .allTouchEvents
|
||
)
|
||
}
|
||
}
|
||
}
|