Files
OSGKeyboard/OSGKeyboardShared/Models/KeyboardChromeLayout.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

161 lines
7.7 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// KeyboardChromeLayout.swift
// OSGKeyboard · Shared
//
// Cross-surface dimensions that must stay identical in voice and typing modes.
import CoreGraphics
public enum KeyboardChromeLayout {
public static let totalHeight: CGFloat = 281
public static let actionKeyHeight: CGFloat = 50
/// Shared capsule action height for unified Send and edit-mode controls.
public static let assistantActionCapsuleHeight: CGFloat = actionKeyHeight
public static let actionKeyCornerRadius: CGFloat = 10
/// Shared spacing for every bottom action row. iPad uses a custom globe
/// slot; iPhone relies on the system-provided switch below the keyboard.
public static let actionKeySpacing: CGFloat = 8
/// Globe (🌐) key — small, icon-only.
public static let globeActionKeyFraction: CGFloat = 0.12
/// Outer side key — pageSwitch / delete.
public static let sideActionKeyFraction: CGFloat = 0.18
/// Center key — space (typing) or return (voice). Widest in the row.
public static let centerActionKeyFraction: CGFloat = 0.50
/// Other outer side key — return (typing) or space/delete (voice).
public static let side2ActionKeyFraction: CGFloat = 0.20
/// iPad typing bottom row: `[globe · 123 · , · space · . · return]`.
///
/// The four-slot phone row gives the centre key 50% of the width, which on
/// a full-width landscape iPad turns the space bar into a ~660 pt runway.
/// The system keyboard spends that width on more keys instead, so iPad
/// gains comma / period and space settles near the system's ~430 pt.
public static let iPadGlobeFraction: CGFloat = 0.09
public static let iPadPageSwitchFraction: CGFloat = 0.13
public static let iPadPunctuationFraction: CGFloat = 0.09
public static let iPadSpaceFraction: CGFloat = 0.38
public static let iPadReturnFraction: CGFloat = 0.22
/// Five gaps separate the six iPad slots.
public static let iPadActionKeyGapCount: CGFloat = 5
public static let horizontalInset: CGFloat = 8
/// Voice-surface content column cap.
///
/// The voice surface is a sparse control cluster over a transparent
/// background, so filling an iPad's width buys no visual width and only
/// parks delete/return at the screen edges. The typing surface has the
/// opposite need (a key grid must fill the width to match the system
/// keyboard), which is why it no longer shares this constant.
public static let voiceContentMaxWidth: CGFloat = 700
/// Hosts that draw the keyboard-style voice key row span the typing width:
/// the row, top bar and logo must sit on the same frame as the typing keys,
/// or they jump sideways on every tab switch.
public static func voiceContentWidthCap(usesVoiceKeyRow: Bool) -> CGFloat {
usesVoiceKeyRow ? .infinity : voiceContentMaxWidth
}
/// Width at or above which the typing surface switches to wide-iPad
/// metrics (taller rows). Chosen to sit between the widest iPad portrait
/// width (1024 pt on 13") and the narrowest landscape width (1133 pt on
/// mini), so it tracks real available width rather than device orientation
/// — Stage Manager and Split View resize into the right bucket for free.
public static let wideIPadWidthThreshold: CGFloat = 1100
/// Uses iPad-scale metrics only when the host is both an iPad and currently
/// exposes regular horizontal space. Compact Split View / Slide Over keeps
/// the phone-scale layout, while wide iPhones never opt into iPad metrics.
public static func usesIPadMetrics(isPad: Bool, hasRegularWidth: Bool) -> Bool {
isPad && hasRegularWidth
}
/// System-sized iPhone-shaped keys for a phone host with regular space on
/// both axes — the unfolded iPhone Duo inner display. Landscape Max-size
/// iPhones are regular × compact and keep the phone metrics.
public static func usesExpandedPhoneMetrics(
isPad: Bool,
hasRegularWidth: Bool,
hasRegularHeight: Bool
) -> Bool {
!isPad && hasRegularWidth && hasRegularHeight
}
/// Shorter phone panel whenever the host is vertically compact: every
/// iPhone in landscape, including the folded iPhone Duo. Compact-height
/// iPad windows keep their own metrics.
public static func usesCompactHeightPhoneMetrics(isPad: Bool, hasCompactHeight: Bool) -> Bool {
!isPad && hasCompactHeight
}
/// iPad always draws its own globe key. Elsewhere UIKit decides: Face ID
/// iPhones get a system switch below the keyboard, while the unfolded
/// iPhone Duo and Home-button iPhones need the keyboard to provide one.
public static func showsInputModeSwitchKey(
isPad: Bool,
systemRequiresSwitchKey: Bool
) -> Bool {
isPad || systemRequiresSwitchKey
}
/// Wide metrics need iPad-scale keys *and* enough width to justify them.
public static func usesWideIPadMetrics(isIPad: Bool, width: CGFloat) -> Bool {
isIPad && width >= wideIPadWidthThreshold
}
/// Below this width the voice microphone capsule shrinks (Display Zoom on
/// a small iPhone, iPad Slide Over). Every current iPhone is wider.
public static let compactVoiceWidthThreshold: CGFloat = 350
/// Below this width an iPad-metrics voice surface uses its smaller circles.
public static let compactIPadVoiceWidthThreshold: CGFloat = 420
/// `width == 0` means "not measured yet" and never counts as compact.
public static func usesCompactVoiceMetrics(width: CGFloat) -> Bool {
width > 0 && width < compactVoiceWidthThreshold
}
public static func usesCompactIPadVoiceMetrics(isIPad: Bool, width: CGFloat) -> Bool {
isIPad && width > 0 && width < compactIPadVoiceWidthThreshold
}
/// Splits the width left after three gaps into a 12 / 18 / 50 / 20 row for
/// layouts that include a globe key, with a wide centre and balanced sides.
public static func actionKeyWidths(availableWidth: CGFloat) -> (globe: CGFloat, side: CGFloat, center: CGFloat, side2: CGFloat) {
let keyWidth = max(0, availableWidth - actionKeySpacing * 3)
return (
globe: keyWidth * globeActionKeyFraction,
side: keyWidth * sideActionKeyFraction,
center: keyWidth * centerActionKeyFraction,
side2: keyWidth * side2ActionKeyFraction
)
}
/// iPhone bottom row `[side · center · side2]`. Preserve the established
/// side/centre balance while redistributing the removed globe slot.
public static func actionKeyWidthsWithoutGlobe(
availableWidth: CGFloat
) -> (side: CGFloat, center: CGFloat, side2: CGFloat) {
let keyWidth = max(0, availableWidth - actionKeySpacing * 2)
let fractionTotal = sideActionKeyFraction
+ centerActionKeyFraction
+ side2ActionKeyFraction
return (
side: keyWidth * sideActionKeyFraction / fractionTotal,
center: keyWidth * centerActionKeyFraction / fractionTotal,
side2: keyWidth * side2ActionKeyFraction / fractionTotal
)
}
/// Six-slot iPad variant of `actionKeyWidths`.
public static func iPadActionKeyWidths(
availableWidth: CGFloat
) -> (globe: CGFloat, pageSwitch: CGFloat, comma: CGFloat, space: CGFloat, period: CGFloat, return: CGFloat) {
let keyWidth = max(0, availableWidth - actionKeySpacing * iPadActionKeyGapCount)
return (
globe: keyWidth * iPadGlobeFraction,
pageSwitch: keyWidth * iPadPageSwitchFraction,
comma: keyWidth * iPadPunctuationFraction,
space: keyWidth * iPadSpaceFraction,
period: keyWidth * iPadPunctuationFraction,
return: keyWidth * iPadReturnFraction
)
}
}