From 2cb560df82f5293cfae7a4317e97e7abd40f1328 Mon Sep 17 00:00:00 2001 From: Melissa Vallee Date: Mon, 14 Sep 2026 08:18:07 -0400 Subject: [PATCH 1/4] fix: preserve native space trackpad feedback --- .../com/keyflow/KeyflowPressFeedbackTest.kt | 61 ++++++++++++++ .../main/java/com/keyflow/KeyflowKeyView.kt | 2 +- docs/coverage.md | 18 +++-- ios/KeyflowKeyboardView.swift | 18 ++++- scripts/ci/workflow.test.mjs | 6 ++ scripts/ios-tests/KeyflowRenderingTests.swift | 81 +++++++++++++++++++ 6 files changed, 173 insertions(+), 13 deletions(-) diff --git a/android/src/androidTest/java/com/keyflow/KeyflowPressFeedbackTest.kt b/android/src/androidTest/java/com/keyflow/KeyflowPressFeedbackTest.kt index a483629..ee7a889 100644 --- a/android/src/androidTest/java/com/keyflow/KeyflowPressFeedbackTest.kt +++ b/android/src/androidTest/java/com/keyflow/KeyflowPressFeedbackTest.kt @@ -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() + 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) diff --git a/android/src/main/java/com/keyflow/KeyflowKeyView.kt b/android/src/main/java/com/keyflow/KeyflowKeyView.kt index 45066e4..fe2b58a 100644 --- a/android/src/main/java/com/keyflow/KeyflowKeyView.kt +++ b/android/src/main/java/com/keyflow/KeyflowKeyView.kt @@ -536,7 +536,7 @@ internal class KeyflowKeyView( sliding = true touchX += steps * 12 * density onSlide?.invoke(steps) - isPressed = false + isPressed = true } } MotionEvent.ACTION_UP -> diff --git a/docs/coverage.md b/docs/coverage.md index 31c1890..9e58038 100644 --- a/docs/coverage.md +++ b/docs/coverage.md @@ -4,20 +4,20 @@ 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 six trackpad feedback cases. 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 | 46 rendering + 60 interaction cases | +| iPad | 46 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 six trackpad feedback cases. 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. @@ -25,6 +25,8 @@ Android native coverage includes layouts, input, accent interactions and press-r 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. +Trackpad regressions verify iOS legend fade entry, restoration on release during the fade, and cancellation. 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 diff --git a/ios/KeyflowKeyboardView.swift b/ios/KeyflowKeyboardView.swift index f15b11a..3dc355e 100644 --- a/ios/KeyflowKeyboardView.swift +++ b/ios/KeyflowKeyboardView.swift @@ -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) @@ -724,11 +723,23 @@ 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 @@ -736,7 +747,6 @@ final class KeyflowKeyboardView: UIView { accentSelectionIndicator.isHidden = true accentItemWidth = 0 updateAccessibleKeys() - rows.flatMap { $0 }.forEach { $0.hidesLegend = false } touchesByID.values.forEach { $0.isPressed = false } touchesByID.removeAll() originalKeysByID.removeAll() diff --git a/scripts/ci/workflow.test.mjs b/scripts/ci/workflow.test.mjs index 19f7c3d..8219966 100644 --- a/scripts/ci/workflow.test.mjs +++ b/scripts/ci/workflow.test.mjs @@ -140,6 +140,9 @@ test('device sources contain the proven inventory plus glyph and Shift regressio group === 'iosRendering' ? [ 'testPhoneDoubleShiftLocksCaseAndShowsLockGlyph', + 'testSpaceTrackpadLegendsFadeOnEntry', + 'testSpaceTrackpadReleaseRestoresLegendsDuringFade', + 'testSpaceTrackpadCancellationRestoresLegends', 'testHeldDeleteStopsOnRelease', 'testHeldDeleteStopsOnCancellation', 'testHeldDeleteStopsOutsideKey', @@ -172,6 +175,9 @@ test('device sources contain the proven inventory plus glyph and Shift regressio 'tabletShiftThenCapsRemainSelected', 'shiftedCommaDisplaysAndInsertsPlatformValue', 'shiftedPeriodDisplaysAndInsertsPlatformValue', + 'spaceTrackpadKeepsPressedColorUntilRelease', + 'raisedSpaceTrackpadKeepsPressedColor', + 'spaceTrackpadCancellationRestoresColor', 'flatPressedGlyphActuallyRenders', 'raisedPressedGlyphActuallyRenders', 'flatRestingGlyphActuallyRenders', diff --git a/scripts/ios-tests/KeyflowRenderingTests.swift b/scripts/ios-tests/KeyflowRenderingTests.swift index cbbdb18..1baf5a4 100644 --- a/scripts/ios-tests/KeyflowRenderingTests.swift +++ b/scripts/ios-tests/KeyflowRenderingTests.swift @@ -217,6 +217,87 @@ final class KeyflowRenderingTests: XCTestCase { wait(for: [stopped], timeout: 0.2) } + func testSpaceTrackpadLegendsFadeOnEntry() throws { + try withTrackpad { keyboard, touch, legends in + for legend in legends { + XCTAssertEqual(legend.alpha, 0) + let animation = try XCTUnwrap(legend.layer.animation(forKey: "opacity"), "Trackpad entry must fade instead of hiding instantly") + XCTAssertGreaterThan(animation.duration, 0) + XCTAssertLessThanOrEqual(animation.duration, 0.3) + } + var moves: [KeyflowAction] = [] + keyboard.onAction = { moves.append($0) } + touch.point.x += 24 + keyboard.touchesMoved([touch], with: nil) + XCTAssertEqual(moves, [.moveCursor(3)], "Fading must not block cursor movement") + } + } + + func testSpaceTrackpadReleaseRestoresLegendsDuringFade() throws { + try withTrackpad { keyboard, touch, legends in + keyboard.touchesEnded([touch], with: nil) + for legend in legends { XCTAssertEqual(legend.alpha, 1) } + RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.3)) + for legend in legends { XCTAssertEqual(legend.layer.presentation()?.opacity ?? legend.layer.opacity, 1, accuracy: 0.01) } + } + } + + func testSpaceTrackpadCancellationRestoresLegends() throws { + try withTrackpad { keyboard, touch, legends in + keyboard.touchesCancelled([touch], with: nil) + RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.3)) + for legend in legends { XCTAssertEqual(legend.layer.presentation()?.opacity ?? legend.layer.opacity, 1, accuracy: 0.01) } + XCTAssertFalse(keyboard.subviews.compactMap { $0 as? KeyflowKey }.contains { $0.hidesLegend }) + } + } + + private func withTrackpad(_ check: (KeyflowKeyboardView, AccentTouch, [UIView]) throws -> Void) throws { + try XCTSkipIf(UIAccessibility.isReduceMotionEnabled, "Fade timing requires standard motion settings") + let scene = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first + let window = scene.map { UIWindow(windowScene: $0) } ?? UIWindow(frame: UIScreen.main.bounds) + let controller = UIViewController() + window.rootViewController = controller + window.makeKeyAndVisible() + defer { window.isHidden = true } + let keyboard = KeyflowKeyboardView() + keyboard.updateViewport(window.bounds.size, insets: .zero) + keyboard.frame = CGRect(x: 0, y: 100, width: window.bounds.width, height: keyboard.intrinsicContentSize.height) + controller.view.addSubview(keyboard) + NSLayoutConstraint.activate([ + keyboard.leadingAnchor.constraint(equalTo: controller.view.leadingAnchor), + keyboard.trailingAnchor.constraint(equalTo: controller.view.trailingAnchor), + keyboard.topAnchor.constraint(equalTo: controller.view.topAnchor, constant: 100), + ]) + window.layoutIfNeeded() + keyboard.layoutIfNeeded() + CATransaction.flush() + RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.03)) + let keys = keyboard.subviews.compactMap { $0 as? KeyflowKey } + let letter = try XCTUnwrap(keys.first { $0.action == .text("a") }) + let shift = try XCTUnwrap(keys.first { $0.action == .shift }) + let space = try XCTUnwrap(keys.first { $0.action == .text(" ") }) + func view(_ key: KeyflowKey, _ property: String) throws -> UIView { + try XCTUnwrap(Mirror(reflecting: key).children.first { $0.label == property }?.value as? UIView) + } + var legends = try [view(letter, "label"), view(shift, "icon")] + if letter.tabletAlternate != nil { legends.append(try view(letter, "padSubtitle")) } + CATransaction.flush() + for legend in legends { XCTAssertEqual(legend.alpha, 1) } + let touch = AccentTouch() + touch.point = CGPoint(x: space.frame.midX, y: space.frame.midY) + keyboard.touchesBegan([touch], with: nil) + defer { keyboard.touchesCancelled([touch], with: nil) } + XCTAssertTrue(space.isPressed, "Space touch must enter the pressed state") + // Sample immediately after the real hold changes the legend model opacity. + let deadline = Date(timeIntervalSinceNow: 1) + while !letter.hidesLegend && Date() < deadline { + RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.005)) + } + XCTAssertTrue(letter.hidesLegend, "Space hold must activate trackpad mode") + CATransaction.flush() + try check(keyboard, touch, legends) + } + private final class AccentTouch: UITouch { var point = CGPoint.zero override func location(in view: UIView?) -> CGPoint { point } From 518b5cc089d33b14170d8590cfec08efb70688e7 Mon Sep 17 00:00:00 2001 From: Melissa Vallee Date: Mon, 14 Sep 2026 08:23:02 -0400 Subject: [PATCH 2/4] chore: remove unused fixtures and obsolete visual scripts --- example/assets/backdrops/transparency-0.png | Bin 27888 -> 0 bytes example/assets/backdrops/transparency-1.png | Bin 27849 -> 0 bytes scripts/generate-transparency-fixtures.mjs | 41 -------- scripts/verify-transparency.mjs | 54 ---------- scripts/visual/raised-surface.mjs | 103 -------------------- 5 files changed, 198 deletions(-) delete mode 100644 example/assets/backdrops/transparency-0.png delete mode 100644 example/assets/backdrops/transparency-1.png delete mode 100644 scripts/generate-transparency-fixtures.mjs delete mode 100644 scripts/verify-transparency.mjs delete mode 100644 scripts/visual/raised-surface.mjs diff --git a/example/assets/backdrops/transparency-0.png b/example/assets/backdrops/transparency-0.png deleted file mode 100644 index 6ab9b4fc57dc0c6879ab2ca2f74ad8c5fba5eff8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27888 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 diff --git a/example/assets/backdrops/transparency-1.png b/example/assets/backdrops/transparency-1.png deleted file mode 100644 index d768b388bc4e407fc3b34eb5c528964941851a43..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27849 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