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
85 lines
3.4 KiB
Swift
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
|
|
)
|
|
}
|
|
}
|