tawasuyu

sergio/humanoid

HTML 38Markdown 1TOML 10 Sutichaykuna

⎇ 5227fbf1abfb2a437968ebc63692560c3a929230 keyboard/src/main/java/com/rizoma/trazo/ui/KeyboardScreen.kt 228 siq'ikuna · 8.9 KB Ñawpa willay ch'uya

Trazo: arreglar cursor al reposicionar + mayúsculas en R↑ y columna reordenada 5227fbf1a · 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.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}