⎇ 8380e44a0947bafb169773353ce2b2036aff9e70
keyboard/src/main/java/com/rizoma/trazo/ui/KeyboardScreen.kt
Historia
crudo
Trazo: predicción, aprendizaje y gestos de espacio/borrado (v0.2)
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.padding
15import androidx.compose.foundation.shape.RoundedCornerShape
16import androidx.compose.material3.Text
17import androidx.compose.runtime.Composable
18import androidx.compose.runtime.rememberCoroutineScope
19import androidx.compose.ui.Alignment
20import androidx.compose.ui.Modifier
21import androidx.compose.ui.draw.clip
22import androidx.compose.ui.graphics.Color
23import androidx.compose.ui.input.pointer.pointerInput
24import androidx.compose.ui.input.pointer.positionChange
25import androidx.compose.ui.platform.LocalDensity
26import androidx.compose.ui.text.font.FontWeight
27import androidx.compose.ui.text.style.TextOverflow
28import androidx.compose.ui.unit.dp
29import androidx.compose.ui.unit.sp
30import com.rizoma.trazo.ime.KeyboardController
31import com.rizoma.trazo.ime.Layer
32import com.rizoma.trazo.ime.ShiftState
33import com.rizoma.trazo.ui.theme.NeonCian
34import com.rizoma.trazo.ui.theme.NeonRosa
35import com.rizoma.trazo.ui.theme.NeonVerde
36import com.rizoma.trazo.ui.theme.Tecla
37import com.rizoma.trazo.ui.theme.Tenue
38import com.rizoma.trazo.ui.theme.Vacio
39import kotlinx.coroutines.delay
40import kotlinx.coroutines.launch
41
42@Composable
43fun KeyboardScreen(controller: KeyboardController) {
44 Column(
45 Modifier
46 .fillMaxWidth()
47 .background(Vacio)
48 .padding(4.dp),
49 ) {
50 SuggestionBar(controller.suggestions, onPick = controller::commitSuggestion)
51
52 val keys = controller.keys
53 for (row in 0 until 3) {
54 Row(Modifier.fillMaxWidth().height(70.dp)) {
55 for (col in 0 until 3) {
56 KeyCell(
57 key = keys[row * 3 + col],
58 shifted = controller.shifted,
59 onChar = controller::onChar,
60 modifier = Modifier.weight(1f).fillMaxHeight(),
61 )
62 }
63 }
64 }
65
66 Row(Modifier.fillMaxWidth().height(52.dp)) {
67 val shiftAccent = when (controller.shift) {
68 ShiftState.OFF -> Tenue
69 ShiftState.ON -> NeonVerde
70 ShiftState.LOCK -> NeonRosa
71 }
72 FuncKey("⇧", shiftAccent, Modifier.weight(1f), onClick = controller::onShift)
73 FuncKey(
74 if (controller.layer == Layer.LETTERS) "?123" else "ABC",
75 NeonCian, Modifier.weight(1f), onClick = controller::toggleLayer,
76 )
77 SpaceKey(Modifier.weight(3f), onSpace = controller::onSpace, onCursor = controller::moveCursor)
78 BackspaceKey(Modifier.weight(1f), onBackspace = controller::onBackspace)
79 FuncKey("↵", NeonVerde, Modifier.weight(1f), onClick = controller::onEnter)
80 }
81 }
82}
83
84@Composable
85private fun SuggestionBar(suggestions: List<String>, onPick: (String) -> Unit) {
86 Row(
87 Modifier.fillMaxWidth().height(40.dp).padding(horizontal = 4.dp),
88 verticalAlignment = Alignment.CenterVertically,
89 ) {
90 suggestions.forEach { word ->
91 Box(
92 Modifier
93 .weight(1f)
94 .fillMaxHeight()
95 .padding(3.dp)
96 .clip(RoundedCornerShape(10.dp))
97 .clickable { onPick(word) },
98 contentAlignment = Alignment.Center,
99 ) {
100 Text(
101 text = word,
102 color = NeonVerde,
103 fontSize = 15.sp,
104 maxLines = 1,
105 overflow = TextOverflow.Ellipsis,
106 )
107 }
108 }
109 }
110}
111
112@Composable
113private fun FuncKey(label: String, accent: Color, modifier: Modifier, onClick: () -> Unit) {
114 Box(
115 modifier
116 .fillMaxHeight()
117 .padding(3.dp)
118 .clip(RoundedCornerShape(14.dp))
119 .background(Tecla)
120 .clickable(onClick = onClick),
121 contentAlignment = Alignment.Center,
122 ) {
123 Text(
124 text = label,
125 color = accent,
126 fontSize = if (label.length > 2) 14.sp else 20.sp,
127 fontWeight = FontWeight.Medium,
128 )
129 }
130}
131
132/** Espacio con swipe horizontal para mover el cursor; toque = espacio. */
133@Composable
134private fun SpaceKey(modifier: Modifier, onSpace: () -> Unit, onCursor: (Int) -> Unit) {
135 val step = with(LocalDensity.current) { 16.dp.toPx() }
136 Box(
137 modifier
138 .fillMaxHeight()
139 .padding(3.dp)
140 .clip(RoundedCornerShape(14.dp))
141 .background(Tecla)
142 .pointerInput(Unit) {
143 awaitEachGesture {
144 val down = awaitFirstDown()
145 var accum = 0f
146 var moved = false
147 while (true) {
148 val event = awaitPointerEvent()
149 val change = event.changes.firstOrNull { it.id == down.id } ?: break
150 accum += change.positionChange().x
151 change.consume()
152 while (accum >= step) { onCursor(1); accum -= step; moved = true }
153 while (accum <= -step) { onCursor(-1); accum += step; moved = true }
154 if (!change.pressed) break
155 }
156 if (!moved) onSpace()
157 }
158 },
159 contentAlignment = Alignment.Center,
160 ) {
161 Text("espacio", color = Tenue, fontSize = 14.sp)
162 }
163}
164
165/** Borrado con repetición al mantener pulsado. */
166@Composable
167private fun BackspaceKey(modifier: Modifier, onBackspace: () -> Unit) {
168 val scope = rememberCoroutineScope()
169 Box(
170 modifier
171 .fillMaxHeight()
172 .padding(3.dp)
173 .clip(RoundedCornerShape(14.dp))
174 .background(Tecla)
175 .pointerInput(Unit) {
176 detectTapGestures(
177 onPress = {
178 onBackspace()
179 val job = scope.launch {
180 delay(380)
181 while (true) {
182 onBackspace()
183 delay(55)
184 }
185 }
186 try {
187 awaitRelease()
188 } finally {
189 job.cancel()
190 }
191 },
192 )
193 },
194 contentAlignment = Alignment.Center,
195 ) {
196 Text("⌫", color = Color.White, fontSize = 20.sp)
197 }
198}