feat(keyboard): add English QuickType bar and system lexicon

Show verbatim/correction/completion slots, mmap a 40k-word list, and
use UITextChecker plus supplementary lexicon for conservative autocorrect.
This commit is contained in:
Rocky
2026-08-14 21:49:22 +08:00
parent 2c3a3f80f3
commit 4749a9cbf2
34 changed files with 45539 additions and 3395 deletions
+5 -3
View File
@@ -132,15 +132,17 @@ struct AIKeyboardView: View {
)
.padding(.horizontal, KeyboardTopBarMetrics.nestedHorizontalInset)
} else {
HStack(spacing: Spacing.xs) {
KeyboardBrandLogo(action: state.openSettings)
Spacer(minLength: 0)
ZStack {
KeyboardTopControls(
state: state,
typing: typing,
palette: palette,
onInsert: onInsert
)
HStack {
KeyboardBrandLogo(action: state.openSettings)
Spacer(minLength: 0)
}
}
.padding(.horizontal, KeyboardTopBarMetrics.nestedHorizontalInset)
}
+7 -5
View File
@@ -238,17 +238,19 @@ public struct KeyboardRootView: View {
onDismiss: state.dismissClipboardSuggestion
)
} else {
HStack(spacing: Spacing.xs) {
KeyboardBrandLogo(action: state.openSettings)
// Globe key now lives at the bottom-left of the keyboard (matching
// iOS system layout); see micActionRow's bottom HStack.
Spacer(minLength: 0)
ZStack {
KeyboardTopControls(
state: state,
typing: typing,
palette: palette,
onInsert: onInsert
)
HStack {
KeyboardBrandLogo(action: state.openSettings)
// Globe key now lives at the bottom-left of the keyboard (matching
// iOS system layout); see micActionRow's bottom HStack.
Spacer(minLength: 0)
}
}
}
}
+47 -27
View File
@@ -23,8 +23,10 @@ enum KeyboardTopBarMetrics {
static let horizontalInset: CGFloat = 12
/// TypingRootView already contributes 8 pt around the entire key surface.
static let nestedHorizontalInset: CGFloat = horizontalInset - KeyboardChromeLayout.horizontalInset
static let logoHeight: CGFloat = 22
static let logoHeight: CGFloat = 16
static let logoWidth: CGFloat = logoHeight * 952 / 291
/// Equal hit width for AI / Voice / Chinese / English input tabs.
static let inputTabWidth: CGFloat = 42
/// Shared footprint for top-trailing chips (clipboard, cancel/X, translation).
static let trailingChipSize: CGFloat = 34
static let trailingChipIconSize: CGFloat = 15
@@ -113,39 +115,45 @@ struct KeyboardTopControls: View {
let onInsert: (String) -> Void
var body: some View {
HStack(spacing: 6) {
// /
HStack(spacing: 2) {
ForEach(KeyboardInputTab.allCases, id: \.self) { tab in
tabButton(tab)
ZStack {
inputTabSwitcher
if state.canShowClipboardEntry {
HStack {
Spacer(minLength: 0)
KeyboardClipboardMenuButton(
palette: palette,
action: state.openClipboardPanel
)
.equatable()
}
}
.padding(2)
.background(tabTrackFill, in: Capsule())
.overlay(
Capsule().stroke(palette.divider, lineWidth: 0.5)
)
}
.frame(maxWidth: .infinity)
}
if state.canShowClipboardEntry {
KeyboardClipboardMenuButton(
palette: palette,
action: state.openClipboardPanel
)
.equatable()
private var inputTabSwitcher: some View {
// Logo /
HStack(spacing: 2) {
ForEach(KeyboardInputTab.allCases, id: \.self) { tab in
tabButton(tab)
}
}
.padding(2)
.background(tabTrackFill, in: Capsule())
.overlay(
Capsule().stroke(palette.divider, lineWidth: 0.5)
)
}
private func tabButton(_ tab: KeyboardInputTab) -> some View {
let selected = isSelected(tab)
let width: CGFloat = tab == .english || tab == .ai ? 34 : 42
return Button {
withAnimation(Motion.soft) {
select(tab)
}
} label: {
tabLabel(tab, selected: selected, width: width)
tabLabel(tab, selected: selected)
}
.buttonStyle(TopControlPressStyle(pressedFill: pressedFill))
.disabled(tab != .voice && !state.canEnterTypingSurface)
@@ -157,13 +165,11 @@ struct KeyboardTopControls: View {
@ViewBuilder
private func tabLabel(
_ tab: KeyboardInputTab,
selected: Bool,
width: CGFloat
selected: Bool
) -> some View {
let label = Text(tab.title)
.font(.system(size: 12, weight: selected ? .semibold : .medium))
let label = tabContent(tab, selected: selected)
.foregroundStyle(selected ? palette.textPrimary : palette.textSecondary)
.frame(width: width, height: 30)
.frame(width: KeyboardTopBarMetrics.inputTabWidth, height: 30)
if selected {
let namespace = sharedSelectionNamespace ?? fallbackSelectionNamespace
@@ -179,6 +185,20 @@ struct KeyboardTopControls: View {
}
}
@ViewBuilder
private func tabContent(_ tab: KeyboardInputTab, selected: Bool) -> some View {
if tab == .ai {
Image(systemName: "sparkle")
.font(.system(size: 15, weight: selected ? .semibold : .medium))
} else if tab == .voice {
Image(systemName: "waveform.mid")
.font(.system(size: 15, weight: selected ? .semibold : .medium))
} else {
Text(tab.title)
.font(.system(size: 12, weight: selected ? .semibold : .medium))
}
}
private func tabOpacity(_ tab: KeyboardInputTab) -> Double {
guard tab != .voice, !state.canEnterTypingSurface else { return 1 }
if case .recording = state.phase {
@@ -191,10 +211,10 @@ struct KeyboardTopControls: View {
colorScheme == .dark ? Color(white: 0.22) : Color(white: 0.84)
}
/// NativeKeyboardKeyColors.fill
///
/// NativeKeyboardKeyColors.fill
/// 宿
private var tabTrackFill: Color {
colorScheme == .dark ? Color(white: 0.12) : Color(white: 0.87)
colorScheme == .dark ? Color(white: 0.12) : Color.black.opacity(0.12)
}
private func isSelected(_ tab: KeyboardInputTab) -> Bool {