tawasuyu

sergio/humanoid

HTML 44Markdown 1TOML 10 Definiciones

⎇ f809af7f4e2025f1b8c420daa6cf585dd2b7966e keyboard/src/main/java/com/rizoma/trazo/ui/KeyboardScreen.kt 201 líneas · 7.2 KB Historia crudo

Trazo: layout MessagEase auténtico + fila de funciones sobre la barra de nav f809af7f4 · 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.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}