feat(keyboard): add Notes skill and system-style typing

Add save-to-Notes export and direct map handoff while aligning multi-touch typing, period shortcuts, return actions, and navigation visuals with system behavior.
This commit is contained in:
Rocky
2026-08-14 18:22:41 +08:00
parent 0a60006d2d
commit 320dc777c9
46 changed files with 1429 additions and 376 deletions
@@ -0,0 +1,181 @@
// TypingTouchTracker.swift
// OSGKeyboard · Shared
//
// Multi-finger typing contract (system-keyboard overlap):
// each finger is independent; a new key-down commits any other pending
// character/space/return so press order, not release order, wins.
// Shift can be held with one finger while another types.
import Foundation
/// Per-step side effects for the UIKit touch pad to apply.
public struct TypingTouchEffects: Equatable, Sendable {
public var commits: [TypingKeyHitTarget] = []
public var playFeedback: TypingKeyHitTarget?
public var deleteFire = false
public var startDeleteRepeat = false
public var stopDeleteRepeat = false
public var beginShift = false
public var endShift = false
public init() {}
}
/// Pure multi-touch state machine. IDs are `ObjectIdentifier` of `UITouch`
/// in the extension, or any unique object in tests.
public final class TypingTouchTracker {
private struct Finger {
let id: ObjectIdentifier
var key: TypingKeyHitTarget?
var committed: Bool
var ownsShift: Bool
var ownsDeleteRepeat: Bool
let order: UInt64
}
private var fingers: [ObjectIdentifier: Finger] = [:]
private var nextOrder: UInt64 = 0
public init() {}
public var highlightedKeyIDs: Set<String> {
Set(
fingers.values.compactMap { finger in
guard !finger.committed else { return nil }
return finger.key?.id
}
)
}
public func began(id: ObjectIdentifier, key: TypingKeyHitTarget?) -> TypingTouchEffects {
var effects = TypingTouchEffects()
if let key, hasUncommittedFinger(on: key.id) {
return effects
}
if key != nil {
commitPendingCharacterKeys(into: &effects)
stopForeignDeleteRepeats(into: &effects)
}
var finger = Finger(
id: id,
key: key,
committed: false,
ownsShift: false,
ownsDeleteRepeat: false,
order: nextOrder
)
nextOrder += 1
if let key {
effects.playFeedback = key
activate(key, on: &finger, effects: &effects)
}
fingers[id] = finger
return effects
}
public func moved(id: ObjectIdentifier, key: TypingKeyHitTarget?) -> TypingTouchEffects {
guard var finger = fingers[id], !finger.committed else {
return TypingTouchEffects()
}
if finger.key?.id == key?.id {
return TypingTouchEffects()
}
var effects = TypingTouchEffects()
if finger.ownsDeleteRepeat {
effects.stopDeleteRepeat = true
finger.ownsDeleteRepeat = false
}
finger.key = key
if let key {
effects.playFeedback = key
activate(key, on: &finger, effects: &effects)
}
fingers[id] = finger
return effects
}
public func ended(id: ObjectIdentifier, key: TypingKeyHitTarget?) -> TypingTouchEffects {
guard let finger = fingers.removeValue(forKey: id) else {
return TypingTouchEffects()
}
return finish(finger, hit: key, commitIfNeeded: true)
}
public func cancelled(id: ObjectIdentifier) -> TypingTouchEffects {
guard let finger = fingers.removeValue(forKey: id) else {
return TypingTouchEffects()
}
return finish(finger, hit: nil, commitIfNeeded: false)
}
// MARK: - Internals
private func activate(
_ key: TypingKeyHitTarget,
on finger: inout Finger,
effects: inout TypingTouchEffects
) {
switch key.behavior {
case .commitOnRelease:
break
case .deleteRepeat:
effects.deleteFire = true
effects.startDeleteRepeat = true
finger.ownsDeleteRepeat = true
case .shiftHold:
if !finger.ownsShift, !fingers.values.contains(where: { $0.ownsShift }) {
effects.beginShift = true
finger.ownsShift = true
}
}
}
/// Press order = typing order: flush other uncommitted character keys.
private func commitPendingCharacterKeys(into effects: inout TypingTouchEffects) {
let pending = fingers.values
.filter { !$0.committed && $0.key?.behavior == .commitOnRelease }
.sorted { $0.order < $1.order }
for finger in pending {
if let key = finger.key {
effects.commits.append(key)
}
fingers[finger.id]?.committed = true
}
}
/// Holding delete + tapping a letter must stop the repeat.
private func stopForeignDeleteRepeats(into effects: inout TypingTouchEffects) {
for (id, finger) in fingers where finger.ownsDeleteRepeat {
effects.stopDeleteRepeat = true
fingers[id]?.ownsDeleteRepeat = false
}
}
private func hasUncommittedFinger(on keyID: String) -> Bool {
fingers.values.contains { !$0.committed && $0.key?.id == keyID }
}
private func finish(
_ finger: Finger,
hit: TypingKeyHitTarget?,
commitIfNeeded: Bool
) -> TypingTouchEffects {
var effects = TypingTouchEffects()
if finger.ownsDeleteRepeat {
effects.stopDeleteRepeat = true
}
if finger.ownsShift {
effects.endShift = true
}
if commitIfNeeded, !finger.committed, let hit, hit.behavior == .commitOnRelease {
effects.commits.append(hit)
}
return effects
}
}