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