From 280bd7c2b15057c0b3549cdcf408ad2d518941bc Mon Sep 17 00:00:00 2001 From: Asaf Mahlev Date: Thu, 11 Jun 2026 09:21:43 +0300 Subject: [PATCH 1/4] feat(spacing): signal-driven grace duration + live tuning knobs (#24) Phase 2 of the spacing-policy epic (#14). The combining grace-timer duration now adapts to the #92 word-state signals instead of a fixed value: graceMs = clamp(base - completeBonus*complete + prefixPenalty*prefixRichScore, 100, 3000) A finished dictionary word commits sooner; an extendable prefix-rich stem waits longer. Wired into enterCombiningMode behind the default-off experimental flag PREF_SPACING_SIGNAL_DRIVEN_GRACE; the formula is a pure static helper (signalDrivenGraceMs) for testability. Live tuning knobs (#26), all experimental on the Two-thumb screen: - PREF_SPACING_SIGNAL_DRIVEN_GRACE (toggle, default off) - PREF_SPACING_COMPLETE_BONUS_MS (slider, default 200) - PREF_SPACING_PREFIX_PENALTY_MS (slider, default 400) base = the existing PREF_COMBINING_GRACE_MS; min/max clamp hardcoded. Verify: SpacingSignalsTest (+6 graceMs cases) / SettingsContainerTest / InputLogicTest -> 128 completed, 3 failed (pre-existing baseline), 1 skipped; 0 new failures. Feel/tuning is on-device. --- .../keyboard/latin/inputlogic/InputLogic.java | 24 ++++++++++++++++- .../keyboard/latin/settings/Defaults.kt | 3 +++ .../keyboard/latin/settings/Settings.java | 5 ++++ .../latin/settings/SettingsValues.java | 10 +++++++ .../settings/screens/TwoThumbTypingScreen.kt | 27 +++++++++++++++++++ app/src/main/res/values/strings.xml | 7 +++++ .../latin/inputlogic/SpacingSignalsTest.kt | 23 ++++++++++++++++ .../settings/SettingsContainerTest.kt | 10 +++++++ 8 files changed, 108 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java index 834cc1561..8496e125f 100644 --- a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java +++ b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java @@ -990,7 +990,14 @@ private void enterCombiningMode(final SettingsValues settingsValues, final boole // separators / cursor-front recompositions there's nothing to auto-commit, and arming // the timer would draw a spurious progress bar. if (!mWordComposer.isComposingWord()) return; - final int graceMs = baseGraceMs + Math.max(0, settingsValues.mCombiningTapExtraMs); + final int graceMs; + if (settingsValues.mSpacingSignalDrivenGrace) { + // #24: vary the grace duration by the per-keystroke word-state signals. + graceMs = signalDrivenGraceMs(baseGraceMs, settingsValues.mSpacingCompleteBonusMs, + settingsValues.mSpacingPrefixPenaltyMs, mSpacingComplete, mSpacingPrefixRichScore); + } else { + graceMs = baseGraceMs + Math.max(0, settingsValues.mCombiningTapExtraMs); + } cancelCombiningTimerOnly(); mInCombiningMode = true; // #14 "only auto-finish swiped words": still ENTER combining mode (so a following swipe @@ -1461,6 +1468,21 @@ static SpacingSignals computeSpacingSignals(final SuggestedWords suggestedWords) return new SpacingSignals(complete, (float) completions / n); } + private static final int SIGNAL_GRACE_MIN_MS = 100; + private static final int SIGNAL_GRACE_MAX_MS = 3000; + + /** + * #24 signal-driven grace duration: a confident complete word commits sooner (subtract + * {@code completeBonus}), while an extendable prefix-rich stem waits longer (add + * {@code prefixPenalty} scaled by the score), clamped to a sane range. Pure for testability. + */ + static int signalDrivenGraceMs(final int baseMs, final int completeBonusMs, + final int prefixPenaltyMs, final boolean complete, final float prefixRichScore) { + final int ms = baseMs - (complete ? completeBonusMs : 0) + + Math.round(prefixPenaltyMs * prefixRichScore); + return Math.max(SIGNAL_GRACE_MIN_MS, Math.min(SIGNAL_GRACE_MAX_MS, ms)); + } + /** * Handle a consumed event. *

diff --git a/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt b/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt index c0e875464..5ace6111b 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt +++ b/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt @@ -151,6 +151,9 @@ object Defaults { const val PREF_COMBINING_AUTOSPACE_ONLY_AFTER_GESTURE = false const val PREF_SPACING_DEFER_GRACE_SPACE = false const val PREF_COMBINING_GRACE_ONLY_AFTER_GESTURE = true // default on: tapped words shouldn't auto-finish + const val PREF_SPACING_SIGNAL_DRIVEN_GRACE = false + const val PREF_SPACING_COMPLETE_BONUS_MS = 200 // complete word commits this much sooner + const val PREF_SPACING_PREFIX_PENALTY_MS = 400 // max extra wait when fully prefix-rich const val PREF_COMBINING_AUTOSPACE_SUGGESTIONS = "alternatives_then_next_word" const val PREF_COMBINING_BACKSPACE_DELETES_GESTURE_WORD = true const val PREF_COMBINING_BACKSPACE_DELETES_COMPOSING_TEXT = true diff --git a/app/src/main/java/helium314/keyboard/latin/settings/Settings.java b/app/src/main/java/helium314/keyboard/latin/settings/Settings.java index cf89a5379..abbacc1bc 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/Settings.java +++ b/app/src/main/java/helium314/keyboard/latin/settings/Settings.java @@ -163,6 +163,11 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang // #14: when on, the combining grace timer only auto-commits words that include a swipe — // pure tap-typed words are never auto-finished by the timer. Experimental, default off. public static final String PREF_COMBINING_GRACE_ONLY_AFTER_GESTURE = "combining_grace_only_after_gesture"; + // #14/#24 signal-driven grace: vary the grace-timer duration by the per-keystroke word-state + // signals (complete / prefix-richness) instead of a fixed value. Experimental, default off. + public static final String PREF_SPACING_SIGNAL_DRIVEN_GRACE = "spacing_signal_driven_grace"; + public static final String PREF_SPACING_COMPLETE_BONUS_MS = "spacing_complete_bonus_ms"; + public static final String PREF_SPACING_PREFIX_PENALTY_MS = "spacing_prefix_penalty_ms"; // What the suggestion strip shows after the combining grace timer auto-commits a word. // Values: "keep_alternatives" (1) | "next_word" (2, default) | "alternatives_then_next_word" (3). public static final String PREF_COMBINING_AUTOSPACE_SUGGESTIONS = "combining_autospace_suggestions"; diff --git a/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java b/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java index 95efd7756..6a93b1349 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java +++ b/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java @@ -139,6 +139,9 @@ public class SettingsValues { public final boolean mCombiningAutospaceOnlyAfterGesture; public final boolean mSpacingDeferGraceSpace; public final boolean mCombiningGraceOnlyAfterGesture; + public final boolean mSpacingSignalDrivenGrace; + public final int mSpacingCompleteBonusMs; + public final int mSpacingPrefixPenaltyMs; // Raw string value: "keep_alternatives" | "next_word" | "alternatives_then_next_word" public final String mCombiningAutospaceSuggestions; public final boolean mCombiningBackspaceDeletesGestureWord; @@ -390,6 +393,13 @@ public SettingsValues(final Context context, final SharedPreferences prefs, fina mCombiningGraceOnlyAfterGesture = prefs.getBoolean( Settings.PREF_COMBINING_GRACE_ONLY_AFTER_GESTURE, Defaults.PREF_COMBINING_GRACE_ONLY_AFTER_GESTURE); + mSpacingSignalDrivenGrace = prefs.getBoolean( + Settings.PREF_SPACING_SIGNAL_DRIVEN_GRACE, + Defaults.PREF_SPACING_SIGNAL_DRIVEN_GRACE); + mSpacingCompleteBonusMs = prefs.getInt(Settings.PREF_SPACING_COMPLETE_BONUS_MS, + Defaults.PREF_SPACING_COMPLETE_BONUS_MS); + mSpacingPrefixPenaltyMs = prefs.getInt(Settings.PREF_SPACING_PREFIX_PENALTY_MS, + Defaults.PREF_SPACING_PREFIX_PENALTY_MS); mCombiningAutospaceSuggestions = prefs.getString(Settings.PREF_COMBINING_AUTOSPACE_SUGGESTIONS, Defaults.PREF_COMBINING_AUTOSPACE_SUGGESTIONS); final boolean nonNormalTwoThumbSpacing = mGestureManualSpacing || mCombiningGraceMs > 0; diff --git a/app/src/main/java/helium314/keyboard/settings/screens/TwoThumbTypingScreen.kt b/app/src/main/java/helium314/keyboard/settings/screens/TwoThumbTypingScreen.kt index e73389788..ea131f980 100644 --- a/app/src/main/java/helium314/keyboard/settings/screens/TwoThumbTypingScreen.kt +++ b/app/src/main/java/helium314/keyboard/settings/screens/TwoThumbTypingScreen.kt @@ -71,6 +71,9 @@ fun TwoThumbTypingScreen( add(Settings.PREF_COMBINING_AUTOSPACE_SUGGESTIONS) add(Settings.PREF_SPACING_DEFER_GRACE_SPACE) add(Settings.PREF_COMBINING_GRACE_ONLY_AFTER_GESTURE) + add(Settings.PREF_SPACING_SIGNAL_DRIVEN_GRACE) + add(Settings.PREF_SPACING_COMPLETE_BONUS_MS) + add(Settings.PREF_SPACING_PREFIX_PENALTY_MS) } if (nonNormalSpacing) { add(Settings.PREF_MULTIPART_FULL_WORD_SUGGESTIONS) @@ -158,6 +161,30 @@ fun createTwoThumbTypingSettings(context: Context) = listOf( R.string.combining_grace_only_after_gesture_summary) { SwitchPreference(it, Defaults.PREF_COMBINING_GRACE_ONLY_AFTER_GESTURE) }, + Setting(context, Settings.PREF_SPACING_SIGNAL_DRIVEN_GRACE, + R.string.spacing_signal_driven_grace, R.string.spacing_signal_driven_grace_summary) { + SwitchPreference(it, Defaults.PREF_SPACING_SIGNAL_DRIVEN_GRACE) + }, + Setting(context, Settings.PREF_SPACING_COMPLETE_BONUS_MS, + R.string.spacing_complete_bonus, R.string.spacing_complete_bonus_summary) { def -> + SliderPreference( + name = def.title, + key = def.key, + default = Defaults.PREF_SPACING_COMPLETE_BONUS_MS, + range = 0f..1000f, + description = { stringResource(R.string.abbreviation_unit_milliseconds, it.toString()) } + ) + }, + Setting(context, Settings.PREF_SPACING_PREFIX_PENALTY_MS, + R.string.spacing_prefix_penalty, R.string.spacing_prefix_penalty_summary) { def -> + SliderPreference( + name = def.title, + key = def.key, + default = Defaults.PREF_SPACING_PREFIX_PENALTY_MS, + range = 0f..1500f, + description = { stringResource(R.string.abbreviation_unit_milliseconds, it.toString()) } + ) + }, Setting(context, Settings.PREF_COMBINING_AUTOSPACE_SUGGESTIONS, R.string.combining_autospace_suggestions, R.string.combining_autospace_suggestions_summary) { def -> val items = listOf( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 82e563c1c..66bc81de3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -303,6 +303,13 @@ Only auto-finish swiped words The pause timer auto-commits a word only when it includes a swipe. Words you tap out stay open until you press space or pick a suggestion, so tapped shortcuts and corrections won\'t fire early. On by default \u2014 this controls whether the word commits (the auto-space option above only controls the trailing space). + + Adapt pause to the word (experimental) + Vary the auto-finish pause by what you\'re typing: a finished dictionary word commits sooner, while a stem that many longer words start with waits longer. Tune the two amounts below. + Finished-word speed-up + How much sooner a complete dictionary word auto-finishes. + Extendable-stem patience + Extra wait when many longer words start with what you\'ve typed (so you can keep going). Backspace deletes last swipe diff --git a/app/src/test/java/helium314/keyboard/latin/inputlogic/SpacingSignalsTest.kt b/app/src/test/java/helium314/keyboard/latin/inputlogic/SpacingSignalsTest.kt index e3296d37d..56844c653 100644 --- a/app/src/test/java/helium314/keyboard/latin/inputlogic/SpacingSignalsTest.kt +++ b/app/src/test/java/helium314/keyboard/latin/inputlogic/SpacingSignalsTest.kt @@ -66,4 +66,27 @@ class SpacingSignalsTest { assertEquals(0f, InputLogic.computeSpacingSignals(words(typed, true, listOf(typed))).prefixRichScore, 0f) } + + // ---- signalDrivenGraceMs ---- + + @Test fun `signal grace is base when neutral`() { + assertEquals(800, InputLogic.signalDrivenGraceMs(800, 200, 400, false, 0f)) + } + + @Test fun `complete word shortens grace`() { + assertEquals(600, InputLogic.signalDrivenGraceMs(800, 200, 400, true, 0f)) + } + + @Test fun `prefix-rich stem lengthens grace`() { + assertEquals(1000, InputLogic.signalDrivenGraceMs(800, 200, 400, false, 0.5f)) // 800 + 400*0.5 + } + + @Test fun `complete and prefix-rich combine`() { + assertEquals(800, InputLogic.signalDrivenGraceMs(800, 200, 400, true, 0.5f)) // 800 - 200 + 200 + } + + @Test fun `grace clamps to the floor and ceiling`() { + assertEquals(100, InputLogic.signalDrivenGraceMs(150, 200, 0, true, 0f)) // -50 -> 100 + assertEquals(3000, InputLogic.signalDrivenGraceMs(2900, 0, 400, false, 1f)) // 3300 -> 3000 + } } diff --git a/app/src/test/java/helium314/keyboard/settings/SettingsContainerTest.kt b/app/src/test/java/helium314/keyboard/settings/SettingsContainerTest.kt index 30451afb7..f7d0e3786 100644 --- a/app/src/test/java/helium314/keyboard/settings/SettingsContainerTest.kt +++ b/app/src/test/java/helium314/keyboard/settings/SettingsContainerTest.kt @@ -72,6 +72,16 @@ class SettingsContainerTest { container[Settings.PREF_COMBINING_GRACE_ONLY_AFTER_GESTURE]?.key) } + @Test + fun signalDrivenGraceSettingsAreRegistered() { + assertEquals(Settings.PREF_SPACING_SIGNAL_DRIVEN_GRACE, + container[Settings.PREF_SPACING_SIGNAL_DRIVEN_GRACE]?.key) + assertEquals(Settings.PREF_SPACING_COMPLETE_BONUS_MS, + container[Settings.PREF_SPACING_COMPLETE_BONUS_MS]?.key) + assertEquals(Settings.PREF_SPACING_PREFIX_PENALTY_MS, + container[Settings.PREF_SPACING_PREFIX_PENALTY_MS]?.key) + } + @Test fun twoThumbLowLevelBackspaceSettingIsHiddenFromSearchRegistry() { assertNull(container[Settings.PREF_COMBINING_BACKSPACE_DELETES_GESTURE_WORD]) From bebb1d14b29d5685d5cf7fb90eea5eadecc4eeb8 Mon Sep 17 00:00:00 2001 From: Asaf Mahlev Date: Thu, 11 Jun 2026 09:55:01 +0300 Subject: [PATCH 2/4] feat(spacing/a11): live spacing-policy signal readout overlay (#A11) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add SpacingInsightDrawingPreview — a debug HUD drawn on the DrawingPreviewPlacerView that shows the per-keystroke spacing-policy signals at combining-mode arm time: spacing | c:Y px:72% g:380ms [timer] Fields: c – complete (Y/N): typed stem is a dictionary word px – prefix-rich score (0-100%): fraction of suggestions that are completions of the current stem g – resolved graceMs after signal-driven adjustment gate – active commit gate; "timer" for the current single-timer model; reserved for the two-gate branch (see updateGate() integration hook) Gating: co-gated behind PREF_GESTURE_DEBUG_DRAW_POINTS (the existing gesture debug setting). No new preference. Integration points: DrawingProxy.setSpacingInsight(complete, prefixRichScore, graceMs, gate) – called from InputLogic.enterCombiningMode on each arm, and cleared on cancelCombiningMode / onCombiningGraceExpired. SpacingInsightDrawingPreview.updateGate(gate) – gate branch calls this once its gate decision is made to re-stamp the gate label without resetting the signal snapshot. Hot-path: string is built once per keystroke commit (not per frame); drawPreview() is allocation-free. Enabled flag check short-circuits both paths when the debug toggle is off. --- .../keyboard/keyboard/MainKeyboardView.java | 20 +- .../keyboard/internal/DrawingProxy.java | 18 ++ .../SpacingInsightDrawingPreview.java | 183 ++++++++++++++++++ .../keyboard/latin/inputlogic/InputLogic.java | 12 +- 4 files changed, 229 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/helium314/keyboard/keyboard/internal/SpacingInsightDrawingPreview.java diff --git a/app/src/main/java/helium314/keyboard/keyboard/MainKeyboardView.java b/app/src/main/java/helium314/keyboard/keyboard/MainKeyboardView.java index 96399c446..09d662959 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/MainKeyboardView.java +++ b/app/src/main/java/helium314/keyboard/keyboard/MainKeyboardView.java @@ -48,6 +48,7 @@ import helium314.keyboard.keyboard.internal.PopupKeySpec; import helium314.keyboard.keyboard.internal.NonDistinctMultitouchHelper; import helium314.keyboard.keyboard.internal.SlidingKeyInputDrawingPreview; +import helium314.keyboard.keyboard.internal.SpacingInsightDrawingPreview; import helium314.keyboard.keyboard.internal.TimerHandler; import helium314.keyboard.keyboard.internal.KeyboardIconsSet; import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode; @@ -126,6 +127,8 @@ public final class MainKeyboardView extends KeyboardView implements DrawingProxy private final SlidingKeyInputDrawingPreview mSlidingKeyInputDrawingPreview; // Debug overlay for two-thumb point hinting (#2.1), toggled by PREF_GESTURE_DEBUG_DRAW_POINTS. private final GestureDebugPointsDrawingPreview mGestureDebugPointsDrawingPreview; + // Spacing-policy signal readout (#A11), co-gated by PREF_GESTURE_DEBUG_DRAW_POINTS. + private final SpacingInsightDrawingPreview mSpacingInsightDrawingPreview; // Key preview private final KeyPreviewDrawParams mKeyPreviewDrawParams; @@ -233,6 +236,8 @@ public MainKeyboardView(final Context context, final AttributeSet attrs, final i // Debug overlay last so it draws ON TOP of the gesture trail / floating preview. mGestureDebugPointsDrawingPreview = new GestureDebugPointsDrawingPreview(); mGestureDebugPointsDrawingPreview.setDrawingView(drawingPreviewPlacerView); + mSpacingInsightDrawingPreview = new SpacingInsightDrawingPreview(); + mSpacingInsightDrawingPreview.setDrawingView(drawingPreviewPlacerView); mainKeyboardViewAttr.recycle(); mDrawingPreviewPlacerView = drawingPreviewPlacerView; @@ -518,8 +523,9 @@ private void setGesturePreviewMode(final boolean isGestureTrailEnabled, mGestureTrailsDrawingPreview.setPreviewEnabled(isGestureTrailEnabled); // The debug overlay tracks its own pref and is independent of the user-visible trail — // enable the preview whenever the pref is on so the drawing pass actually runs. - mGestureDebugPointsDrawingPreview.setPreviewEnabled( - Settings.getValues().mGestureDebugDrawPoints); + final boolean debugEnabled = Settings.getValues().mGestureDebugDrawPoints; + mGestureDebugPointsDrawingPreview.setPreviewEnabled(debugEnabled); + mSpacingInsightDrawingPreview.setPreviewEnabled(debugEnabled); } public void showGestureFloatingPreviewText(@NonNull final SuggestedWords suggestedWords, @@ -588,6 +594,16 @@ public void setGestureCommitPending(final boolean pending) { mGestureFloatingTextDrawingPreview.setCommitPending(pending); } + // Implements {@link DrawingProxy#setSpacingInsight} (#A11). The readout is co-gated by + // PREF_GESTURE_DEBUG_DRAW_POINTS so there are no new settings to expose. + @Override + public void setSpacingInsight(final boolean complete, final float prefixRichScore, + final int graceMs, @Nullable final String gate) { + if (!Settings.getValues().mGestureDebugDrawPoints) return; + locatePreviewPlacerView(); + mSpacingInsightDrawingPreview.update(complete, prefixRichScore, graceMs, gate); + } + // Note that this method is called from a non-UI thread. @SuppressWarnings("static-method") public void setMainDictionaryAvailability(final boolean mainDictionaryAvailable) { diff --git a/app/src/main/java/helium314/keyboard/keyboard/internal/DrawingProxy.java b/app/src/main/java/helium314/keyboard/keyboard/internal/DrawingProxy.java index 731d4df51..7d79f05e3 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/internal/DrawingProxy.java +++ b/app/src/main/java/helium314/keyboard/keyboard/internal/DrawingProxy.java @@ -103,4 +103,22 @@ void setGestureDebugPoints(@NonNull helium314.keyboard.latin.common.InputPointer * normal commit / cancel / continuation. */ void setGestureCommitPending(boolean pending); + + /** + * Push a spacing-policy signal snapshot to the debug overlay (#A11), gated behind + * {@code PREF_GESTURE_DEBUG_DRAW_POINTS}. Called from {@code InputLogic} each time the + * combining-mode timer is armed or cleared. + * + *

Implementations must guard on the debug-draw setting internally; calling with + * the setting off must be a cheap no-op. + * + * @param complete whether the current typed stem is a dictionary word + * @param prefixRichScore fraction of suggestions that are prefix-completions [0..1] + * @param graceMs resolved grace duration (ms); {@code <= 0} clears the overlay + * @param gate active gate label; pass {@code null} to use the default + * ({@value SpacingInsightDrawingPreview#GATE_TIMER}). The two-gate + * branch passes its own label here. + */ + void setSpacingInsight(boolean complete, float prefixRichScore, int graceMs, + @Nullable String gate); } diff --git a/app/src/main/java/helium314/keyboard/keyboard/internal/SpacingInsightDrawingPreview.java b/app/src/main/java/helium314/keyboard/keyboard/internal/SpacingInsightDrawingPreview.java new file mode 100644 index 000000000..f4295151c --- /dev/null +++ b/app/src/main/java/helium314/keyboard/keyboard/internal/SpacingInsightDrawingPreview.java @@ -0,0 +1,183 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + */ + +package helium314.keyboard.keyboard.internal; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Typeface; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import helium314.keyboard.keyboard.PointerTracker; +import helium314.keyboard.latin.common.CoordinateUtils; + +/** + * Debug overlay (#A11) that shows the spacing-policy signals for the most recent combining-mode + * arm, drawn in the bottom-left corner of the keyboard area on top of all other previews. + * + *

Gated behind {@code PREF_GESTURE_DEBUG_DRAW_POINTS}; no new preference required. + * + *

Displayed fields: + *

+ * + *

The snapshot string is built once per signal update (in {@link #update}), not per draw + * frame, so the drawing path is allocation-free. + * + *

Integration point for the gate branch: pass a non-null {@code gate} string to + * {@link #update} (e.g. {@code "timer"}, {@code "two-gate"}) or call {@link #updateGate} to + * re-stamp only the gate label without resetting the other signals. + */ +public final class SpacingInsightDrawingPreview extends AbstractDrawingPreview { + + private static final float TEXT_SIZE_SP = 11f; // scaled in setKeyboardViewGeometry + private static final float PADDING_PX = 6f; + private static final int BG_COLOR = 0xCC1A1A2E; // dark navy, 80% opaque + private static final int TEXT_COLOR = 0xFFD0E8FF; // soft blue-white + private static final int LABEL_COLOR = 0xFF90B8D8; // dimmer for key labels + + /** Gate label used when the caller passes {@code null}. */ + public static final String GATE_TIMER = "timer"; + /** Sentinel gate label for "no gate / idle". */ + public static final String GATE_NONE = "none"; + + private final Paint mBgPaint = new Paint(); + private final Paint mTextPaint = new Paint(); + private final Paint mLabelPaint = new Paint(); + + // Keyboard area from setKeyboardViewGeometry – used for corner positioning. + private int mKeyboardWidth; + private int mKeyboardHeight; + + // Raw signal fields retained so gate branch can call updateGate() without re-supplying all. + private boolean mComplete; + private float mPrefixRichScore; + private int mGraceMs; + @Nullable private String mGate; + + /** + * Pre-formatted snapshot string; {@code null} means no active combining-mode arm + * (overlay draws nothing). + */ + @Nullable private String mSnapshot; + + public SpacingInsightDrawingPreview() { + mBgPaint.setStyle(Paint.Style.FILL); + mBgPaint.setColor(BG_COLOR); + + mTextPaint.setAntiAlias(true); + mTextPaint.setTypeface(Typeface.MONOSPACE); + mTextPaint.setColor(TEXT_COLOR); + mTextPaint.setTextSize(TEXT_SIZE_SP * 2.5f); // rough default; refined in setKeyboardViewGeometry + + mLabelPaint.setAntiAlias(true); + mLabelPaint.setTypeface(Typeface.MONOSPACE); + mLabelPaint.setColor(LABEL_COLOR); + mLabelPaint.setTextSize(TEXT_SIZE_SP * 2.5f); + } + + @Override + public void setKeyboardViewGeometry(@NonNull final int[] originCoords, + final int width, final int height) { + super.setKeyboardViewGeometry(originCoords, width, height); + mKeyboardWidth = width; + mKeyboardHeight = height; + // Scale text relative to keyboard height so it stays readable at any DPI. + final float textPx = Math.max(24f, height * 0.045f); + mTextPaint.setTextSize(textPx); + mLabelPaint.setTextSize(textPx); + } + + /** + * Push a new signal snapshot. Call from {@code InputLogic} each time the combining-mode + * timer is armed. Call with {@code graceMs <= 0} to clear (e.g. on commit/cancel). + * + * @param complete whether the current stem is a dictionary word + * @param prefixRichScore fraction of suggestions that are prefix-completions [0..1] + * @param graceMs resolved grace duration (ms); {@code <= 0} clears the overlay + * @param gate active gate label; {@code null} renders as {@value #GATE_TIMER} + */ + public void update(final boolean complete, final float prefixRichScore, + final int graceMs, @Nullable final String gate) { + if (graceMs <= 0) { + mSnapshot = null; + invalidateDrawingView(); + return; + } + mComplete = complete; + mPrefixRichScore = prefixRichScore; + mGraceMs = graceMs; + mGate = gate; + mSnapshot = buildSnapshot(complete, prefixRichScore, graceMs, gate); + invalidateDrawingView(); + } + + /** + * Re-stamp only the gate label on the current snapshot without resetting the signal + * fields. No-op if there is no active snapshot (no live combining-mode arm). + * + *

This is the integration hook for the gate branch: call it as soon as the gate + * decision is made to update the readout without waiting for the next keystroke. + * + * @param gate new gate label; {@code null} falls back to {@value #GATE_TIMER} + */ + public void updateGate(@Nullable final String gate) { + if (mSnapshot == null) return; + mGate = gate; + mSnapshot = buildSnapshot(mComplete, mPrefixRichScore, mGraceMs, gate); + invalidateDrawingView(); + } + + private static String buildSnapshot(final boolean complete, final float prefixRichScore, + final int graceMs, @Nullable final String gate) { + // Avoid String.format for the numeric fields to reduce alloc pressure; still called + // only once per keystroke/gesture commit (not per frame), so a bit of string work here + // is fine. + final int prefixPct = Math.round(prefixRichScore * 100f); + return "spacing | c:" + (complete ? "Y" : "N") + + " px:" + prefixPct + "%" + + " g:" + graceMs + "ms" + + " [" + (gate != null ? gate : GATE_TIMER) + "]"; + } + + @Override + public void drawPreview(@NonNull final Canvas canvas) { + if (!isPreviewEnabled()) return; + final String snap = mSnapshot; + if (snap == null) return; + + final float textH = mTextPaint.getTextSize(); + final float textW = mTextPaint.measureText(snap); + final float padH = PADDING_PX; + final float padV = PADDING_PX; + + // Position: bottom-left of the keyboard area with a small margin. + final float left = padH; + final float top = mKeyboardHeight - textH - padV * 2f; + final float right = left + textW + padH * 2f; + final float bottom = mKeyboardHeight - padV * 0.4f; + + canvas.drawRoundRect(left, top, right, bottom, 4f, 4f, mBgPaint); + canvas.drawText(snap, left + padH, bottom - padV * 0.6f, mTextPaint); + } + + @Override + public void onDeallocateMemory() { + mSnapshot = null; + } + + @Override + public void setPreviewPosition(@NonNull final PointerTracker tracker) { + // Position is fixed (bottom-left of keyboard) — no tracker tracking needed. + } +} diff --git a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java index 8496e125f..5f0257c93 100644 --- a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java +++ b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java @@ -1021,6 +1021,8 @@ private void enterCombiningMode(final SettingsValues settingsValues, final boole && !mSuppressAutospaceForForceNextSpace; kv.setCombiningMode(showAutospaceIndicator, startTime, graceMs, true /* compositionActiveForDebug */); + // #A11: push spacing-policy signals to the debug overlay. + kv.setSpacingInsight(mSpacingComplete, mSpacingPrefixRichScore, graceMs, null); } } @@ -1144,7 +1146,10 @@ void cancelCombiningMode() { if (mInCombiningMode) { mInCombiningMode = false; final MainKeyboardView kv = KeyboardSwitcher.getInstance().getMainKeyboardView(); - if (kv != null) kv.setCombiningMode(false, 0L, 0); + if (kv != null) { + kv.setCombiningMode(false, 0L, 0); + kv.setSpacingInsight(false, 0f, 0, null); // clear the #A11 readout + } } } @@ -1198,7 +1203,10 @@ private void onCombiningGraceExpired() { mPendingCombiningCommit = null; mInCombiningMode = false; final MainKeyboardView kv = KeyboardSwitcher.getInstance().getMainKeyboardView(); - if (kv != null) kv.setCombiningMode(false, 0L, 0); + if (kv != null) { + kv.setCombiningMode(false, 0L, 0); + kv.setSpacingInsight(false, 0f, 0, null); // clear the #A11 readout + } final SettingsValues sv = Settings.getInstance().getCurrent(); if (!mWordComposer.isComposingWord()) return; // Capture whether the word being committed by this timer came from a gesture. We From 303fcc1da266a05edac0402c5c3d287824570555 Mon Sep 17 00:00:00 2001 From: Asaf Mahlev Date: Thu, 11 Jun 2026 11:02:45 +0300 Subject: [PATCH 3/4] docs(spacing): practical playtest plan; make HUD decision-first The first spacing HUD exposed raw telemetry (c/px/g), which was hard to read while typing and disappeared too quickly. Make it practical: - decision-first labels: FAST / WAIT / TIMER / INSTANT - detail line explains finished-word vs many-continuations - linger briefly after commit/cancel so the user can read it - inset/two-line box to avoid clipping Add docs/SPACING_TEST_PLAN.md with concrete playtest scenarios for complete words, extendable stems, Adapt on/off, shortcuts, corrections, and punctuation. Verification: :app:compileStandardDebugJavaWithJavac -> BUILD SUCCESSFUL. --- .../SpacingInsightDrawingPreview.java | 119 +++++++++------- docs/SPACING_TEST_PLAN.md | 129 ++++++++++++++++++ 2 files changed, 201 insertions(+), 47 deletions(-) create mode 100644 docs/SPACING_TEST_PLAN.md diff --git a/app/src/main/java/helium314/keyboard/keyboard/internal/SpacingInsightDrawingPreview.java b/app/src/main/java/helium314/keyboard/keyboard/internal/SpacingInsightDrawingPreview.java index f4295151c..e9aab0b72 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/internal/SpacingInsightDrawingPreview.java +++ b/app/src/main/java/helium314/keyboard/keyboard/internal/SpacingInsightDrawingPreview.java @@ -8,6 +8,7 @@ import android.graphics.Color; import android.graphics.Paint; import android.graphics.Typeface; +import android.os.SystemClock; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -21,14 +22,11 @@ * *

Gated behind {@code PREF_GESTURE_DEBUG_DRAW_POINTS}; no new preference required. * - *

Displayed fields: + *

Displayed as decision-first, human-readable text: *

* *

The snapshot string is built once per signal update (in {@link #update}), not per draw @@ -40,12 +38,12 @@ */ public final class SpacingInsightDrawingPreview extends AbstractDrawingPreview { - private static final float TEXT_SIZE_SP = 11f; // scaled in setKeyboardViewGeometry - private static final float PADDING_PX = 6f; - private static final int BG_COLOR = 0xCC1A1A2E; // dark navy, 80% opaque - private static final int TEXT_COLOR = 0xFFD0E8FF; // soft blue-white - private static final int LABEL_COLOR = 0xFF90B8D8; // dimmer for key labels - + private static final float TEXT_SIZE_SP = 11f; // scaled in setKeyboardViewGeometry + private static final float PADDING_PX = 8f; + private static final long LINGER_AFTER_CLEAR = 1800L; // keep visible long enough to read + private static final int BG_COLOR = 0xDD1A1A2E; // dark navy, ~87% opaque + private static final int TEXT_COLOR = 0xFFFFFFFF; // primary line + private static final int LABEL_COLOR = 0xFFD0E8FF; // detail line /** Gate label used when the caller passes {@code null}. */ public static final String GATE_TIMER = "timer"; /** Sentinel gate label for "no gate / idle". */ @@ -65,11 +63,10 @@ public final class SpacingInsightDrawingPreview extends AbstractDrawingPreview { private int mGraceMs; @Nullable private String mGate; - /** - * Pre-formatted snapshot string; {@code null} means no active combining-mode arm - * (overlay draws nothing). - */ - @Nullable private String mSnapshot; + /** Primary/detail readout lines; {@code null} primary means overlay draws nothing. */ + @Nullable private String mPrimary; + @Nullable private String mDetail; + private long mVisibleUntilMs; public SpacingInsightDrawingPreview() { mBgPaint.setStyle(Paint.Style.FILL); @@ -110,15 +107,19 @@ public void setKeyboardViewGeometry(@NonNull final int[] originCoords, public void update(final boolean complete, final float prefixRichScore, final int graceMs, @Nullable final String gate) { if (graceMs <= 0) { - mSnapshot = null; - invalidateDrawingView(); + // Don't disappear immediately on commit/cancel — leave the last decision readable. + if (mPrimary != null) { + mVisibleUntilMs = SystemClock.uptimeMillis() + LINGER_AFTER_CLEAR; + invalidateDrawingView(); + } return; } mComplete = complete; mPrefixRichScore = prefixRichScore; mGraceMs = graceMs; mGate = gate; - mSnapshot = buildSnapshot(complete, prefixRichScore, graceMs, gate); + buildSnapshot(complete, prefixRichScore, graceMs, gate); + mVisibleUntilMs = SystemClock.uptimeMillis() + Math.max(LINGER_AFTER_CLEAR, graceMs + 800L); invalidateDrawingView(); } @@ -132,48 +133,72 @@ public void update(final boolean complete, final float prefixRichScore, * @param gate new gate label; {@code null} falls back to {@value #GATE_TIMER} */ public void updateGate(@Nullable final String gate) { - if (mSnapshot == null) return; - mGate = gate; - mSnapshot = buildSnapshot(mComplete, mPrefixRichScore, mGraceMs, gate); + if (mPrimary == null) return; + mGate = gate; + buildSnapshot(mComplete, mPrefixRichScore, mGraceMs, gate); + mVisibleUntilMs = SystemClock.uptimeMillis() + Math.max(LINGER_AFTER_CLEAR, mGraceMs + 800L); invalidateDrawingView(); } - private static String buildSnapshot(final boolean complete, final float prefixRichScore, + private void buildSnapshot(final boolean complete, final float prefixRichScore, final int graceMs, @Nullable final String gate) { - // Avoid String.format for the numeric fields to reduce alloc pressure; still called - // only once per keystroke/gesture commit (not per frame), so a bit of string work here - // is fine. final int prefixPct = Math.round(prefixRichScore * 100f); - return "spacing | c:" + (complete ? "Y" : "N") - + " px:" + prefixPct + "%" - + " g:" + graceMs + "ms" - + " [" + (gate != null ? gate : GATE_TIMER) + "]"; + final String gateLabel = gate == null ? GATE_TIMER : gate; + + if ("instant".equals(gateLabel)) { + mPrimary = "INSTANT"; + mDetail = "finished word + low prefix"; + } else if ("pause".equals(gateLabel)) { + mPrimary = "WAIT " + graceMs + "ms"; + mDetail = "many continuations · px " + prefixPct + "%"; + } else if (complete) { + mPrimary = "FAST " + graceMs + "ms"; + mDetail = "finished word · px " + prefixPct + "%"; + } else if (prefixRichScore >= 0.50f) { + mPrimary = "WAIT " + graceMs + "ms"; + mDetail = "many continuations · px " + prefixPct + "%"; + } else { + mPrimary = "TIMER " + graceMs + "ms"; + mDetail = "not complete · px " + prefixPct + "%"; + } } @Override public void drawPreview(@NonNull final Canvas canvas) { if (!isPreviewEnabled()) return; - final String snap = mSnapshot; - if (snap == null) return; + final String primary = mPrimary; + if (primary == null) return; + if (SystemClock.uptimeMillis() > mVisibleUntilMs) { + mPrimary = null; + mDetail = null; + return; + } + final String detail = mDetail == null ? "" : mDetail; final float textH = mTextPaint.getTextSize(); - final float textW = mTextPaint.measureText(snap); - final float padH = PADDING_PX; - final float padV = PADDING_PX; - - // Position: bottom-left of the keyboard area with a small margin. - final float left = padH; - final float top = mKeyboardHeight - textH - padV * 2f; - final float right = left + textW + padH * 2f; - final float bottom = mKeyboardHeight - padV * 0.4f; - - canvas.drawRoundRect(left, top, right, bottom, 4f, 4f, mBgPaint); - canvas.drawText(snap, left + padH, bottom - padV * 0.6f, mTextPaint); + final float lineGap = Math.max(2f, textH * 0.18f); + final float primaryW = mTextPaint.measureText(primary); + final float detailW = mLabelPaint.measureText(detail); + final float boxW = Math.min(mKeyboardWidth - PADDING_PX * 2f, + Math.max(primaryW, detailW) + PADDING_PX * 2f); + final float boxH = textH * 2f + lineGap + PADDING_PX * 2f; + + // Position: bottom-left of the keyboard area, inset enough to avoid clipping. + final float left = PADDING_PX; + final float top = Math.max(PADDING_PX, mKeyboardHeight - boxH - PADDING_PX); + final float right = left + boxW; + final float bottom = top + boxH; + + canvas.drawRoundRect(left, top, right, bottom, 8f, 8f, mBgPaint); + canvas.drawText(primary, left + PADDING_PX, top + PADDING_PX + textH, mTextPaint); + canvas.drawText(detail, left + PADDING_PX, + top + PADDING_PX + textH * 2f + lineGap, mLabelPaint); } @Override public void onDeallocateMemory() { - mSnapshot = null; + mPrimary = null; + mDetail = null; } @Override diff --git a/docs/SPACING_TEST_PLAN.md b/docs/SPACING_TEST_PLAN.md new file mode 100644 index 000000000..507f79be8 --- /dev/null +++ b/docs/SPACING_TEST_PLAN.md @@ -0,0 +1,129 @@ +# Spacing Policy Playtest Plan + +Use this when tuning the two-thumb spacing policy. The goal is practical feel, not reading raw telemetry. + +## Setup + +Use a normal text field where suggestions work. + +Enable: +- **Two-thumb typing** / combining mode +- a non-zero **grace timer** +- **Only auto-finish swiped words** (default on) +- **Adapt pause to the word** when testing signal-driven grace +- **Experimental → Draw gesture debug points** when you want the HUD + +Start with: +- **Finished-word speed-up:** `200 ms` +- **Extendable-stem patience:** `400 ms` + +HUD labels: +- `FAST Nms · finished word` — complete dictionary word; timer shortened. +- `WAIT Nms · many continuations` — prefix-rich stem; timer lengthened. +- `TIMER Nms · not complete` — normal timer; no complete-word signal yet. +- `INSTANT` / `PAUSE` — Assisted-tier gate decision once enabled. + +## Test A — complete words should finish faster + +Type or swipe: +- `I` +- `the` +- `and` +- `hello` + +Expected: +- HUD says **FAST** or **INSTANT** (later Assisted tier). +- The word commits sooner than with **Adapt pause to the word** off. +- It should not feel like the keyboard is waiting for an extension. + +Tune: +- Too eager / commits before you expected → lower **Finished-word speed-up**. +- Still too slow → raise **Finished-word speed-up**. + +## Test B — extendable stems should stay open longer + +Tap or partially swipe stems: +- `ba` (bad / bar / bat / ball / back / bank) +- `ca` (can / car / cat / call / came) +- `pre` (pretty / press / prefer / previous) +- `con` (continue / control / content / consider) + +Expected: +- HUD says **WAIT**. +- The word does **not** auto-finish immediately. +- You can keep typing/swiping the rest without fighting the timer. + +Tune: +- Still commits too soon → raise **Extendable-stem patience**. +- Feels sticky / never finishes → lower **Extendable-stem patience**. + +## Test C — Adapt ON vs OFF comparison + +Use the same words with **Adapt pause to the word** off and on: +- `the` +- `ba` +- `pre` + +Expected: +- OFF: same pause for everything. +- ON: complete words faster, prefix-rich stems slower. + +If you cannot feel a difference: +- try **Finished-word speed-up = 350 ms** +- try **Extendable-stem patience = 700 ms** + +## Test D — shortcut safety + +With **Only auto-finish swiped words** on: +- tap a saved Text Expander shortcut such as `ba` +- pause + +Expected: +- no expansion yet +- no auto-commit +- the shortcut stays composing until you press space or pick it + +This should remain true even when **Adapt pause to the word** is on. + +## Test E — corrections replace, not append + +1. Misspell a word by tapping. +2. Wait briefly. +3. Pick the correction from the suggestion strip. + +Expected: +- The correction replaces the misspelled word. +- It does not append a second word. + +If this fails, first confirm **Only auto-finish swiped words** is on. The historical append bug came from the grace timer auto-committing tap words before the pick. + +## Test F — punctuation and deferred spacing + +With **Defer grace space** on, try: +- `hello.` +- `the,` +- `word?` + +Expected: +- no double spaces +- no space before punctuation +- backspace after a grace commit still removes the right thing + +## Recording results + +For each run, note: +- Base grace timer value +- Finished-word speed-up value +- Extendable-stem patience value +- Whether **Adapt pause to the word** was on +- Whether **Defer grace space** was on +- Example word / result / whether it felt too fast, too slow, or right + +Good tuning notes look like: + +```text +base 500, speed-up 250, patience 650 +"the" FAST 250ms felt right +"ba" WAIT 760ms still too fast, raised patience +shortcut "ba" stayed composing, good +``` From b32754916036cb1497d156ed61fad48671c1fac5 Mon Sep 17 00:00:00 2001 From: Asaf Mahlev Date: Thu, 11 Jun 2026 11:23:16 +0300 Subject: [PATCH 4/4] docs(spacing): clarify tap/swipe paths vs timing controls The first playtest plan mixed input modality with timing behavior. Rewrite it around the actual axes: auto-finish vs auto-space, tap-only vs swipe vs tap-then-swipe, and which timing settings affect each path. Adds timing presets, explicit path matrix, and practical tests for tap-only safety, swiped complete words, swiped stems, tap-then-swipe extension, Adapt on/off, corrections, and deferred punctuation. Verification: :app:compileStandardDebugJavaWithJavac -> BUILD SUCCESSFUL. --- docs/SPACING_TEST_PLAN.md | 272 +++++++++++++++++++++++++++++--------- 1 file changed, 209 insertions(+), 63 deletions(-) diff --git a/docs/SPACING_TEST_PLAN.md b/docs/SPACING_TEST_PLAN.md index 507f79be8..f7705ca21 100644 --- a/docs/SPACING_TEST_PLAN.md +++ b/docs/SPACING_TEST_PLAN.md @@ -2,104 +2,236 @@ Use this when tuning the two-thumb spacing policy. The goal is practical feel, not reading raw telemetry. -## Setup +The most important distinction: -Use a normal text field where suggestions work. +- **Auto-finish** = when the word commits. +- **Auto-space** = when/if a trailing space appears after that commit. -Enable: -- **Two-thumb typing** / combining mode -- a non-zero **grace timer** -- **Only auto-finish swiped words** (default on) -- **Adapt pause to the word** when testing signal-driven grace -- **Experimental → Draw gesture debug points** when you want the HUD +Those are separate. Several settings sound similar but affect different stages. -Start with: -- **Finished-word speed-up:** `200 ms` -- **Extendable-stem patience:** `400 ms` +--- + +## 1. Timing model cheat sheet + +| Setting | Affects | Tap-only words | Swiped words | Tap-then-swipe words | +|---|---|---|---|---| +| **Grace timer** | base auto-finish delay | yes, unless swipe-only finish is ON | yes | yes | +| **Tap extra grace** | extra delay for tap-started combining | yes, unless swipe-only finish is ON | no | yes before the swipe extends | +| **Only auto-finish swiped words** | whether tap-only words can auto-commit | **blocks tap-only auto-finish** | no effect | still allows auto-finish after the swipe fragment | +| **Adapt pause to the word** | modifies the grace timer by word signals | only if the word is allowed to auto-finish | yes | yes after the swipe fragment | +| **Only auto-space after swipes** | trailing space only | tap word may still commit, just no space | allows space | allows space after swiped fragment | +| **Defer grace space** | when the space materializes | no change to commit timing | no change to commit timing | no change to commit timing | + +Recommended safety defaults: + +```text +Only auto-finish swiped words: ON +Only auto-space after swipes: ON if you dislike tap-created spaces +Adapt pause to the word: ON only while tuning / testing +Defer grace space: ON only while testing deferred-space feel +``` + +--- + +## 2. Presets to try + +Do not tune one slider at a time first. Try a complete profile. + +### Profile A — Conservative / safe + +```text +Base grace timer: 650 ms +Tap extra grace: 250 ms +Finished-word speed-up: 150 ms +Extendable-stem patience: 700 ms +Only auto-finish swiped words: ON +Adapt pause to the word: ON +``` + +Expected: +- swiped complete words: about 500 ms +- swiped prefix-rich stems: roughly 900–1200 ms +- tap-only words: no auto-finish + +### Profile B — Balanced + +```text +Base grace timer: 550 ms +Tap extra grace: 250 ms +Finished-word speed-up: 250 ms +Extendable-stem patience: 600 ms +Only auto-finish swiped words: ON +Adapt pause to the word: ON +``` + +Expected: +- swiped complete words: about 300 ms +- medium stems: about 700–900 ms +- prefix-heavy stems: about 1000 ms + +### Profile C — Fast / assisted + +```text +Base grace timer: 450 ms +Tap extra grace: 250 ms +Finished-word speed-up: 300 ms +Extendable-stem patience: 650 ms +Only auto-finish swiped words: ON +Adapt pause to the word: ON +``` + +Expected: +- swiped complete words: about 150–250 ms +- stems still protected by patience + +### Profile D — Debug / exaggerated + +```text +Base grace timer: 600 ms +Tap extra grace: 250 ms +Finished-word speed-up: 450 ms +Extendable-stem patience: 1000 ms +Only auto-finish swiped words: ON +Adapt pause to the word: ON +``` + +Expected: +- swiped complete words commit very fast +- `ba`, `pre`, `con` wait a long time + +Use this only to understand the system. + +--- + +## 3. HUD labels + +Enable **Experimental → Draw gesture debug points**. HUD labels: + - `FAST Nms · finished word` — complete dictionary word; timer shortened. - `WAIT Nms · many continuations` — prefix-rich stem; timer lengthened. - `TIMER Nms · not complete` — normal timer; no complete-word signal yet. - `INSTANT` / `PAUSE` — Assisted-tier gate decision once enabled. -## Test A — complete words should finish faster +If the HUD says `WAIT` for a word you expected to be finished, the dictionary/suggestion signal thinks many continuations are plausible. -Type or swipe: -- `I` +--- + +## 4. Practical tests + +### Test A — tap-only safety + +Input method: **tap only**. + +Use: +- `ba` (saved shortcut if available) +- any short typed word + +Settings: +- **Only auto-finish swiped words: ON** + +Expected: +- no auto-commit +- no Text Expander expansion +- no correction append behavior +- word stays composing until you press space or pick a suggestion + +If this fails, the swipe-only finish gate is broken. + +### Test B — swiped complete words should finish faster + +Input method: **swipe**. + +Try: - `the` - `and` +- `I` - `hello` -Expected: -- HUD says **FAST** or **INSTANT** (later Assisted tier). -- The word commits sooner than with **Adapt pause to the word** off. -- It should not feel like the keyboard is waiting for an extension. +Expected with **Adapt pause to the word ON**: +- HUD says **FAST** +- word commits sooner than with Adapt OFF +- no need to tap space if the grace auto-space settings allow it -Tune: -- Too eager / commits before you expected → lower **Finished-word speed-up**. -- Still too slow → raise **Finished-word speed-up**. +Tuning: +- Too eager → lower **Finished-word speed-up** or raise base grace. +- Too slow → raise **Finished-word speed-up** or lower base grace. -## Test B — extendable stems should stay open longer +### Test C — swiped extendable stems should stay open longer -Tap or partially swipe stems: +Input method: **swipe or partial swipe**. + +Try stems: - `ba` (bad / bar / bat / ball / back / bank) - `ca` (can / car / cat / call / came) - `pre` (pretty / press / prefer / previous) - `con` (continue / control / content / consider) Expected: -- HUD says **WAIT**. -- The word does **not** auto-finish immediately. -- You can keep typing/swiping the rest without fighting the timer. +- HUD says **WAIT** +- word does not auto-finish immediately +- you can keep extending without fighting the timer + +Tuning: +- Still commits too soon → raise **Extendable-stem patience** or base grace. +- Too sticky / never finishes → lower **Extendable-stem patience**. + +### Test D — tap-then-swipe still extends + +Input method: **tap prefix, then swipe rest**. + +Example: +- tap `fire` +- swipe `truck` + +Expected: +- tap prefix does not auto-finish by itself +- after the swipe fragment, the combined word still auto-finishes normally +- it should not become `fire firetruck` -Tune: -- Still commits too soon → raise **Extendable-stem patience**. -- Feels sticky / never finishes → lower **Extendable-stem patience**. +This verifies that **Only auto-finish swiped words** suppresses only the tap timer, not combining-mode entry. -## Test C — Adapt ON vs OFF comparison +### Test E — Adapt ON vs OFF comparison -Use the same words with **Adapt pause to the word** off and on: +Input methods: **swipe**, plus tap-only safety check. + +Words: - `the` - `ba` - `pre` Expected: -- OFF: same pause for everything. -- ON: complete words faster, prefix-rich stems slower. +- Adapt OFF: same pause for all swiped words/stems. +- Adapt ON: complete words faster, prefix-rich stems slower. +- Tap-only words still do not auto-finish when swipe-only finish is ON. If you cannot feel a difference: -- try **Finished-word speed-up = 350 ms** -- try **Extendable-stem patience = 700 ms** - -## Test D — shortcut safety -With **Only auto-finish swiped words** on: -- tap a saved Text Expander shortcut such as `ba` -- pause - -Expected: -- no expansion yet -- no auto-commit -- the shortcut stays composing until you press space or pick it +```text +Finished-word speed-up = 350 ms +Extendable-stem patience = 700 ms +``` -This should remain true even when **Adapt pause to the word** is on. +### Test F — corrections replace, not append -## Test E — corrections replace, not append +Input method: **tap only**. 1. Misspell a word by tapping. 2. Wait briefly. 3. Pick the correction from the suggestion strip. Expected: -- The correction replaces the misspelled word. -- It does not append a second word. +- correction replaces the misspelled word +- it does not append a second word -If this fails, first confirm **Only auto-finish swiped words** is on. The historical append bug came from the grace timer auto-committing tap words before the pick. +If this fails, first confirm **Only auto-finish swiped words** is ON. The historical append bug came from the grace timer auto-committing tap words before the pick. -## Test F — punctuation and deferred spacing +### Test G — punctuation and deferred spacing -With **Defer grace space** on, try: +Input method: **swipe**. + +With **Defer grace space** ON, try: - `hello.` - `the,` - `word?` @@ -109,21 +241,35 @@ Expected: - no space before punctuation - backspace after a grace commit still removes the right thing -## Recording results +Remember: this tests **space materialization**, not auto-finish timing. + +--- + +## 5. Recording results For each run, note: -- Base grace timer value -- Finished-word speed-up value -- Extendable-stem patience value -- Whether **Adapt pause to the word** was on -- Whether **Defer grace space** was on -- Example word / result / whether it felt too fast, too slow, or right + +```text +base grace: +tap extra grace: +finished-word speed-up: +extendable-stem patience: +Adapt pause to the word: on/off +Only auto-finish swiped words: on/off +Only auto-space after swipes: on/off +Defer grace space: on/off +input method: tap / swipe / tap-then-swipe +word: +HUD: +result: +``` Good tuning notes look like: ```text -base 500, speed-up 250, patience 650 -"the" FAST 250ms felt right -"ba" WAIT 760ms still too fast, raised patience -shortcut "ba" stayed composing, good +base 550, tap extra 250, speed-up 250, patience 600 +swipe "the" -> FAST 300ms, felt right +swipe "ba" -> WAIT 910ms, still too fast, raised patience +TAP shortcut "ba" -> stayed composing, good +TAP then SWIPE fire+truck -> combined and committed, good ```