gioser

sergio/humanoid

HTML 57Markdown 2TOML 10 Definiciones

⎇ b021d6829a602218866114f86e2350d80f0500e2 keyboard/src/main/java/com/rizoma/trazo/ui/KeyboardScreen.kt 216 líneas · 8.1 KB Historia crudo

Trazo: pulido v0.4 — háptica, palabra siguiente, emojis y ajustes b021d6829 · 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            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}