⎇ 5227fbf1abfb2a437968ebc63692560c3a929230
keyboard/src/main/java/com/rizoma/trazo/ui/KeyboardScreen.kt
Ñawpa willay
ch'uya
Trazo: arreglar cursor al reposicionar + mayúsculas en R↑ y columna reordenada
1package com.rizoma.trazo.ui
2
3import androidx.compose.foundation.background
4import androidx.compose.foundation.clickable
5import androidx.compose.foundation.gestures.awaitEachGesture
6import androidx.compose.foundation.gestures.awaitFirstDown
7import androidx.compose.foundation.gestures.detectTapGestures
8import androidx.compose.foundation.layout.Box
9import androidx.compose.foundation.layout.Column
10import androidx.compose.foundation.layout.Row
11import androidx.compose.foundation.layout.fillMaxHeight
12import androidx.compose.foundation.layout.fillMaxWidth
13import androidx.compose.foundation.layout.height
14import androidx.compose.foundation.layout.navigationBarsPadding
15import androidx.compose.foundation.layout.padding
16import androidx.compose.foundation.shape.RoundedCornerShape
17import androidx.compose.material3.Text
18import androidx.compose.runtime.Composable
19import androidx.compose.runtime.rememberCoroutineScope
20import androidx.compose.ui.Alignment
21import androidx.compose.ui.Modifier
22import androidx.compose.ui.draw.clip
23import androidx.compose.ui.graphics.Color
24import androidx.compose.ui.input.pointer.pointerInput
25import androidx.compose.ui.input.pointer.positionChange
26import androidx.compose.ui.platform.LocalDensity
27import androidx.compose.ui.text.font.FontWeight
28import androidx.compose.ui.text.style.TextOverflow
29import androidx.compose.ui.unit.dp
30import androidx.compose.ui.unit.sp
31import com.rizoma.trazo.data.TrazoSettings
32import com.rizoma.trazo.ime.KeyboardController
33import com.rizoma.trazo.ime.Layer
34import com.rizoma.trazo.ime.ShiftState
35import com.rizoma.trazo.ui.theme.NeonCian
36import com.rizoma.trazo.ui.theme.NeonRosa
37import com.rizoma.trazo.ui.theme.NeonVerde
38import com.rizoma.trazo.ui.theme.Tecla
39import com.rizoma.trazo.ui.theme.Tenue
40import com.rizoma.trazo.ui.theme.Vacio
41import kotlinx.coroutines.delay
42import kotlinx.coroutines.launch
43
44@Composable
45fun KeyboardScreen(controller: KeyboardController, settings: TrazoSettings) {
46 Column(
47 Modifier
48 .fillMaxWidth()
49 .background(Vacio)
50 // Evita que la barra de navegación del sistema tape la fila de funciones.
51 .navigationBarsPadding()
52 .padding(4.dp),
53 ) {
54 SuggestionBar(controller.suggestions, onPick = controller::commitSuggestion)
55
56 if (controller.layer == Layer.EMOJIS) {
57 EmojiPad(
58 recents = settings.recentEmojis,
59 onEmoji = controller::onEmoji,
60 modifier = Modifier.fillMaxWidth().height(210.dp),
61 )
62 Row(Modifier.fillMaxWidth().height(52.dp)) {
63 FuncKey("ABC", NeonCian, Modifier.weight(1.4f), onClick = controller::toggleLayer)
64 SpaceKey(Modifier.weight(3f), onSpace = controller::onSpace, onCursor = controller::moveCursor)
65 BackspaceKey(Modifier.weight(1.4f), onBackspace = controller::onBackspace)
66 }
67 } else {
68 val keys = controller.keys
69 // Color de la pista de mayúsculas (R↑) según el estado del shift.
70 val shiftAccent = when (controller.shift) {
71 ShiftState.OFF -> Tenue
72 ShiftState.ON -> NeonVerde
73 ShiftState.LOCK -> NeonRosa
74 }
75 // Grid 3×3 (izquierda, ~¾) + columna de funciones (derecha, ~¼), como MessagEase.
76 Row(Modifier.fillMaxWidth().height(210.dp)) {
77 Column(Modifier.weight(3f).fillMaxHeight()) {
78 for (row in 0 until 3) {
79 Row(Modifier.fillMaxWidth().weight(1f)) {
80 for (col in 0 until 3) {
81 KeyCell(
82 key = keys[row * 3 + col],
83 shifted = controller.shifted,
84 onChar = controller::onChar,
85 onShift = controller::onShift,
86 shiftAccent = shiftAccent,
87 modifier = Modifier.weight(1f).fillMaxHeight(),
88 )
89 }
90 }
91 }
92 }
93 // Columna derecha, arriba→abajo: emoticons, núm/alfa, borrar, enter.
94 // (Mayúsculas ya no va aquí: vive en el swipe ↑ de la R.)
95 Column(Modifier.weight(1f).fillMaxHeight()) {
96 FuncKey("😊", NeonRosa, Modifier.fillMaxWidth().weight(1f), onClick = controller::showEmojis)
97 FuncKey(
98 if (controller.layer == Layer.LETTERS) "?123" else "ABC",
99 NeonCian, Modifier.fillMaxWidth().weight(1f), onClick = controller::toggleLayer,
100 )
101 BackspaceKey(Modifier.fillMaxWidth().weight(1f), onBackspace = controller::onBackspace)
102 FuncKey("↵", NeonVerde, Modifier.fillMaxWidth().weight(1f), onClick = controller::onEnter)
103 }
104 }
105
106 // Fila inferior: barra de espacio a todo el ancho.
107 Row(Modifier.fillMaxWidth().height(56.dp)) {
108 SpaceKey(Modifier.weight(1f), onSpace = controller::onSpace, onCursor = controller::moveCursor)
109 }
110 }
111 }
112}
113
114@Composable
115private fun SuggestionBar(suggestions: List<String>, onPick: (String) -> Unit) {
116 Row(
117 Modifier.fillMaxWidth().height(40.dp).padding(horizontal = 4.dp),
118 verticalAlignment = Alignment.CenterVertically,
119 ) {
120 suggestions.forEach { word ->
121 Box(
122 Modifier
123 .weight(1f)
124 .fillMaxHeight()
125 .padding(3.dp)
126 .clip(RoundedCornerShape(10.dp))
127 .clickable { onPick(word) },
128 contentAlignment = Alignment.Center,
129 ) {
130 Text(
131 text = word,
132 color = NeonVerde,
133 fontSize = 15.sp,
134 maxLines = 1,
135 overflow = TextOverflow.Ellipsis,
136 )
137 }
138 }
139 }
140}
141
142@Composable
143private fun FuncKey(label: String, accent: Color, modifier: Modifier, onClick: () -> Unit) {
144 Box(
145 modifier
146 .fillMaxHeight()
147 .padding(3.dp)
148 .clip(RoundedCornerShape(14.dp))
149 .background(Tecla)
150 .clickable(onClick = onClick),
151 contentAlignment = Alignment.Center,
152 ) {
153 Text(
154 text = label,
155 color = accent,
156 fontSize = if (label.length > 2) 14.sp else 20.sp,
157 fontWeight = FontWeight.Medium,
158 )
159 }
160}
161
162/** Espacio con swipe horizontal para mover el cursor; toque = espacio. */
163@Composable
164private fun SpaceKey(modifier: Modifier, onSpace: () -> Unit, onCursor: (Int) -> Unit) {
165 val step = with(LocalDensity.current) { 16.dp.toPx() }
166 Box(
167 modifier
168 .fillMaxHeight()
169 .padding(3.dp)
170 .clip(RoundedCornerShape(14.dp))
171 .background(Tecla)
172 .pointerInput(Unit) {
173 awaitEachGesture {
174 val down = awaitFirstDown()
175 var accum = 0f
176 var moved = false
177 while (true) {
178 val event = awaitPointerEvent()
179 val change = event.changes.firstOrNull { it.id == down.id } ?: break
180 accum += change.positionChange().x
181 change.consume()
182 while (accum >= step) { onCursor(1); accum -= step; moved = true }
183 while (accum <= -step) { onCursor(-1); accum += step; moved = true }
184 if (!change.pressed) break
185 }
186 if (!moved) onSpace()
187 }
188 },
189 contentAlignment = Alignment.Center,
190 ) {
191 Text("espacio", color = Tenue, fontSize = 14.sp)
192 }
193}
194
195/** Borrado con repetición al mantener pulsado. */
196@Composable
197private fun BackspaceKey(modifier: Modifier, onBackspace: () -> Unit) {
198 val scope = rememberCoroutineScope()
199 Box(
200 modifier
201 .fillMaxHeight()
202 .padding(3.dp)
203 .clip(RoundedCornerShape(14.dp))
204 .background(Tecla)
205 .pointerInput(Unit) {
206 detectTapGestures(
207 onPress = {
208 onBackspace()
209 val job = scope.launch {
210 delay(380)
211 while (true) {
212 onBackspace()
213 delay(55)
214 }
215 }
216 try {
217 awaitRelease()
218 } finally {
219 job.cancel()
220 }
221 },
222 )
223 },
224 contentAlignment = Alignment.Center,
225 ) {
226 Text("⌫", color = Color.White, fontSize = 20.sp)
227 }
228}