Files
OSGKeyboard/OSGKeyboardExt/Views/GlobeInputModeButton.swift
T
Rocky 2bc8c1b87d fix(ipad): ship iPad P0 layout/globe fixes, edit-last-input, drop clipboard commands
Adapt typing/voice surfaces for iPad width and height, add the system globe
key and last-input editing flow, harden host-only Rime deployment, and remove
clipboard voice commands. Bump build to 61.
2026-08-10 13:50:50 +08:00

125 lines
4.4 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 SwiftUI
import UIKit
import OSGKeyboardShared
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
)
}
}
}