Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,67 @@ class KeyflowPressFeedbackTest {
}
}

@Test fun spaceTrackpadKeepsPressedColorUntilRelease() = assertTrackpadFeedback("flat", false)

@Test fun raisedSpaceTrackpadKeepsPressedColor() = assertTrackpadFeedback("raised", false)

@Test fun spaceTrackpadCancellationRestoresColor() = assertTrackpadFeedback("flat", true)

private fun assertTrackpadFeedback(material: String, cancel: Boolean) {
ActivityScenario.launch(KeyflowTestActivity::class.java).use { scenario ->
var clicks = 0
val moves = mutableListOf<Int>()
scenario.onActivity { activity ->
val key = KeyflowKeyView(activity, "space", "") { clicks++ }
key.onSlide = { moves += it }
val host = FrameLayout(activity)
host.addView(key, FrameLayout.LayoutParams(240, 180))
activity.setContentView(host)
key.layout(0, 0, 240, 180)
key.applyTheme(
KeyflowTheme(
JSONObject()
.put("material", material)
.put("keyBackground", "#17324F")
.put("pressedKeyBackground", "#A13FC5")
)
)
val normal = pixels(key)
val down = SystemClock.uptimeMillis()
fun touch(action: Int, x: Float, y: Float = 90f) {
MotionEvent.obtain(down, SystemClock.uptimeMillis(), action, x, y, 0).also {
key.dispatchTouchEvent(it)
it.recycle()
}
}
touch(MotionEvent.ACTION_DOWN, 120f)
val pressed = pixels(key)
assertFalse(normal.sameAs(pressed))
val step = 24 * activity.resources.displayMetrics.density
for ((x, y) in listOf(120f + step to 90f, key.width + step to -20f, 120f to 90f)) {
touch(MotionEvent.ACTION_MOVE, x, y)
assertTrue("Space must remain pressed while moving the cursor", key.isPressed)
val active = pixels(key)
assertTrue(
"Trackpad must keep the configured pressed face in $material",
pressed.sameAs(active),
)
active.recycle()
}
assertTrue("The gesture must actually move the cursor", moves.isNotEmpty())
touch(if (cancel) MotionEvent.ACTION_CANCEL else MotionEvent.ACTION_UP, 120f)
assertFalse(key.isPressed)
val released = pixels(key)
assertTrue("Restore the resting color immediately", normal.sameAs(released))
normal.recycle()
pressed.recycle()
released.recycle()
}
SystemClock.sleep(100)
scenario.onActivity { assertEquals("Cursor movement must not insert a space", 0, clicks) }
}
}

private fun pixels(key: KeyflowKeyView): Bitmap =
Bitmap.createBitmap(key.width, key.height, Bitmap.Config.ARGB_8888).also {
key.background.setBounds(0, 0, key.width, key.height)
Expand Down
2 changes: 1 addition & 1 deletion android/src/main/java/com/keyflow/KeyflowKeyView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,7 @@ internal class KeyflowKeyView(
sliding = true
touchX += steps * 12 * density
onSlide?.invoke(steps)
isPressed = false
isPressed = true
}
}
MotionEvent.ACTION_UP ->
Expand Down
20 changes: 12 additions & 8 deletions docs/coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,29 @@

The device suite retains the pre-release test inventory recorded in
[`baseline-tests.json`](../scripts/ci/baseline-tests.json),
plus four Android regressions for disappearing glyphs, eight for Android modifier states, and five for iOS modifier states. The tests run against the
plus four Android regressions for disappearing glyphs, eight for Android modifier states, five for iOS modifier states, three iOS held-delete cases, and nine space/trackpad feedback cases and one iPad stationary currency-hold case. The tests run against the
current keyboard implementation. There is no optional expanded suite or runtime
filter hiding additional cases.

| Device | Required cases before platform-specific skips |
| -------------- | ------------------------------------------------ |
| Android phone | 48 original + 4 glyph + 8 Shift regressions = 60 |
| Android tablet | 48 original + 4 glyph + 8 Shift regressions = 60 |
| iPhone | 43 rendering + 60 interaction cases |
| iPad | 43 rendering + 60 interaction cases |
| Device | Required cases before platform-specific skips |
| -------------- | ------------------------------------------------------------- |
| Android phone | 48 original + 4 glyph + 8 Shift + 3 trackpad regressions = 63 |
| Android tablet | 48 original + 4 glyph + 8 Shift + 3 trackpad regressions = 63 |
| iPhone | 50 rendering + 60 interaction cases |
| iPad | 50 rendering + 60 interaction cases |

