Files
OSGKeyboard/OSGKeyboardShared/Typing/VoiceKeyRowLayout.swift
T
Rocky 4dce67df84 chore(release): bump version to 2.1.6 (build 131)
Version: CURRENT_PROJECT_VERSION 130 -> 131 (project.yml, AGENTS.md, READMEs,
docs/llms.txt).

fix(flow): host-app identification now compiles in every build configuration
(Debug, TestFlight, Release). The arbiter probe and HostAppIdentity were
DEBUG-only, so TestFlight/App Store builds never knew the host bundle id and
could not auto-return after a startflow handoff. Debug-only logging and the
Home test card are unchanged.

Also includes the pending work in the tree:
- Home: single account/setup card surface (HomeAccountSurfacePolicy/Views)
- Wide screens: voice key row and centred mic placement, iPhone Duo layout
- Keyboard: clipboard skill ranking and AI skill layout updates
- Debug: account session handoff to the Simulator (+ import script)
- Docs: portfolio assets, privacy policy URL, site/README refresh
2026-10-29 11:54:13 +08:00

85 lines
3.4 KiB
Swift

// VoiceKeyRowLayout.swift
// OSGKeyboard · Shared
//
// Keyboard-style voice bottom row for wide hosts (iPad, unfolded iPhone Duo).
import CoreGraphics
/// Horizontal geometry of the wide-host voice bottom row:
///
/// [leadingSide][leadingContext][ send ][trailingContext][trailingSide]
///
/// - The four icon keys (space / undo / edit / delete) share one width and send
/// is exactly twice as wide, so the primary action reads as the main key while
/// the row fills edge to edge with uniform gaps. The row is symmetric, so send
/// sits on the centre line under the microphone.
/// - No globe: the voice surface leaves input-mode switching to the typing tab.
/// - Context slots (undo / edit) exist even while the keys are disabled, so
/// nothing in the row shifts when they toggle.
///
/// Coordinates are relative to the row's own leading edge.
public struct VoiceKeyRowLayout: Equatable, Sendable {
public struct Slot: Equatable, Sendable {
public let minX: CGFloat
public let width: CGFloat
public var midX: CGFloat { minX + width / 2 }
public var maxX: CGFloat { minX + width }
public init(minX: CGFloat, width: CGFloat) {
self.minX = minX
self.width = width
}
}
/// Send width as a multiple of one icon key.
public static let sendWidthMultiplier: CGFloat = 2
/// Below this host width the icon keys would shrink past a comfortable
/// touch target, so narrower hosts keep the phone's circle cluster.
public static let minimumHostWidth: CGFloat = 500
public let leadingSide: Slot
public let leadingContext: Slot
public let send: Slot
public let trailingContext: Slot
public let trailingSide: Slot
/// Wide hosts only: iPad-scale metrics, the expanded-phone tier, or a
/// landscape iPhone (whose shorter panel has no room for two circle rows),
/// with enough width for the row. Portrait iPhones keep the circle
/// cluster, which suits one-handed thumb reach.
public static func isAvailable(
usesIPadMetrics: Bool,
usesExpandedPhoneMetrics: Bool,
usesCompactHeightPhoneMetrics: Bool,
hostWidth: CGFloat
) -> Bool {
(usesIPadMetrics || usesExpandedPhoneMetrics || usesCompactHeightPhoneMetrics)
&& hostWidth >= minimumHostWidth
}
/// - Parameter rowWidth: The row's width, i.e. the same width the typing bottom row spans.
public static func make(rowWidth: CGFloat) -> VoiceKeyRowLayout {
let spacing = KeyboardChromeLayout.actionKeySpacing
// 4 icon keys + one send key worth `sendWidthMultiplier` icon keys,
// with 4 gaps between those five.
let keySpan = max(0, rowWidth - spacing * 4)
let keyWidth = keySpan / (4 + sendWidthMultiplier)
let leadingSide = Slot(minX: 0, width: keyWidth)
let leadingContext = Slot(minX: leadingSide.maxX + spacing, width: keyWidth)
let send = Slot(minX: leadingContext.maxX + spacing, width: keyWidth * sendWidthMultiplier)
let trailingContext = Slot(minX: send.maxX + spacing, width: keyWidth)
let trailingSide = Slot(minX: trailingContext.maxX + spacing, width: keyWidth)
return VoiceKeyRowLayout(
leadingSide: leadingSide,
leadingContext: leadingContext,
send: send,
trailingContext: trailingContext,
trailingSide: trailingSide
)
}
}