tawasuyu

sergio/humanoid

HTML 38Markdown 1TOML 10 Definiciones

⎇ 8380e44a0947bafb169773353ce2b2036aff9e70 keyboard/src/main/java/com/rizoma/trazo/ui/KeyboardScreen.kt 198 líneas · 7.0 KB Historia crudo

Trazo: predicción, aprendizaje y gestos de espacio/borrado (v0.2) 8380e44a0 · sergio ·

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}