From 280bd7c2b15057c0b3549cdcf408ad2d518941bc Mon Sep 17 00:00:00 2001 From: Asaf Mahlev Date: Thu, 11 Jun 2026 09:21:43 +0300 Subject: [PATCH] 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])