feat(typing): add English autocomplete and Chinese candidate expand panel

Offline English lexicon suggestions with autocapitalization, plus a same-height Chinese more-candidates grid over the key area.
This commit is contained in:
Rocky
2026-08-03 18:30:20 +08:00
parent d1e5fed964
commit 38e5ad570d
35 changed files with 5086 additions and 179 deletions
@@ -0,0 +1,69 @@
// TypingAutocapitalization.swift
// OSGKeyboard · Shared
//
// Decides when the English letter page should arm Shift once for
// automatic capitalization (mirrors UITextAutocapitalizationType).
import Foundation
public enum TypingAutocapitalizationMode: String, Sendable, Equatable {
case none
case words
case sentences
case allCharacters
}
public enum TypingAutocapitalization: Sendable {
/// Whether Shift should be armed for the next Latin letter.
public static func shouldCapitalize(
precedingText: String?,
mode: TypingAutocapitalizationMode
) -> Bool {
switch mode {
case .none:
return false
case .allCharacters:
return true
case .words:
return needsWordCapitalization(precedingText)
case .sentences:
return needsSentenceCapitalization(precedingText)
}
}
// MARK: - Private
private static func needsWordCapitalization(_ preceding: String?) -> Bool {
guard let preceding, !preceding.isEmpty else { return true }
guard let last = preceding.last else { return true }
return last.isWhitespace || last.isNewline
}
private static func needsSentenceCapitalization(_ preceding: String?) -> Bool {
guard let preceding, !preceding.isEmpty else { return true }
// Walk backward past trailing whitespace / newlines; capitalize when
// the field is empty or the previous visible character ends a sentence.
var index = preceding.endIndex
var sawContent = false
while index > preceding.startIndex {
index = preceding.index(before: index)
let character = preceding[index]
if character.isWhitespace || character.isNewline {
continue
}
sawContent = true
return isSentenceTerminator(character)
}
return !sawContent
}
private static func isSentenceTerminator(_ character: Character) -> Bool {
switch character {
case ".", "!", "?", "", "", "", "":
return true
default:
return false
}
}
}