⎇ 8380e44a0947bafb169773353ce2b2036aff9e70
keyboard/src/main/java/com/rizoma/trazo/ime/KeyboardController.kt
Ñawpa willay
ch'uya
Trazo: predicción, aprendizaje y gestos de espacio/borrado (v0.2)
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}