⎇ b021d6829a602218866114f86e2350d80f0500e2
keyboard/src/main/java/com/rizoma/trazo/ui/KeyboardScreen.kt
History
raw
Trazo: pulido v0.4 — háptica, palabra siguiente, emojis y ajustes
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 for (row in 0 until 3) {
70 Row(Modifier.fillMaxWidth().height(70.dp)) {
71 for (col in 0 until 3) {
72 KeyCell(
73 key = keys[row * 3 + col],
74 shifted = controller.shifted,
75 onChar = controller::onChar,
76 modifier = Modifier.weight(1f).fillMaxHeight(),
77 )
78 }
79 }
80 }
81
82 Row(Modifier.fillMaxWidth().height(52.dp)) {
83 val shiftAccent = when (controller.shift) {
84 ShiftState.OFF -> Tenue
85 ShiftState.ON -> NeonVerde
86 ShiftState.LOCK -> NeonRosa
87 }
88 FuncKey("⇧", shiftAccent, Modifier.weight(1f), onClick = controller::onShift)
89 FuncKey(
90 if (controller.layer == Layer.LETTERS) "?123" else "ABC",
91 NeonCian, Modifier.weight(1f), onClick = controller::toggleLayer,
92 )
93 FuncKey("😊", NeonRosa, Modifier.weight(1f), onClick = controller::showEmojis)
94 SpaceKey(Modifier.weight(3f), onSpace = controller::onSpace, onCursor = controller::moveCursor)
95 BackspaceKey(Modifier.weight(1f), onBackspace = controller::onBackspace)
96 FuncKey("↵", NeonVerde, Modifier.weight(1f), onClick = controller::onEnter)
97 }
98 }
99 }
100}
101
102@Composable
103private fun SuggestionBar(suggestions: List<String>, onPick: (String) -> Unit) {
104 Row(
105 Modifier.fillMaxWidth().height(40.dp).padding(horizontal = 4.dp),
106 verticalAlignment = Alignment.CenterVertically,
107 ) {
108 suggestions.forEach { word ->
109 Box(
110 Modifier
111 .weight(1f)
112 .fillMaxHeight()
113 .padding(3.dp)
114 .clip(RoundedCornerShape(10.dp))
115 .clickable { onPick(word) },
116 contentAlignment = Alignment.Center,
117 ) {
118 Text(
119 text = word,
120 color = NeonVerde,
121 fontSize = 15.sp,
122 maxLines = 1,
123 overflow = TextOverflow.Ellipsis,
124 )
125 }
126 }
127 }
128}
129
130@Composable
131private fun FuncKey(label: String, accent: Color, modifier: Modifier, onClick: () -> Unit) {
132 Box(
133 modifier
134 .fillMaxHeight()
135 .padding(3.dp)
136 .clip(RoundedCornerShape(14.dp))
137 .background(Tecla)
138 .clickable(onClick = onClick),
139 contentAlignment = Alignment.Center,
140 ) {
141 Text(
142 text = label,
143 color = accent,
144 fontSize = if (label.length > 2) 14.sp else 20.sp,
145 fontWeight = FontWeight.Medium,
146 )
147 }
148}
149
150/** Espacio con swipe horizontal para mover el cursor; toque = espacio. */
151@Composable
152private fun SpaceKey(modifier: Modifier, onSpace: () -> Unit, onCursor: (Int) -> Unit) {
153 val step = with(LocalDensity.current) { 16.dp.toPx() }
154 Box(
155 modifier
156 .fillMaxHeight()
157 .padding(3.dp)
158 .clip(RoundedCornerShape(14.dp))
159 .background(Tecla)
160 .pointerInput(Unit) {
161 awaitEachGesture {
162 val down = awaitFirstDown()
163 var accum = 0f
164 var moved = false
165 while (true) {
166 val event = awaitPointerEvent()
167 val change = event.changes.firstOrNull { it.id == down.id } ?: break
168 accum += change.positionChange().x
169 change.consume()
170 while (accum >= step) { onCursor(1); accum -= step; moved = true }
171 while (accum <= -step) { onCursor(-1); accum += step; moved = true }
172 if (!change.pressed) break
173 }
174 if (!moved) onSpace()
175 }
176 },
177 contentAlignment = Alignment.Center,
178 ) {
179 Text("espacio", color = Tenue, fontSize = 14.sp)
180 }
181}
182
183/** Borrado con repetición al mantener pulsado. */
184@Composable
185private fun BackspaceKey(modifier: Modifier, onBackspace: () -> Unit) {
186 val scope = rememberCoroutineScope()
187 Box(
188 modifier
189 .fillMaxHeight()
190 .padding(3.dp)
191 .clip(RoundedCornerShape(14.dp))
192 .background(Tecla)
193 .pointerInput(Unit) {
194 detectTapGestures(
195 onPress = {
196 onBackspace()
197 val job = scope.launch {
198 delay(380)
199 while (true) {
200 onBackspace()
201 delay(55)
202 }
203 }
204 try {
205 awaitRelease()
206 } finally {
207 job.cancel()
208 }
209 },
210 )
211 },
212 contentAlignment = Alignment.Center,
213 ) {
214 Text("⌫", color = Color.White, fontSize = 20.sp)
215 }
216}