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,90 @@
// TypingSurfaceMetrics.swift
// OSGKeyboard · Shared
//
// Size decisions for the typing surface: which key metrics apply, and how tall
// the keyboard must be to hold them.
//
// These live here rather than next to the SwiftUI view because two independent
// consumers must agree on them exactly `KeyboardViewController` sets a UIKit
// height constraint, and the SwiftUI grid lays keys out inside it. If they ever
// pick different metrics the bottom row is clipped, so there is one source of
// truth and it is unit-testable without the extension target.
import CoreGraphics
public enum TypingSurfaceMetrics {
// MARK: - Structural bands (identical on every device)
/// Candidate / top control band above the key grid.
public static let topRegionHeight: CGFloat = 44
/// Gap between the top band and the first key row.
public static let verticalKeySpacing: CGFloat = 8
public static let outerPaddingTop: CGFloat = 4
public static let outerPaddingBottom: CGFloat = 4
// MARK: - Phone
public static let keyRowHeight: CGFloat = 50
public static let keyRowSpacing: CGFloat = 7
public static let keyHorizontalSpacing: CGFloat = 6
public static let secondRowInset: CGFloat = 18
// MARK: - iPad (narrow: portrait, or a compact-ish regular window)
public static let iPadKeyRowHeight: CGFloat = 54
public static let iPadKeyRowSpacing: CGFloat = 8
public static let iPadKeyHorizontalSpacing: CGFloat = 8
// MARK: - iPad (wide: landscape or a large Stage Manager window)
/// The grid spans the full host width here, so rows must grow with it.
/// Holding the narrow 54 pt height at ~1200 pt wide produces 110×54 keys
/// flatter than any system key.
public static let wideIPadKeyRowHeight: CGFloat = 76
public static let wideIPadKeyRowSpacing: CGFloat = 10
public static let wideIPadKeyHorizontalSpacing: CGFloat = 10
/// Key metrics for a device class and available width. Width not device
/// orientation picks the wide bucket, so a resized Stage Manager window
/// lands on the right metrics without consulting orientation at all.
public static func metrics(isIPad: Bool, width: CGFloat) -> TypingKeyLayoutBuilder.Metrics {
guard isIPad else {
return TypingKeyLayoutBuilder.Metrics(
keyRowHeight: keyRowHeight,
keyRowSpacing: keyRowSpacing,
keyHorizontalSpacing: keyHorizontalSpacing,
secondRowInset: secondRowInset,
bottomRowHeight: KeyboardChromeLayout.actionKeyHeight,
bottomActionSpacing: KeyboardChromeLayout.actionKeySpacing,
gridToBottomSpacing: keyRowSpacing
)
}
let isWide = KeyboardChromeLayout.usesWideIPadMetrics(isIPad: true, width: width)
let rowHeight = isWide ? wideIPadKeyRowHeight : iPadKeyRowHeight
let rowSpacing = isWide ? wideIPadKeyRowSpacing : iPadKeyRowSpacing
return TypingKeyLayoutBuilder.Metrics(
keyRowHeight: rowHeight,
keyRowSpacing: rowSpacing,
keyHorizontalSpacing: isWide ? wideIPadKeyHorizontalSpacing : iPadKeyHorizontalSpacing,
// Ignored: iPad derives the indent from the first row's key width.
secondRowInset: 0,
bottomRowHeight: rowHeight,
bottomActionSpacing: KeyboardChromeLayout.actionKeySpacing,
gridToBottomSpacing: rowSpacing,
derivesSecondRowInsetFromKeyWidth: true
)
}
/// Content-driven keyboard height. iPad grows its rows, so the shell has to
/// grow with them or the bottom row is clipped.
public static func contentHeight(isIPad: Bool, width: CGFloat) -> CGFloat {
guard isIPad else { return KeyboardChromeLayout.totalHeight }
let m = metrics(isIPad: true, width: width)
let inner = topRegionHeight
+ verticalKeySpacing
+ (3 * m.keyRowHeight + 2 * m.keyRowSpacing)
+ m.gridToBottomSpacing
+ m.bottomRowHeight
return inner + outerPaddingTop + outerPaddingBottom
}
}