Repository navigation
Two-thumb: drop the word-joining layer if thumbs are genuinely simultaneous #143
Description
The observation
From daily use: "if we always have simultaneous typing, we never need to join together parts of words, right?"
That's correct, and it's a stronger statement than it first appears.
Every piece of the multi-part machinery exists for one reason: the code assumes the gesture decoder accepts one trail, so two thumbs must be spliced into one. That assumption produced:
- connector-point synthesis (
StrokeAlignerCONNECTORmode) - the merged-trail extend base (
WordComposer.setExtendBatchInputBase) - the ideal-prefix builder (
IdealPrefixTrailBuilder, tap → micro-stroke) - stroke re-timing knobs
- fragment stitching and fragment backspace
The research in #141 established that the assumption is false: the AOSP decoder models two pointer tracks (MAX_POINTER_COUNT_G == 2) and partitions points by pointer id, not by timing. InputPointers.appendAll hardcoded id 0, which is the sole reason the splicing layer had to exist.
So if both thumbs are handed over as two tracks, there is nothing to join, and that entire layer can be deleted rather than tuned.
Proposal
For the experimental build: stop synthesising, and pass the two thumbs through as what they are.
- Remove connector synthesis, ideal-prefix promotion, and the re-timing controls from the active path.
- Keep the first-seen pointer-id normalisation from Two-thumb experimental build (upstream v4.1.2 merged, installs side by side) #141 — that fixed a real bug where a stroke carrying no id
0returned zero suggestions. - Simplify word assembly: a word is whatever the decoder returns for the concurrent tracks, with no extend-base and no fragment stitching.
- Retain only the part of the combining timer that decides when the word is finished, which is a separate job from gluing.
Two things that must be settled first
1. This is native-decoder only. sHaveNativeGestureLib is true only for a user-supplied or system glide library; the bundled one is dictionary-only and sets it false. The Java fallback engine ignores pointer ids entirely and scores a single trail. Simplifying this way makes two-thumb typing a native-decoder feature, and leaves fallback users with either the old machinery or nothing. That's a product decision, not a code one.
2. Two tracks is a hard ceiling. Genuinely concurrent thumbs fit in two tracks. A word built from three or more sequential fragments does not, and that is precisely what the current machinery was built to handle. Whether that case matters in practice is a question about how people actually type, and it is the thing to measure before deleting anything.
What would settle it
Real use of the experimental build with Joining word parts → Two separate thumb tracks, comparing against One joined trail. Specifically: are both thumbs on the screen at overlapping times, or is it more alternating? If overlapping dominates, the joining layer is dead weight and should go. If alternating dominates, two tracks are not enough and the layer earns its place.
Related
- Two-thumb experimental build (upstream v4.1.2 merged, installs side by side) #141 — experimental build (
com.asafmah.leantypedual.exp, "LeanTypeDual EXP" in the picker), installs side by side - B7c: Adaptive connector bridge + distance-based re-timing #100 — the re-timing half of this was measured and falsified; do not rebuild it
- B4: Per-thumb pointer attribution (true simultaneous two-thumb) #29 — per-thumb pointer attribution, which is the same lever and is probably mis-classified as separate from [Epic] B7: Multi-part fake-track synthesis (tap→swipe / swipe→swipe quality) #97
- B7a: Prefix-aware gesture result stripping #98 / B7b: Ideal prefix trail (key-center synthetic prefix; tap → micro-stroke) #99 — tap → micro-stroke promotion, already built on
b7a-prefix-aware-stripping
Note on the gesture library
Worth recording, since it cost real confusion: the library lives in the app's private files/libjni_latinime.so, not in preferences. A separately-packaged build (.exp, or a fresh install of the release package) therefore starts without it, and exporting/importing settings does not carry it across. Until it is loaded, every recognition setting in this area is inert — and one of them was actively harmful, which #141 fixes by gating the whole group and saying why.
Activity
AsafMah commentedon Aug 20, 2026
Re-scope needed. This issue assumed the decoder that models two pointer tracks is the decoder that actually runs. It isn't, on any device where the setting is even visible.
sHaveNativeGestureLibis true only when a user-supplied (or system) glide library is loaded viaSystem.load(); the bundled in-repo library explicitly sets itfalse("standard dictionary only, no gesture engine"). So the AOSP source that was measured —MAX_POINTER_COUNT_G == 2, pointer-id partitioning inupdateTouchPoints— is not what decodes on the device. A closed third-party binary is.Real-world result: typing "ambulance" repeatedly with
DUAL_POINTERenabled and multi-part composition armed produces nonsense.The simplification argument here — that the joining layer is unnecessary because the decoder natively accepts two tracks — is therefore unsupported for the decoder that actually runs. It may still hold for the bundled AOSP engine, but that engine doesn't do glide typing at all.
Worth keeping open: the underlying observation (splicing exists only because the code assumed a single trail) is still interesting, and the pointer-id normalisation fix from #141 stands on its own. But the premise needs re-establishing against the real decoder before any of the joining layer is deleted.
AsafMah commentedon Sep 3, 2026
Closed after #151. This simplification premise depended on the loaded gesture decoder supporting the two-track semantics measured in the in-repo AOSP engine. Device testing proved that assumption false: the actual closed library produced nonsense. The dead joining experiments are removed; normal connector behavior remains. Reopen only with measurements against the actual loaded decoder.