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:
@@ -60,6 +60,12 @@ public enum TypingKeyLayoutBuilder {
|
||||
public var bottomActionSpacing: CGFloat
|
||||
/// Gap between the last letter row and the bottom action row.
|
||||
public var gridToBottomSpacing: CGFloat
|
||||
/// When true, `secondRowInset` is ignored and the second row is inset
|
||||
/// so its keys are exactly as wide as the first row's, leaving a half
|
||||
/// key at each end — what the system keyboard does. A fixed inset is a
|
||||
/// fraction of a 700 pt column and stops reading as a deliberate
|
||||
/// indent once the grid fills an iPad's width.
|
||||
public var derivesSecondRowInsetFromKeyWidth: Bool
|
||||
|
||||
public init(
|
||||
keyRowHeight: CGFloat = 50,
|
||||
@@ -68,7 +74,8 @@ public enum TypingKeyLayoutBuilder {
|
||||
secondRowInset: CGFloat = 18,
|
||||
bottomRowHeight: CGFloat = KeyboardChromeLayout.actionKeyHeight,
|
||||
bottomActionSpacing: CGFloat = KeyboardChromeLayout.actionKeySpacing,
|
||||
gridToBottomSpacing: CGFloat = 7
|
||||
gridToBottomSpacing: CGFloat = 7,
|
||||
derivesSecondRowInsetFromKeyWidth: Bool = false
|
||||
) {
|
||||
self.keyRowHeight = keyRowHeight
|
||||
self.keyRowSpacing = keyRowSpacing
|
||||
@@ -77,16 +84,53 @@ public enum TypingKeyLayoutBuilder {
|
||||
self.bottomRowHeight = bottomRowHeight
|
||||
self.bottomActionSpacing = bottomActionSpacing
|
||||
self.gridToBottomSpacing = gridToBottomSpacing
|
||||
self.derivesSecondRowInsetFromKeyWidth = derivesSecondRowInsetFromKeyWidth
|
||||
}
|
||||
}
|
||||
|
||||
/// Inset that makes `row` keys as wide as a full `referenceCount` row.
|
||||
/// Both rows are laid out at the same unit width, so the second row simply
|
||||
/// gives back the width of the keys it does not have, split evenly.
|
||||
static func derivedSecondRowInset(
|
||||
totalWidth: CGFloat,
|
||||
referenceCount: Int,
|
||||
rowCount: Int,
|
||||
spacing: CGFloat,
|
||||
weightTotal: CGFloat,
|
||||
referenceWeightTotal: CGFloat
|
||||
) -> CGFloat {
|
||||
guard referenceCount > 0, rowCount > 0, referenceWeightTotal > 0 else { return 0 }
|
||||
let referenceSpacing = spacing * CGFloat(max(0, referenceCount - 1))
|
||||
let unitWidth = (totalWidth - referenceSpacing) / referenceWeightTotal
|
||||
let rowWidth = unitWidth * weightTotal + spacing * CGFloat(max(0, rowCount - 1))
|
||||
return max(0, (totalWidth - rowWidth) / 2)
|
||||
}
|
||||
|
||||
/// Bottom-row semantic labels used by the touch pad (not always the glyph).
|
||||
/// When present on iPad, the globe key is excluded from the touch pad's hit
|
||||
/// testing — its `SystemGlobeKey` UIButton handles tap (advance) /
|
||||
/// long-press (system input-mode list) directly.
|
||||
public enum BottomKeyID: String, Sendable {
|
||||
case globe = "bottom.globe"
|
||||
case pageSwitch = "bottom.page"
|
||||
case comma = "bottom.comma"
|
||||
case space = "bottom.space"
|
||||
case period = "bottom.period"
|
||||
case `return` = "bottom.return"
|
||||
}
|
||||
|
||||
/// Comma / period glyphs for the iPad bottom row. `nil` keeps the phone's
|
||||
/// four-slot row.
|
||||
public struct PunctuationKeys: Equatable, Sendable {
|
||||
public let comma: String
|
||||
public let period: String
|
||||
|
||||
public init(comma: String, period: String) {
|
||||
self.comma = comma
|
||||
self.period = period
|
||||
}
|
||||
}
|
||||
|
||||
public static func build(
|
||||
size: CGSize,
|
||||
letterRows: [[String]],
|
||||
@@ -94,14 +138,37 @@ public enum TypingKeyLayoutBuilder {
|
||||
spaceLabel: String,
|
||||
returnLabel: String,
|
||||
metrics: Metrics = Metrics(),
|
||||
includeGlobeKey: Bool = true,
|
||||
punctuationKeys: PunctuationKeys? = nil,
|
||||
showTopRowNumbers: Bool = false,
|
||||
topRowNumbers: [String] = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"],
|
||||
keyWeight: (_ label: String, _ index: Int, _ rowIndex: Int) -> CGFloat
|
||||
) -> TypingKeyLayout {
|
||||
var keys: [TypingKeyHitTarget] = []
|
||||
var cursorY: CGFloat = 0
|
||||
|
||||
let firstRow = letterRows.first ?? []
|
||||
let firstRowWeightTotal = firstRow.enumerated()
|
||||
.map { keyWeight($0.element, $0.offset, 0) }
|
||||
.reduce(0, +)
|
||||
|
||||
for (rowIndex, row) in letterRows.enumerated() {
|
||||
let inset = rowIndex == 1 ? metrics.secondRowInset : 0
|
||||
let weights = row.enumerated().map { keyWeight($0.element, $0.offset, rowIndex) }
|
||||
let inset: CGFloat
|
||||
if rowIndex == 1 {
|
||||
inset = metrics.derivesSecondRowInsetFromKeyWidth
|
||||
? derivedSecondRowInset(
|
||||
totalWidth: size.width,
|
||||
referenceCount: firstRow.count,
|
||||
rowCount: row.count,
|
||||
spacing: metrics.keyHorizontalSpacing,
|
||||
weightTotal: weights.reduce(0, +),
|
||||
referenceWeightTotal: firstRowWeightTotal
|
||||
)
|
||||
: metrics.secondRowInset
|
||||
} else {
|
||||
inset = 0
|
||||
}
|
||||
let spacingTotal = metrics.keyHorizontalSpacing * CGFloat(max(0, row.count - 1))
|
||||
let availableWidth = size.width - inset * 2 - spacingTotal
|
||||
let unitWidth = availableWidth / max(1, weights.reduce(0, +))
|
||||
@@ -115,12 +182,20 @@ public enum TypingKeyLayoutBuilder {
|
||||
width: width,
|
||||
height: metrics.keyRowHeight
|
||||
)
|
||||
// iPad top letter row carries the small number overlay (1–0),
|
||||
// mirroring the iOS system keyboard. Interior rows don't.
|
||||
let number = (showTopRowNumbers
|
||||
&& rowIndex == 0
|
||||
&& keyIndex < topRowNumbers.count)
|
||||
? topRowNumbers[keyIndex]
|
||||
: nil
|
||||
keys.append(
|
||||
TypingKeyHitTarget(
|
||||
id: "grid.\(rowIndex).\(keyIndex)",
|
||||
label: label,
|
||||
visualFrame: frame,
|
||||
behavior: TypingKeyBehaviorResolver.behavior(for: label)
|
||||
behavior: TypingKeyBehaviorResolver.behavior(for: label),
|
||||
displayNumber: number
|
||||
)
|
||||
)
|
||||
x += width + metrics.keyHorizontalSpacing
|
||||
@@ -134,12 +209,38 @@ public enum TypingKeyLayoutBuilder {
|
||||
|
||||
cursorY += metrics.gridToBottomSpacing
|
||||
let bottomY = cursorY
|
||||
let widths = KeyboardChromeLayout.actionKeyWidths(availableWidth: size.width)
|
||||
let bottomFrames: [(String, String, CGFloat)] = [
|
||||
(BottomKeyID.pageSwitch.rawValue, pageSwitchLabel, widths.side),
|
||||
(BottomKeyID.space.rawValue, spaceLabel, widths.center),
|
||||
(BottomKeyID.return.rawValue, returnLabel, widths.side)
|
||||
]
|
||||
// On iPad the globe lives at the far-left as a UIKit-backed key
|
||||
// (handled by SystemGlobeKey); registering its frame reserves the slot
|
||||
// and lets the touch pad skip hit-testing it. iPhone omits the slot.
|
||||
let bottomFrames: [(String, String, CGFloat)]
|
||||
if let punctuationKeys {
|
||||
let widths = KeyboardChromeLayout.iPadActionKeyWidths(availableWidth: size.width)
|
||||
bottomFrames = [
|
||||
(BottomKeyID.globe.rawValue, "", widths.globe),
|
||||
(BottomKeyID.pageSwitch.rawValue, pageSwitchLabel, widths.pageSwitch),
|
||||
(BottomKeyID.comma.rawValue, punctuationKeys.comma, widths.comma),
|
||||
(BottomKeyID.space.rawValue, spaceLabel, widths.space),
|
||||
(BottomKeyID.period.rawValue, punctuationKeys.period, widths.period),
|
||||
(BottomKeyID.return.rawValue, returnLabel, widths.return)
|
||||
]
|
||||
} else if includeGlobeKey {
|
||||
let widths = KeyboardChromeLayout.actionKeyWidths(availableWidth: size.width)
|
||||
bottomFrames = [
|
||||
(BottomKeyID.globe.rawValue, "", widths.globe),
|
||||
(BottomKeyID.pageSwitch.rawValue, pageSwitchLabel, widths.side),
|
||||
(BottomKeyID.space.rawValue, spaceLabel, widths.center),
|
||||
(BottomKeyID.return.rawValue, returnLabel, widths.side2)
|
||||
]
|
||||
} else {
|
||||
let widths = KeyboardChromeLayout.actionKeyWidthsWithoutGlobe(
|
||||
availableWidth: size.width
|
||||
)
|
||||
bottomFrames = [
|
||||
(BottomKeyID.pageSwitch.rawValue, pageSwitchLabel, widths.side),
|
||||
(BottomKeyID.space.rawValue, spaceLabel, widths.center),
|
||||
(BottomKeyID.return.rawValue, returnLabel, widths.side2)
|
||||
]
|
||||
}
|
||||
|
||||
var bottomX: CGFloat = 0
|
||||
for (index, item) in bottomFrames.enumerated() {
|
||||
|
||||
Reference in New Issue
Block a user