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.
This commit is contained in:
Rocky
2026-08-10 13:50:50 +08:00
parent 53abad2050
commit 2bc8c1b87d
85 changed files with 5703 additions and 3997 deletions
@@ -0,0 +1,124 @@
// 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
)
}
}
}