tawasuyu

sergio/humanoid

HTML 38Markdown 1TOML 10 Definitions

Trazo: arreglar cursor al reposicionar + mayúsculas en R↑ y columna reordenada

onUpdateSelection ahora detecta el toque del usuario y finaliza la composición
en su sitio (antes el siguiente carácter saltaba a la posición vieja). Las
mayúsculas pasan al swipe ↑ de la R; la columna derecha queda emoji / ?123 /
borrar / enter y el espacio ocupa toda la fila inferior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

5227fbf1abfb2a437968ebc63692560c3a929230 · by Sergio · (4 months ago) · parent 76f95ed72 · browse the tree at this commit

Semantic delta

No definition changed: this change is text, documentation or data.

5 files · +161 −62

diff --git a/keyboard/src/main/java/com/rizoma/trazo/TrazoImeService.kt b/keyboard/src/main/java/com/rizoma/trazo/TrazoImeService.kt
index 536c387..c3bddeb 100644
--- a/keyboard/src/main/java/com/rizoma/trazo/TrazoImeService.kt
+++ b/keyboard/src/main/java/com/rizoma/trazo/TrazoImeService.kt
@@ -144,11 +144,15 @@ class TrazoImeService : InputMethodService(), LifecycleOwner, ViewModelStoreOwne
         candidatesStart: Int, candidatesEnd: Int,
     ) {
         super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, candidatesStart, candidatesEnd)