`scripts/ci/baseline-tests.json` records the original inventory for verification.
A fast workflow test checks that the actual device sources contain precisely that
inventory plus the seventeen retained glyph/modifier cases and three held-delete lifecycle cases. It does not select or skip tests.
inventory plus the seventeen retained glyph/modifier cases plus three held-delete lifecycle cases and nine space/trackpad feedback cases and one iPad stationary currency-hold case. It does not select or skip tests.

iOS coverage includes typing, symbol pages, long presses, accent selection,
number pads, tablet layouts, customization, transparency and transition diagnostics.
Android native coverage includes layouts, input, accent interactions and press-release
styling. The four additional cases verify actual visible glyph pixels in flat and
raised materials, both at rest and while pressed. Shift regressions check left/right activation, filled-arrow pixels, one-letter reset, independent Android Caps Lock/Shift activation, shifted punctuation output, and the separate automatic-capitalization state. iPad regressions verify its distinct modifier behavior and `! ?` punctuation, including accessibility labels and rendered attachments. Phone regressions check persistent uppercase and the distinct Caps Lock glyph on both platforms.

Space/trackpad regressions verify iOS touch-down fill contrast, normal release and custom-color cancellation, legend and key-face fading, restoration during an interrupted fade, and restoration after cursor dragging. Android tests compare rendered space-bar pixels during cursor movement in flat and raised materials, and verify that release or cancellation restores the resting color without inserting a space.

The newer Android React Native app automation, additional accessibility/window
suites and iOS allocation/performance/background cases were removed. Android
example-level lifecycle, transparency and transition performance are therefore
Expand Down Expand Up @@ -81,3 +83,5 @@ harness-only changes, run focused local regressions and use GitHub to validate i
runner-specific conditions. Broaden local testing when production keyboard changes
or a concrete failure warrants it. Evidence is saved under `artifacts/` and is not
published with the library.

