tawasuyu

sergio/humanoid

HTML 25Markdown 1TOML 10 Sutichaykuna

⎇ 8380e44a0947bafb169773353ce2b2036aff9e70 keyboard/src/main/java/com/rizoma/trazo/ime/KeyboardController.kt 161 siq'ikuna · 4.7 KB Ñawpa willay ch'uya

Trazo: predicción, aprendizaje y gestos de espacio/borrado (v0.2) 8380e44a0 · sergio ·

1package com.rizoma.trazo.ime
2
3import androidx.compose.runtime.getValue
4import androidx.compose.runtime.mutableStateOf
5import androidx.compose.runtime.setValue
6import com.rizoma.trazo.data.SuggestionEngine
7import com.rizoma.trazo.model.Layouts
8
9/** Puente con el editor real. Lo implementa el InputMethodService. */
10interface ImeHost {
11    fun commit(text: String)
12    fun backspace()
13    fun newline()
14    fun setComposing(text: String)
15    fun finishComposing()
16    fun moveCursor(steps: Int)
17    fun textBeforeCursor(n: Int): CharSequence
18    /** TextUtils.CAP_MODE_* combinado: !=0 si el editor quiere mayúscula aquí. */
19    fun cursorCapsMode(): Int
20}
21
22enum class ShiftState { OFF, ON, LOCK }
23enum class Layer { LETTERS, SYMBOLS }
24
25/**
26 * Toda la lógica de escritura. La palabra en curso vive como "texto en composición"
27 * del editor (subrayada) para poder reemplazarla por una sugerencia de un toque.
28 */
29class KeyboardController(
30    private val host: ImeHost,
31    private val engine: SuggestionEngine,
32) {
33
34    var shift by mutableStateOf(ShiftState.ON)
35        private set
36    var layer by mutableStateOf(Layer.LETTERS)
37        private set
38    var suggestions by mutableStateOf<List<String>>(emptyList())
39        private set
40
41    private val composing = StringBuilder()
42
43    val keys get() = if (layer == Layer.LETTERS) Layouts.letters else Layouts.symbols
44    val shifted get() = shift != ShiftState.OFF
45
46    fun onChar(raw: Char) {
47        val out = if (shifted) raw.uppercaseChar() else raw
48        if (layer == Layer.LETTERS && raw.isLetter()) {
49            composing.append(out)
50            if (shift == ShiftState.ON) shift = ShiftState.OFF
51            host.setComposing(composing.toString())
52            refreshSuggestions()
53        } else {
54            finishWord()
55            host.commit(out.toString())
56            if (shift == ShiftState.ON) shift = ShiftState.OFF
57            syncAutoShift()
58        }
59    }
60
61    fun onSpace() {
62        finishWord()
63        val before = host.textBeforeCursor(2)
64        if (before.length == 2 && before[1] == ' ' && before[0].isLetterOrDigit()) {
65            host.backspace()
66            host.commit(". ")
67        } else {
68            host.commit(" ")
69        }
70        syncAutoShift()
71    }
72
73    fun onBackspace() {
74        if (composing.isNotEmpty()) {
75            composing.deleteCharAt(composing.length - 1)
76            if (composing.isEmpty()) {
77                host.finishComposing()
78                suggestions = emptyList()
79            } else {
80                host.setComposing(composing.toString())
81                refreshSuggestions()
82            }
83        } else {
84            host.backspace()
85            syncAutoShift()
86        }
87    }
88
89    fun onEnter() {
90        finishWord()
91        host.newline()
92        syncAutoShift()
93    }
94
95    fun onShift() {
96        shift = when (shift) {
97            ShiftState.OFF -> ShiftState.ON
98            ShiftState.ON -> ShiftState.LOCK
99            ShiftState.LOCK -> ShiftState.OFF
100        }
101    }
102
103    fun toggleLayer() {
104        finishWord()
105        layer = if (layer == Layer.LETTERS) Layer.SYMBOLS else Layer.LETTERS
106    }
107
108    /** Elige una sugerencia: reemplaza la palabra en composición y deja un espacio. */
109    fun commitSuggestion(word: String) {
110        val cased = if (composing.firstOrNull()?.isUpperCase() == true) {
111            word.replaceFirstChar { it.uppercaseChar() }
112        } else {
113            word
114        }
115        host.commit("$cased ") // commitText reemplaza la región en composición
116        engine.learn(word)
117        composing.clear()
118        suggestions = emptyList()
119        syncAutoShift()
120    }
121
122    fun moveCursor(steps: Int) {
123        if (composing.isNotEmpty()) finishWord()
124        host.moveCursor(steps)
125        syncAutoShift()
126    }
127
128    /** Nuevo campo de entrada: descarta cualquier composición local. */
129    fun reset() {
130        composing.clear()
131        suggestions = emptyList()
132    }
133
134    /** El editor terminó la composición por su cuenta (cursor movido fuera, etc.). */
135    fun abandonComposing() {
136        if (composing.isEmpty()) return
137        composing.clear()
138        suggestions = emptyList()
139    }
140
141    fun hasComposing(): Boolean = composing.isNotEmpty()
142
143    /** Recalcula la mayúscula automática según el contexto del editor. */
144    fun syncAutoShift() {
145        if (shift == ShiftState.LOCK) return
146        shift = if (host.cursorCapsMode() != 0) ShiftState.ON else ShiftState.OFF
147    }
148
149    private fun finishWord() {
150        if (composing.isNotEmpty()) {
151            host.finishComposing()
152            engine.learn(composing.toString())
153            composing.clear()
154        }
155        suggestions = emptyList()
156    }
157
158    private fun refreshSuggestions() {
159        suggestions = engine.suggest(composing.toString(), 3)
160    }
161}