-        // Si el editor ya no tiene región en composición pero nosotros creíamos tenerla,
-        // significa que el cursor se movió fuera: abandonamos la palabra local.
-        if (candidatesStart == -1 && controller.hasComposing()) {
-            controller.abandonComposing()
-        }
+        if (!controller.hasComposing()) return
+        // Tras NUESTRA propia edición, el caret queda justo al final de la región
+        // en composición (sin selección). Cualquier otra cosa —un toque en el
+        // texto, una selección, o que el editor cerrara la composición
+        // (candidatesEnd == -1)— significa que el cursor se movió por el usuario:
+        // cerramos la palabra para que lo siguiente caiga donde tocó.
+        val nuestraEdicion = newSelStart == newSelEnd &&
+            candidatesEnd != -1 && newSelEnd == candidatesEnd
+        if (!nuestraEdicion) controller.finishComposingForCursorMove()
     }
 
     override fun onFinishInputView(finishingInput: Boolean) {
diff --git a/keyboard/src/main/java/com/rizoma/trazo/ime/KeyboardController.kt b/keyboard/src/main/java/com/rizoma/trazo/ime/KeyboardController.kt
index 7e630e9..23fad00 100644
--- a/keyboard/src/main/java/com/rizoma/trazo/ime/KeyboardController.kt
+++ b/keyboard/src/main/java/com/rizoma/trazo/ime/KeyboardController.kt
@@ -175,12 +175,21 @@ class KeyboardController(
         lastWord = null
     }
 
-    /** El editor terminó la composición por su cuenta (cursor movido fuera, etc.). */
-    fun abandonComposing() {
-        if (composing.isEmpty()) return
-        composing.clear()
+    /**
+     * El cursor se movió por acción del usuario (un toque en el texto) con una
+     * palabra a medio componer. Finalizamos la composición EN SU SITIO con
+     * [ImeHost.finishComposing]: si no, el editor conserva la región vieja y el
+     * siguiente carácter la reemplazaría, devolviendo el cursor a la posición
+     * anterior. Tras cerrarla, lo que se escriba cae donde tocó el usuario.
+     */
+    fun finishComposingForCursorMove() {
+        if (composing.isNotEmpty()) {
+            host.finishComposing()
+            composing.clear()
+        }
         suggestions = emptyList()
         lastWord = null
+        syncAutoShift()
     }
 
     fun hasComposing(): Boolean = composing.isNotEmpty()
diff --git a/keyboard/src/main/java/com/rizoma/trazo/model/KeyLayout.kt b/keyboard/src/main/java/com/rizoma/trazo/model/KeyLayout.kt
index 4d1756c..9730fe7 100644
--- a/keyboard/src/main/java/com/rizoma/trazo/model/KeyLayout.kt
+++ b/keyboard/src/main/java/com/rizoma/trazo/model/KeyLayout.kt
@@ -13,6 +13,8 @@ enum class Dir { C, N, NE, E, SE, S, SW, W, NW }
 data class KeyDef(
     val center: Char,
     val swipes: Map<Dir, Char> = emptyMap(),
+    /** Dirección de swipe que activa mayúsculas en vez de emitir carácter (la R↑). */
+    val shiftDir: Dir? = null,
 ) {
     fun charFor(dir: Dir): Char? = if (dir == Dir.C) center else swipes[dir]
 }
@@ -20,52 +22,108 @@ data class KeyDef(
 object Layouts {
 
     /**
-     * Letras — layout MessagEase AUTÉNTICO (inglés), tomado de la fuente de Thumb-Key
-     * (sucesor FOSS de MessagEase). Toques A N I / H O R / T E S; las secundarias en su
-     * dirección de trazo original (V = A↘ hacia O, D = O↓, C = O←, etc.).
-     * Añadido para español: Ñ con trazo ↑ en la N. Puntuación frecuente en la E.
+     * Letras — layout MessagEase ESPAÑOL AUTÉNTICO, mapeado 1:1 desde el teclado
+     * oficial de Exideas (Spanish). Toques A N I / D O R / T E S.
+     *   · Acentos en swipe sobre su vocal: á(A↙) í(I↑) é(E←) ó(T↓) ú(T↑).
+     *   · ñ en N↑, l en N↓.  ü y ç en la tecla D.  v en A↘.
+     *   · O es el hub: q u p / c · b / g h j  (la h, antes tecla propia, va en O↓).
+     *   · Puntuación frecuente en la E. La R lleva mayús (shift) con trazo ↑ en la UI.
+     *
+     * Overlay de símbolos en modo letra: cada dirección SIN letra recibe el mismo símbolo
+     * que esa posición de tecla tiene en [symbols] (espejo posicional → el símbolo queda en
+     * el mismo sitio en ambas capas). Los que chocan con una letra viven solo en ?123.
+     * Excepción español: ¿ ¡ chocan con x/l, así que se reubican juntos en la tecla I.
      */
     val letters: List<KeyDef> = listOf(
-        // fila superior: A N I
-        KeyDef('a', mapOf(Dir.SE to 'v')),
-        KeyDef('n', mapOf(Dir.S to 'l', Dir.N to 'ñ')),
-        KeyDef('i', mapOf(Dir.SW to 'x')),
-        // fila media: H O R
-        KeyDef('h', mapOf(Dir.E to 'k')),
+        // fila superior: A N I   (símbolos espejo de 1 2 3)
+        KeyDef('a', mapOf(Dir.S to 'á', Dir.SE to 'v', Dir.SW to '$', Dir.E to '-')),
+        KeyDef(
+            'n',
+            mapOf(
+                Dir.N to 'ñ', Dir.S to 'l',
+                Dir.NW to '`', Dir.NE to '´', Dir.W to '+', Dir.E to '!', Dir.SW to '/', Dir.SE to '\\',
+            ),
+        ),
+        KeyDef(
+            'i',
+            mapOf(
+                Dir.NW to 'í', Dir.SW to 'x',
+                Dir.W to '?', Dir.S to '=', Dir.SE to '€', Dir.E to '¿', Dir.NE to '¡',
+            ),
+        ),
+        // fila media: D O R   (símbolos espejo de 4 5 6)
+        KeyDef(
+            'd',
+            mapOf(
+                Dir.N to 'ü', Dir.E to 'k', Dir.S to 'ç',
+                Dir.NW to '{', Dir.NE to '%', Dir.W to '(', Dir.SW to '[', Dir.SE to '_',
+            ),
+        ),
         KeyDef(
             'o',
             mapOf(
                 Dir.NW to 'q', Dir.N to 'u', Dir.NE to 'p', Dir.E to 'b',
-                Dir.SE to 'j', Dir.S to 'd', Dir.SW to 'g', Dir.W to 'c',
+                Dir.SE to 'j', Dir.S to 'h', Dir.SW to 'g', Dir.W to 'c',
+            ),
+        ),
+        KeyDef(
+            'r',
+            mapOf(
+                Dir.W to 'm',
+                Dir.NW to '|', Dir.NE to '}', Dir.E to ')', Dir.SW to '@', Dir.SE to ']',
+            ),
+            shiftDir = Dir.N, // R↑ = mayúsculas (OFF→ON→LOCK)
+        ),
+        // fila inferior: T E S   (símbolos espejo de 7 8 9)
+        KeyDef(
+            't',
+            mapOf(
+                Dir.N to 'ú', Dir.NE to 'y', Dir.S to 'ó',
+                Dir.NW to '~', Dir.W to '<', Dir.E to '*', Dir.SE to '\t',
             ),
         ),
-        KeyDef('r', mapOf(Dir.W to 'm')),
-        // fila inferior: T E S
-        KeyDef('t', mapOf(Dir.NE to 'y')),
         KeyDef(
             'e',
             mapOf(
-                Dir.N to 'w', Dir.NE to '\'', Dir.E to 'z',
-                Dir.SE to ':', Dir.S to '.', Dir.SW to ',',
+                Dir.N to 'w', Dir.W to 'é', Dir.E to 'z',
+                Dir.NE to '\'', Dir.SE to ':', Dir.S to '.', Dir.SW to ',', Dir.NW to '"',
+            ),
+        ),
+        KeyDef(
+            's',
+            mapOf(
+                Dir.NW to 'f', Dir.N to '&',
+                Dir.NE to '°', Dir.W to '#', Dir.E to '>', Dir.SW to ';',
             ),
         ),
-        KeyDef('s', mapOf(Dir.NW to 'f')),
     )
 
     /**
-     * Números y símbolos. Dígitos 1-9 en posición de marcador telefónico; 0 y la
-     * puntuación frecuente como swipes.
+     * Números y símbolos — capa MessagEase ESPAÑOL AUTÉNTICA (captura oficial de Exideas).
+     * Dígitos 1-9 en marcador telefónico; cada símbolo en su dirección de trazo original.
+     * Desviaciones respecto a la captura (esta app no tiene celda 0 dedicada como MessagEase):
+     *   · 0 → 5↓ (la tecla 5 va libre; toca 5 y desliza abajo).
+     *   · ¿ ¡ no salen en la captura pero el español los exige: ¡ en 2↓, ¿ en 3↙.
      */
     val symbols: List<KeyDef> = listOf(
-        KeyDef('1', mapOf(Dir.N to '!', Dir.E to '@')),
-        KeyDef('2', mapOf(Dir.N to '"', Dir.E to '#')),
-        KeyDef('3', mapOf(Dir.N to '\'', Dir.W to '$')),
-        KeyDef('4', mapOf(Dir.N to '%', Dir.E to '&')),
-        KeyDef('5', mapOf(Dir.N to '*', Dir.E to '+', Dir.W to '-', Dir.S to '=')),
-        KeyDef('6', mapOf(Dir.N to '/', Dir.W to '\\')),
-        KeyDef('7', mapOf(Dir.N to '(', Dir.E to ')')),
-        KeyDef('8', mapOf(Dir.N to ',', Dir.E to '.', Dir.W to '?')),
-        KeyDef('9', mapOf(Dir.N to ';', Dir.W to ':', Dir.E to '0')),
+        // fila 1: 1 2 3
+        KeyDef('1', mapOf(Dir.E to '-', Dir.SW to '$')),
+        KeyDef(
+            '2',
+            mapOf(
+                Dir.NW to '`', Dir.N to '^', Dir.NE to '´', Dir.W to '+',
+                Dir.E to '!', Dir.S to '¡', Dir.SW to '/', Dir.SE to '\\',
+            ),
+        ),
+        KeyDef('3', mapOf(Dir.W to '?', Dir.SW to '¿', Dir.S to '=', Dir.SE to '€')),
+        // fila 2: 4 5 6
+        KeyDef('4', mapOf(Dir.NW to '{', Dir.NE to '%', Dir.W to '(', Dir.SW to '[', Dir.SE to '_')),
+        KeyDef('5', mapOf(Dir.S to '0')),
+        KeyDef('6', mapOf(Dir.NW to '|', Dir.NE to '}', Dir.E to ')', Dir.SW to '@', Dir.SE to ']')),
+        // fila 3: 7 8 9
+        KeyDef('7', mapOf(Dir.NW to '~', Dir.N to '¨', Dir.W to '<', Dir.E to '*', Dir.SE to '\t')),
+        KeyDef('8', mapOf(Dir.NW to '"', Dir.NE to '\'', Dir.SW to ',', Dir.S to '.', Dir.SE to ':')),
+        KeyDef('9', mapOf(Dir.N to '&', Dir.NE to '°', Dir.W to '#', Dir.E to '>', Dir.SW to ';')),
     )
 }
 
diff --git a/keyboard/src/main/java/com/rizoma/trazo/ui/KeyCell.kt b/keyboard/src/main/java/com/rizoma/trazo/ui/KeyCell.kt
index b59bbaf..2c654a6 100644
--- a/keyboard/src/main/java/com/rizoma/trazo/ui/KeyCell.kt
+++ b/keyboard/src/main/java/com/rizoma/trazo/ui/KeyCell.kt
@@ -13,6 +13,7 @@ import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.text.font.FontWeight
@@ -41,6 +42,8 @@ fun KeyCell(
     key: KeyDef,
     shifted: Boolean,
     onChar: (Char) -> Unit,
+    onShift: () -> Unit = {},
+    shiftAccent: Color = Tenue,
     modifier: Modifier = Modifier,
 ) {
     val tapRadiusPx = with(LocalDensity.current) { 22.dp.toPx() }
@@ -65,7 +68,8 @@ fun KeyCell(
                     val dx = current.x - down.position.x
                     val dy = current.y - down.position.y
                     val dir = directionOf(dx, dy, tapRadiusPx)
-                    key.charFor(dir)?.let { raw -> onChar(raw) }
+                    if (key.shiftDir != null && dir == key.shiftDir) onShift()
+                    else key.charFor(dir)?.let { raw -> onChar(raw) }
                 }
             },
     ) {
@@ -79,6 +83,18 @@ fun KeyCell(
                 modifier = Modifier.align(align).padding(horizontal = 6.dp, vertical = 3.dp),
             )
         }
+        // Pista de mayúsculas (R↑): coloreada según el estado del shift.
+        key.shiftDir?.let { d ->
+            hintAlignments[d]?.let { align ->
+                Text(
+                    text = "⇧",
+                    color = shiftAccent,
+                    fontSize = 13.sp,
+                    fontWeight = FontWeight.Bold,
+                    modifier = Modifier.align(align).padding(horizontal = 6.dp, vertical = 3.dp),
+                )
+            }
+        }
         // Carácter central.
         Text(
             text = glyph(key.center, shifted),
diff --git a/keyboard/src/main/java/com/rizoma/trazo/ui/KeyboardScreen.kt b/keyboard/src/main/java/com/rizoma/trazo/ui/KeyboardScreen.kt
index abbf561..4801628 100644
--- a/keyboard/src/main/java/com/rizoma/trazo/ui/KeyboardScreen.kt
+++ b/keyboard/src/main/java/com/rizoma/trazo/ui/KeyboardScreen.kt
@@ -66,34 +66,46 @@ fun KeyboardScreen(controller: KeyboardController, settings: TrazoSettings) {
             }
         } else {
             val keys = controller.keys
-            for (row in 0 until 3) {
-                Row(Modifier.fillMaxWidth().height(70.dp)) {
-                    for (col in 0 until 3) {
-                        KeyCell(
-                            key = keys[row * 3 + col],
-                            shifted = controller.shifted,
-                            onChar = controller::onChar,
-                            modifier = Modifier.weight(1f).fillMaxHeight(),
-                        )
+            // Color de la pista de mayúsculas (R↑) según el estado del shift.
+            val shiftAccent = when (controller.shift) {
+                ShiftState.OFF -> Tenue
+                ShiftState.ON -> NeonVerde
+                ShiftState.LOCK -> NeonRosa
+            }
+            // Grid 3×3 (izquierda, ~¾) + columna de funciones (derecha, ~¼), como MessagEase.
+            Row(Modifier.fillMaxWidth().height(210.dp)) {
+                Column(Modifier.weight(3f).fillMaxHeight()) {
+                    for (row in 0 until 3) {
+                        Row(Modifier.fillMaxWidth().weight(1f)) {
+                            for (col in 0 until 3) {
+                                KeyCell(
+                                    key = keys[row * 3 + col],
+                                    shifted = controller.shifted,
+                                    onChar = controller::onChar,
+                                    onShift = controller::onShift,
+                                    shiftAccent = shiftAccent,
+                                    modifier = Modifier.weight(1f).fillMaxHeight(),
+                                )
+                            }
+                        }
                     }
                 }
+                // Columna derecha, arriba→abajo: emoticons, núm/alfa, borrar, enter.
+                // (Mayúsculas ya no va aquí: vive en el swipe ↑ de la R.)
+                Column(Modifier.weight(1f).fillMaxHeight()) {
+                    FuncKey("😊", NeonRosa, Modifier.fillMaxWidth().weight(1f), onClick = controller::showEmojis)
+                    FuncKey(
+                        if (controller.layer == Layer.LETTERS) "?123" else "ABC",
+                        NeonCian, Modifier.fillMaxWidth().weight(1f), onClick = controller::toggleLayer,
+                    )
+                    BackspaceKey(Modifier.fillMaxWidth().weight(1f), onBackspace = controller::onBackspace)
+                    FuncKey("↵", NeonVerde, Modifier.fillMaxWidth().weight(1f), onClick = controller::onEnter)
+                }
             }
 
-            Row(Modifier.fillMaxWidth().height(52.dp)) {
-                val shiftAccent = when (controller.shift) {
-                    ShiftState.OFF -> Tenue
-                    ShiftState.ON -> NeonVerde
-                    ShiftState.LOCK -> NeonRosa
-                }
-                FuncKey("⇧", shiftAccent, Modifier.weight(1f), onClick = controller::onShift)
-                FuncKey(
-                    if (controller.layer == Layer.LETTERS) "?123" else "ABC",
-                    NeonCian, Modifier.weight(1f), onClick = controller::toggleLayer,
-                )
-                FuncKey("😊", NeonRosa, Modifier.weight(1f), onClick = controller::showEmojis)
-                SpaceKey(Modifier.weight(3f), onSpace = controller::onSpace, onCursor = controller::moveCursor)
-                BackspaceKey(Modifier.weight(1f), onBackspace = controller::onBackspace)
-                FuncKey("↵", NeonVerde, Modifier.weight(1f), onClick = controller::onEnter)
+            // Fila inferior: barra de espacio a todo el ancho.
+            Row(Modifier.fillMaxWidth().height(56.dp)) {
+                SpaceKey(Modifier.weight(1f), onSpace = controller::onSpace, onCursor = controller::moveCursor)
             }
         }
     }