The iPad dollar UI comparison explicitly slides into the currency popup before release: Apple's stationary dollar hold can show a highlighted choice yet commit nothing on CI. The non-empty native result and exact Keyflow output comparison remain required. A separate rendering regression preserves coverage of Keyflow's stationary dollar hold and release.
Binary file removed example/assets/backdrops/transparency-0.png
Binary file not shown.
Binary file removed example/assets/backdrops/transparency-1.png
Binary file not shown.
1 change: 1 addition & 0 deletions ios/KeyflowKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ final class KeyflowKey: UIView {
label.alpha = hidesLegend || !preview.isHidden ? 0 : (isDisabledKey ? 0.35 : 1)
padSubtitle.alpha = hidesLegend ? 0 : (isDisabledKey ? 0.35 : 1)
icon.alpha = hidesLegend ? 0 : 1
face.alpha = hidesLegend ? 0.5 : 1
}
}
var allowsPreview = true
Expand Down
18 changes: 14 additions & 4 deletions ios/KeyflowKeyboardView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -460,9 +460,8 @@ final class KeyflowKeyboardView: UIView {
guard let self, let key, touchesByID[id] === key else { return }
key.isPressed = false
if value == " " {
cursorMode = true
cursorLastX = heldOrigin.x
rows.flatMap { $0 }.forEach { $0.hidesLegend = true }
setCursorMode(true)
if hapticsEnabled { haptic.impactOccurred() }
} else {
showAccents(for: key, value: value)
Expand Down Expand Up @@ -724,19 +723,30 @@ final class KeyflowKeyboardView: UIView {
return true
}

private func setCursorMode(_ active: Bool) {
guard cursorMode != active else { return }
cursorMode = active
UIView.animate(
withDuration: UIAccessibility.isReduceMotionEnabled ? 0 : 0.2,
delay: 0,
options: [.beginFromCurrentState, .allowUserInteraction, .curveEaseOut]
) {
self.rows.flatMap { $0 }.forEach { $0.hidesLegend = active }
}
}

private func cancelTouches() {
holdWork?.cancel()
holdWork = nil
heldTouch = nil
cursorMode = false
setCursorMode(false)
accentKeys.forEach { $0.removeFromSuperview() }
accentKeys = []
selectedAccent = nil
accentCallout.isHidden = true
accentSelectionIndicator.isHidden = true
accentItemWidth = 0
updateAccessibleKeys()
rows.flatMap { $0 }.forEach { $0.hidesLegend = false }
touchesByID.values.forEach { $0.isPressed = false }
touchesByID.removeAll()
originalKeysByID.removeAll()
Expand Down
2 changes: 1 addition & 1 deletion ios/KeyflowTheme.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ struct KeyflowTheme: Decodable, Equatable {
var background = "#E0E2E7"
var keyBackground = "#FFFFFF"
var keyForeground = "#000000"
var pressedKeyBackground = "#FFFFFF"
var pressedKeyBackground = "#C1C3C6"
var selectedKeyBackground = "#008FFF"
var selectedKeyForeground = "#FFFFFF"
var specialKeyBackground = "#FFFFFF"
Expand Down
10 changes: 10 additions & 0 deletions scripts/ci/workflow.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,13 @@ test('device sources contain the proven inventory plus glyph and Shift regressio
group === 'iosRendering'
? [
'testPhoneDoubleShiftLocksCaseAndShowsLockGlyph',
'testSpacePressChangesDefaultFillAndRestoresOnRelease',
'testSpacePressRestoresCustomFillOnCancellation',
'testSpaceTrackpadSoftensFacesAndRestoresCustomColors',
'testTabletDollarHoldCommitsInitialChoiceWithoutDrag',
'testSpaceTrackpadLegendsFadeOnEntry',
'testSpaceTrackpadReleaseRestoresLegendsDuringFade',
'testSpaceTrackpadCancellationRestoresLegends',
'testHeldDeleteStopsOnRelease',
'testHeldDeleteStopsOnCancellation',
'testHeldDeleteStopsOutsideKey',
Expand Down Expand Up @@ -172,6 +179,9 @@ test('device sources contain the proven inventory plus glyph and Shift regressio
'tabletShiftThenCapsRemainSelected',
'shiftedCommaDisplaysAndInsertsPlatformValue',
'shiftedPeriodDisplaysAndInsertsPlatformValue',
'spaceTrackpadKeepsPressedColorUntilRelease',
'raisedSpaceTrackpadKeepsPressedColor',
'spaceTrackpadCancellationRestoresColor',
'flatPressedGlyphActuallyRenders',
'raisedPressedGlyphActuallyRenders',
'flatRestingGlyphActuallyRenders',
Expand Down
41 changes: 0 additions & 41 deletions scripts/generate-transparency-fixtures.mjs

This file was deleted.

15 changes: 14 additions & 1 deletion scripts/ios-tests/KeyflowQwertyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -303,10 +303,23 @@ final class KeyflowQwertyTests: XCTestCase {
key(["numbers", "123"]).tap()
let editMenu = app.descendants(matching: .any).matching(NSPredicate(format: "label == 'Select All' OR label == 'AutoFill'")).firstMatch
XCTAssertFalse(editMenu.exists && editMenu.isHittable, "Punctuation hold must start without an edit menu")
key([symbol]).press(forDuration: 1.2)
let source = key([symbol])
if tablet && symbol == "$" {
// iPadOS can highlight cents yet cancel a stationary release at the
// original dollar key. Explicitly enter the popup before releasing.
// Derive the target from the key frame, not a device-specific point.
let origin = source.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5))
let choice = source.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: -0.5))
origin.press(forDuration: 1.2, thenDragTo: choice, withVelocity: .slow, thenHoldForDuration: 0.1)
} else {
source.press(forDuration: 1.2)
}
if native {
expected = text
XCTAssertNotEqual(expected, "alpha beta", "Native hold must insert a character")
if tablet && symbol == "$" {
XCTAssertEqual(expected, "alpha beta¢", "The currency gesture must select cents, not merely type a dollar")
}
}
else { XCTAssertEqual(text, expected, "Held \(symbol) release must match Apple") }
capture("\(native ? "apple" : "keyflow")-punctuation-\(symbol.unicodeScalars.first!.value)")
Expand Down
Loading
Loading