2ed16e1fbf
Add gap-filling hit regions, release-to-commit with slide-to-reselect, touch intent offset, and full-pinyin next-key bias; cut release 1.6.1 (build 45).
185 lines
5.9 KiB
Swift
185 lines
5.9 KiB
Swift
// KeyHitTestingTests.swift
|
|
// OSGKeyboard · Ext unit tests
|
|
//
|
|
// Phase 1 / 3: gap fill, nearest-center, intent offset, edge expansion.
|
|
|
|
import XCTest
|
|
@testable import OSGKeyboardShared
|
|
|
|
final class KeyHitTestingTests: XCTestCase {
|
|
private func makePairTargets(
|
|
left: CGRect = CGRect(x: 0, y: 0, width: 40, height: 50),
|
|
gap: CGFloat = 6
|
|
) -> (targets: [TypingKeyHitTarget], plane: CGRect) {
|
|
let right = CGRect(
|
|
x: left.maxX + gap,
|
|
y: left.minY,
|
|
width: left.width,
|
|
height: left.height
|
|
)
|
|
let targets = [
|
|
TypingKeyHitTarget(
|
|
id: "L",
|
|
label: "A",
|
|
visualFrame: left,
|
|
behavior: .commitOnRelease
|
|
),
|
|
TypingKeyHitTarget(
|
|
id: "R",
|
|
label: "S",
|
|
visualFrame: right,
|
|
behavior: .commitOnRelease
|
|
)
|
|
]
|
|
let plane = left.union(right)
|
|
return (targets, plane)
|
|
}
|
|
|
|
func testGapMidpointHitsNearestKey() {
|
|
let pair = makePairTargets()
|
|
let mid = CGPoint(x: 43, y: 25) // center of 6pt gap between 40 and 46
|
|
let hit = KeyHitTesting.hitTarget(
|
|
at: mid,
|
|
targets: pair.targets,
|
|
keyPlaneBounds: pair.plane,
|
|
horizontalGap: 6,
|
|
verticalGap: 7,
|
|
edgeExpansion: 0
|
|
)
|
|
// Midpoint is equidistant; either is acceptable, but must not miss.
|
|
XCTAssertNotNil(hit)
|
|
}
|
|
|
|
func testGapCloserToLeftSelectsLeft() {
|
|
let pair = makePairTargets()
|
|
// Just right of left key visual edge, still in gap, closer to left center.
|
|
let point = CGPoint(x: 41, y: 25)
|
|
let hit = KeyHitTesting.hitTarget(
|
|
at: point,
|
|
targets: pair.targets,
|
|
keyPlaneBounds: pair.plane,
|
|
horizontalGap: 6,
|
|
verticalGap: 7,
|
|
edgeExpansion: 0
|
|
)
|
|
XCTAssertEqual(hit?.id, "L")
|
|
}
|
|
|
|
func testGapCloserToRightSelectsRight() {
|
|
let pair = makePairTargets()
|
|
let point = CGPoint(x: 45, y: 25)
|
|
let hit = KeyHitTesting.hitTarget(
|
|
at: point,
|
|
targets: pair.targets,
|
|
keyPlaneBounds: pair.plane,
|
|
horizontalGap: 6,
|
|
verticalGap: 7,
|
|
edgeExpansion: 0
|
|
)
|
|
XCTAssertEqual(hit?.id, "R")
|
|
}
|
|
|
|
func testOutsidePlaneReturnsNil() {
|
|
let pair = makePairTargets()
|
|
let hit = KeyHitTesting.hitTarget(
|
|
at: CGPoint(x: 200, y: 200),
|
|
targets: pair.targets,
|
|
keyPlaneBounds: pair.plane,
|
|
horizontalGap: 6,
|
|
verticalGap: 7,
|
|
edgeExpansion: 0
|
|
)
|
|
XCTAssertNil(hit)
|
|
}
|
|
|
|
func testIntentOffsetShiftsHitUpward() {
|
|
// Key occupies y 10…60. Raw touch at y=62 is below the key; with a
|
|
// 4pt upward intent offset it maps to y=58 and should still hit.
|
|
let target = TypingKeyHitTarget(
|
|
id: "K",
|
|
label: "M",
|
|
visualFrame: CGRect(x: 0, y: 10, width: 40, height: 50),
|
|
behavior: .commitOnRelease
|
|
)
|
|
let plane = target.visualFrame
|
|
let raw = CGPoint(x: 20, y: 62)
|
|
let withoutOffset = KeyHitTesting.hitTarget(
|
|
at: raw,
|
|
targets: [target],
|
|
keyPlaneBounds: plane,
|
|
horizontalGap: 0,
|
|
verticalGap: 0,
|
|
edgeExpansion: 0
|
|
)
|
|
let withOffset = KeyHitTesting.hitTarget(
|
|
rawTouch: raw,
|
|
targets: [target],
|
|
keyPlaneBounds: plane,
|
|
horizontalGap: 0,
|
|
verticalGap: 0,
|
|
intentOffsetY: 4,
|
|
edgeExpansion: 0
|
|
)
|
|
XCTAssertNil(withoutOffset)
|
|
XCTAssertEqual(withOffset?.id, "K")
|
|
}
|
|
|
|
func testEdgeExpansionExtendsOuterHit() {
|
|
let target = TypingKeyHitTarget(
|
|
id: "Q",
|
|
label: "Q",
|
|
visualFrame: CGRect(x: 10, y: 0, width: 40, height: 50),
|
|
behavior: .commitOnRelease
|
|
)
|
|
let plane = target.visualFrame
|
|
// Point just left of the visual key, inside edge expansion.
|
|
let point = CGPoint(x: 7, y: 25)
|
|
let hit = KeyHitTesting.hitTarget(
|
|
at: point,
|
|
targets: [target],
|
|
keyPlaneBounds: plane,
|
|
horizontalGap: 0,
|
|
verticalGap: 0,
|
|
edgeExpansion: 5
|
|
)
|
|
XCTAssertEqual(hit?.id, "Q")
|
|
}
|
|
|
|
func testLayoutBuilderCoversGapsBetweenKeys() {
|
|
let layout = TypingKeyLayoutBuilder.build(
|
|
size: CGSize(width: 300, height: 200),
|
|
letterRows: [
|
|
["Q", "W", "E"],
|
|
["A", "S", "D"],
|
|
["Z", "X", "C"]
|
|
],
|
|
pageSwitchLabel: "123",
|
|
spaceLabel: "space",
|
|
returnLabel: "return",
|
|
keyWeight: { _, _, _ in 1 }
|
|
)
|
|
XCTAssertEqual(layout.keys.count, 12) // 9 letters + 3 bottom
|
|
|
|
// Mid-gap between Q and W on first row should hit something.
|
|
let q = layout.keys.first { $0.label == "Q" }!
|
|
let w = layout.keys.first { $0.label == "W" }!
|
|
let mid = CGPoint(x: (q.visualFrame.maxX + w.visualFrame.minX) / 2, y: q.center.y)
|
|
let hit = KeyHitTesting.hitTarget(
|
|
at: mid,
|
|
targets: layout.keys,
|
|
keyPlaneBounds: layout.keyPlaneBounds,
|
|
horizontalGap: layout.horizontalGap,
|
|
verticalGap: layout.verticalGap
|
|
)
|
|
XCTAssertNotNil(hit)
|
|
XCTAssertTrue(hit?.label == "Q" || hit?.label == "W")
|
|
}
|
|
|
|
func testBehaviorResolver() {
|
|
XCTAssertEqual(TypingKeyBehaviorResolver.behavior(for: "⌫"), .deleteRepeat)
|
|
XCTAssertEqual(TypingKeyBehaviorResolver.behavior(for: "⇧"), .shiftHold)
|
|
XCTAssertEqual(TypingKeyBehaviorResolver.behavior(for: "A"), .commitOnRelease)
|
|
XCTAssertEqual(TypingKeyBehaviorResolver.behavior(for: "123"), .commitOnRelease)
|
|
}
|
|
}
|