mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-01 10:21:59 +00:00
feat(ui-new): a cursor mode that keeps the keyboard in view
The cursor marks the character it stands before instead of pushing the text apart: underlined while typing, an inverted block while moving it. The grid stays on screen with the special row turned into the key hints; Up/Down move a word, Hold-Enter deletes before the cursor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -208,11 +208,14 @@ shows Latin instead (see `KB_ACCENT_VARIANTS` below).
|
||||
Shift is one-shot by default (capitalises the next letter, including whichever
|
||||
candidate a T9 multi-tap cycle settles on) or Hold-Enter to toggle caps-lock;
|
||||
Hold-Clear erases the whole field. **UP from the top letter row** enters
|
||||
**cursor mode** (LEFT/RIGHT move the insertion point; UP/DOWN jump to
|
||||
start/end, then — pressed again once already at that boundary — continue on
|
||||
**cursor mode** (LEFT/RIGHT move the insertion point a codepoint; UP/DOWN a
|
||||
word back / on, then — pressed again once already at that end — continue on
|
||||
to the special row / letter grid, the same destinations the plain grid wrap
|
||||
used to reach directly) so edits/inserts can target any point in the typed
|
||||
text, not just the end; Enter/Cancel exit immediately from anywhere.
|
||||
text, not just the end. The grid stays drawn, unselected, with the special
|
||||
row replaced by the key hints; the cursor is an inverted cell instead of the
|
||||
typing underline. Hold-Enter deletes before the cursor without leaving;
|
||||
Enter/Cancel exit immediately from anywhere.
|
||||
Hold-Enter on a Latin-page letter cell with accented variants instead opens
|
||||
the **accent popup**: one horizontal row of `KB_ACCENT_VARIANTS[group]`
|
||||
(a UTF-8 string per base letter, same shape as a T9 group string), LEFT/RIGHT
|
||||
|
||||
+22
-1
@@ -73,7 +73,28 @@ history). Pick a quick message or **Custom message** for the keyboard.
|
||||
### The keyboard
|
||||
|
||||
The on-screen keyboard is a letter grid (or a phone-style T9 keypad, Settings ›
|
||||
Keyboard › Layout). Up from the top row moves the cursor through the text.
|
||||
Keyboard › Layout).
|
||||
|
||||
#### Moving the cursor
|
||||
|
||||
While you type, a line under the text marks where the next letter goes. To
|
||||
fix something earlier in the message, press **Up** from the top row of keys.
|
||||
The cursor turns into a block on the letter it stands before, the keys stay
|
||||
on screen without a selection, and the bottom row shows what the arrows do
|
||||
now (`<> char ^v word`):
|
||||
|
||||
| Key | Does |
|
||||
| --- | ---- |
|
||||
| **Left / Right** | Moves one letter |
|
||||
| **Up** | Back to the start of the word (the one before, if already there) |
|
||||
| **Down** | On to the end of the word (the next one, if already there) |
|
||||
| **Hold Enter** | Deletes the letter before the cursor |
|
||||
| **Enter**, **Back** | Back to the keys; typing carries on at the cursor |
|
||||
|
||||
Up at the very start of the text goes to the bottom row of keys, and Down at
|
||||
the very end goes back to the top row. So if you pressed Up by mistake, one
|
||||
Down returns you where you were. The text never shifts around the cursor:
|
||||
what you see is what will be sent.
|
||||
|
||||
Accented letters don't need a language setting: **hold Enter** on the base
|
||||
letter and pick from its variants, for example `a` → `á à â ä å ą…`, `z` →
|
||||
|
||||
@@ -532,6 +532,29 @@ struct KeyboardWidget {
|
||||
else if (key == KEY_DOWN) { cursor_pos = len; }
|
||||
}
|
||||
|
||||
// Word steps for cursor mode: back to the start of the word before the
|
||||
// cursor, on to the end of the one after it. Spaces are ASCII, so every stop
|
||||
// is a codepoint boundary.
|
||||
int wordStartBefore(int p) const {
|
||||
while (p > 0 && buf[p - 1] == ' ') p--;
|
||||
while (p > 0 && buf[p - 1] != ' ') p--;
|
||||
return p;
|
||||
}
|
||||
int wordEndAfter(int p) const {
|
||||
while (p < len && buf[p] == ' ') p++;
|
||||
while (p < len && buf[p] != ' ') p++;
|
||||
return p;
|
||||
}
|
||||
|
||||
void deleteBeforeCursor() {
|
||||
t9_cell = -1;
|
||||
if (cursor_pos <= 0) return;
|
||||
int n = kbUtf8LastCharBytes(buf, cursor_pos);
|
||||
memmove(buf + cursor_pos - n, buf + cursor_pos, len - cursor_pos);
|
||||
len -= n; cursor_pos -= n;
|
||||
buf[len] = '\0';
|
||||
}
|
||||
|
||||
// PIN preview: what it's for, centred, and the digits below it spaced out
|
||||
// (filled soft squares when masked), with an underline on the next slot.
|
||||
void renderPinPreview(DisplayDriver& display, int sep_y) {
|
||||
@@ -647,16 +670,8 @@ struct KeyboardWidget {
|
||||
// wrap boundary, where cursor_line has already moved to the next row.
|
||||
bool cursor_here = (first_line + pl == cursor_line);
|
||||
int line_end = (len < pe) ? len : pe;
|
||||
char linebuf[KB_PREVIEW_BYTES + 2]; // cpl codepoints + cursor '_' + NUL
|
||||
if (cursor_here) {
|
||||
// Cursor drawn as an inserted '_' between whatever text precedes and
|
||||
// follows it on this line -- reduces to the old "text + trailing _"
|
||||
// when cursor_pos == len (after_n is always 0 in that case).
|
||||
int before_n = cursor_pos - ps; if (before_n < 0) before_n = 0;
|
||||
if (before_n > line_end - ps) before_n = line_end - ps;
|
||||
int after_n = line_end - cursor_pos; if (after_n < 0) after_n = 0;
|
||||
snprintf(linebuf, sizeof(linebuf), "%.*s_%.*s", before_n, buf + ps, after_n, buf + ps + before_n);
|
||||
} else if (len > ps) {
|
||||
char linebuf[KB_PREVIEW_BYTES + 2];
|
||||
if (len > ps) {
|
||||
snprintf(linebuf, sizeof(linebuf), "%.*s", line_end - ps, buf + ps);
|
||||
} else {
|
||||
linebuf[0] = '\0';
|
||||
@@ -667,9 +682,8 @@ struct KeyboardWidget {
|
||||
int mi = 0;
|
||||
int blen = (int)strlen(linebuf);
|
||||
for (int bi = 0; bi < blen; ) {
|
||||
int u = kbUtf8CharBytesAt(linebuf, bi, blen);
|
||||
masked[mi++] = (u == 1 && linebuf[bi] == '_') ? '_' : '*';
|
||||
bi += u;
|
||||
masked[mi++] = '*';
|
||||
bi += kbUtf8CharBytesAt(linebuf, bi, blen);
|
||||
}
|
||||
masked[mi] = '\0';
|
||||
strncpy(linebuf, masked, sizeof(linebuf));
|
||||
@@ -678,6 +692,27 @@ struct KeyboardWidget {
|
||||
display.translateUTF8ToBlocks(linebuf_t, linebuf, sizeof(linebuf_t));
|
||||
display.setCursor(0, pl * lh);
|
||||
display.print(linebuf_t);
|
||||
// The cursor marks the character it stands before, so the text never
|
||||
// shifts around it: underlined while typing, inverted while moving it.
|
||||
if (cursor_here) {
|
||||
int at = 0;
|
||||
for (int p = ps; p < cursor_pos; ) { p += kbUtf8CharBytesAt(buf, p, len); at++; }
|
||||
const int bx = at * cw, by = pl * lh;
|
||||
if (cursor_mode) {
|
||||
display.fillRect(bx, by, cw, lh);
|
||||
if (cursor_pos < len && !pin_kb_mask_enabled) {
|
||||
char one[5], one_t[8];
|
||||
kbUtf8CharAt(buf + cursor_pos, 0, one);
|
||||
display.translateUTF8ToBlocks(one_t, one, sizeof(one_t));
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
display.setCursor(bx, by);
|
||||
display.print(one_t);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
}
|
||||
} else {
|
||||
display.fillRect(bx, by + lh - display.sepH(), cw - display.sepH(), display.sepH());
|
||||
}
|
||||
}
|
||||
ps = pe;
|
||||
}
|
||||
if (count[0]) {
|
||||
@@ -713,22 +748,6 @@ struct KeyboardWidget {
|
||||
}
|
||||
display.fillRect(0, sep_y, display.width(), display.sepH());
|
||||
|
||||
// Cursor-positioning mode: LEFT/RIGHT/UP/DOWN now drive the text cursor
|
||||
// instead of the grid (see handleInput), so the grid would otherwise just
|
||||
// sit there frozen with no sign anything's different. Replace it with an
|
||||
// explicit hint instead.
|
||||
if (cursor_mode) {
|
||||
const int hh = lh + 2;
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.fillRect(0, chars_y, display.width(), hh);
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
display.drawTextCentered(display.width() / 2, chars_y + 1, "Cursor mode");
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawTextCentered(display.width() / 2, chars_y + hh + 2, "L/R move");
|
||||
display.drawTextCentered(display.width() / 2, chars_y + hh + 2 + lh, "U/D start/end");
|
||||
return 50;
|
||||
}
|
||||
|
||||
// PIN mode
|
||||
if (isPin()) {
|
||||
const int s = miniIconScale(display);
|
||||
@@ -778,15 +797,19 @@ struct KeyboardWidget {
|
||||
// just won't be visible on this screen which cell is selected.
|
||||
if (compact_ui) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawTextCentered(display.width() / 2, chars_y, "Tab: placeholders");
|
||||
display.drawTextCentered(display.width() / 2, chars_y + lh, "Fn+letter: accent");
|
||||
if (cursor_mode) {
|
||||
display.drawTextCentered(display.width() / 2, chars_y, "<> char ^v word");
|
||||
} else {
|
||||
display.drawTextCentered(display.width() / 2, chars_y, "Tab: placeholders");
|
||||
display.drawTextCentered(display.width() / 2, chars_y + lh, "Fn+letter: accent");
|
||||
}
|
||||
} else {
|
||||
// character grid
|
||||
if (isT9()) {
|
||||
for (int r = 0; r < rows; r++) {
|
||||
int y = chars_y + r * cell_h;
|
||||
for (int c = 0; c < cols; c++) {
|
||||
bool sel = (row == r && col == c);
|
||||
bool sel = !cursor_mode && row == r && col == c;
|
||||
int cell = r * cols + c;
|
||||
// Label the cell "<digit><group>" so it reads like a phone keypad. The
|
||||
// digit is what the multi-tap cycle lands on after the letters (see
|
||||
@@ -808,7 +831,7 @@ struct KeyboardWidget {
|
||||
for (int r = 0; r < rows; r++) {
|
||||
int y = chars_y + r * cell_h;
|
||||
for (int c = 0; c < cols; c++) {
|
||||
bool sel = (row == r && col == c);
|
||||
bool sel = !cursor_mode && row == r && col == c;
|
||||
char ch_buf[3];
|
||||
kbApplyCapsUtf8(cellStr(r, c), caps, ch_buf, sizeof(ch_buf));
|
||||
if (ch_buf[0] == ' ' && ch_buf[1] == '\0') ch_buf[0] = '_';
|
||||
@@ -821,6 +844,12 @@ struct KeyboardWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// Cursor mode: the arrows drive the text cursor (see handleKey), so the
|
||||
// grid stays as it was, unselected, and the special row gives way to
|
||||
// what the keys do now.
|
||||
if (cursor_mode) {
|
||||
display.drawTextCentered(display.width() / 2, spec_y + key_ty, "<> char ^v word");
|
||||
} else {
|
||||
// special row: caps ⇧ · space ⎵ · delete ⌫ · placeholders {} (text) · OK ✓
|
||||
const int s = miniIconScale(display);
|
||||
for (int i = 0; i < KB_SPECIAL; i++) {
|
||||
@@ -863,6 +892,7 @@ struct KeyboardWidget {
|
||||
}
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Accent popup: floats over the still-visible grid (same idea as the
|
||||
@@ -985,29 +1015,29 @@ struct KeyboardWidget {
|
||||
|
||||
// Cursor-positioning sub-mode (see the KEY_UP block below): the grid
|
||||
// selection is parked while LEFT/RIGHT walk cursor_pos one codepoint at a
|
||||
// time. UP/DOWN jump to the very start/end -- Home/End, in effect -- and,
|
||||
// once already at that boundary, continue the wrap the entry trigger
|
||||
// interrupted: UP again lands on the special row, DOWN again back on the
|
||||
// letter grid's row 0, same destinations the plain grid wrap used to reach
|
||||
// directly (see the entry/exit comment below). Enter/Cancel just leave the
|
||||
// mode from anywhere; the actual edit (insert/backspace) happens back in
|
||||
// normal typing, now targeting the repositioned cursor.
|
||||
// time and UP/DOWN a word back / on. Once already at that end, UP and
|
||||
// DOWN continue the wrap the entry trigger interrupted: UP again lands on
|
||||
// the special row, DOWN again back on the letter grid's row 0 (so DOWN
|
||||
// right after entering, the cursor still at the end, is the way back).
|
||||
// Hold-Enter deletes before the cursor without leaving; Enter/Cancel
|
||||
// leave the mode from anywhere, typing resuming at the cursor.
|
||||
if (cursor_mode) {
|
||||
if (c == KEY_LEFT) { if (cursor_pos > 0) cursor_pos -= kbUtf8LastCharBytes(buf, cursor_pos); return NONE; }
|
||||
if (c == KEY_RIGHT) { if (cursor_pos < len) cursor_pos += kbUtf8CharBytesAt(buf, cursor_pos, len); return NONE; }
|
||||
if (c == KEY_UP) {
|
||||
if (cursor_pos > 0) { cursor_pos = 0; return NONE; }
|
||||
if (cursor_pos > 0) { cursor_pos = wordStartBefore(cursor_pos); return NONE; }
|
||||
cursor_mode = false;
|
||||
row = gridRows();
|
||||
col = col * KB_SPECIAL / gridCols();
|
||||
return NONE;
|
||||
}
|
||||
if (c == KEY_DOWN) {
|
||||
if (cursor_pos < len) { cursor_pos = len; return NONE; }
|
||||
if (cursor_pos < len) { cursor_pos = wordEndAfter(cursor_pos); return NONE; }
|
||||
cursor_mode = false;
|
||||
row = 0;
|
||||
return NONE;
|
||||
}
|
||||
if (c == KEY_CONTEXT_MENU || c == 0x08) { deleteBeforeCursor(); return NONE; }
|
||||
// KEY_KB_ENTER (external keyboard's Fn+Enter) leaves the mode too rather
|
||||
// than being silently eaten -- it's the one key an external-keyboard
|
||||
// typist would reach for here, and cursor mode is only ever entered from
|
||||
|
||||
Reference in New Issue
Block a user