mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-26 14:58:12 +00:00
feat(ui): message history newest-at-bottom; keyboard one-shot Shift + hold-clear
Message history (DM + channel) now stacks bubbles upward from the compose row instead of the header, so the newest message sits at the bottom like a typical messenger, with older messages progressively above. UP/DOWN and the scrollbar direction are swapped to match; the newest bubble now keeps the same breathing-room gap against the compose row as it does against another bubble, instead of touching it. Keyboard: Shift is one-shot by default (capitalises just the next letter, then reverts), with Hold-Enter on Shift toggling a persistent caps-lock for the old sticky behavior. Hold-Enter on Backspace clears the whole field in one action. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -319,6 +319,10 @@ struct KeyboardWidget {
|
|||||||
int row, col;
|
int row, col;
|
||||||
int page; // see totalPages()/pageIsAltAlphabet()/pageIsSymbols() below
|
int page; // see totalPages()/pageIsAltAlphabet()/pageIsSymbols() below
|
||||||
bool caps;
|
bool caps;
|
||||||
|
// Shift is one-shot by default (like a phone keyboard: capitalises just the
|
||||||
|
// next letter, then reverts) — Hold-Enter on Shift toggles caps_lock, which
|
||||||
|
// keeps it on for a whole run of capitals instead.
|
||||||
|
bool caps_lock = false;
|
||||||
char _ph_buf[KB_PH_MAX][KB_PH_LEN];
|
char _ph_buf[KB_PH_MAX][KB_PH_LEN];
|
||||||
int _ph_count;
|
int _ph_count;
|
||||||
PopupMenu _ph_menu;
|
PopupMenu _ph_menu;
|
||||||
@@ -433,6 +437,7 @@ struct KeyboardWidget {
|
|||||||
row = col = 0;
|
row = col = 0;
|
||||||
page = 0;
|
page = 0;
|
||||||
caps = false;
|
caps = false;
|
||||||
|
caps_lock = false;
|
||||||
t9_cell = -1;
|
t9_cell = -1;
|
||||||
t9_cycle = 0;
|
t9_cycle = 0;
|
||||||
_ph_menu.active = false;
|
_ph_menu.active = false;
|
||||||
@@ -617,11 +622,30 @@ struct KeyboardWidget {
|
|||||||
return NONE;
|
return NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (c == KEY_CANCEL || c == KEY_CONTEXT_MENU) return CANCELLED;
|
if (c == KEY_CANCEL) return CANCELLED;
|
||||||
|
|
||||||
const int rows = gridRows();
|
const int rows = gridRows();
|
||||||
const int cols = gridCols();
|
const int cols = gridCols();
|
||||||
|
|
||||||
|
// Hold-Enter is normally "cancel", but over the two keys where a long-press
|
||||||
|
// has its own well-known meaning on a phone keyboard, it does that instead:
|
||||||
|
// Shift -> toggle a persistent caps-lock (a plain tap is one-shot -- see the
|
||||||
|
// commit sites below); Backspace -> clear the whole field in one action
|
||||||
|
// instead of holding it down. Every other cell keeps hold-to-cancel.
|
||||||
|
if (c == KEY_CONTEXT_MENU) {
|
||||||
|
if (row == rows && col == 0) { // Shift
|
||||||
|
caps_lock = !caps_lock;
|
||||||
|
caps = caps_lock;
|
||||||
|
return NONE;
|
||||||
|
}
|
||||||
|
if (row == rows && col == 2) { // Backspace
|
||||||
|
len = 0; buf[0] = '\0';
|
||||||
|
t9_cell = -1;
|
||||||
|
return NONE;
|
||||||
|
}
|
||||||
|
return CANCELLED;
|
||||||
|
}
|
||||||
|
|
||||||
if (c == KEY_UP) {
|
if (c == KEY_UP) {
|
||||||
if (row > 0) {
|
if (row > 0) {
|
||||||
row--;
|
row--;
|
||||||
@@ -692,6 +716,7 @@ struct KeyboardWidget {
|
|||||||
buf[len] = '\0';
|
buf[len] = '\0';
|
||||||
t9_cell = cell;
|
t9_cell = cell;
|
||||||
t9_cycle = 0;
|
t9_cycle = 0;
|
||||||
|
if (caps && !caps_lock) caps = false; // one-shot: only this first tap gets capitalised
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
t9_last_ms = millis();
|
t9_last_ms = millis();
|
||||||
@@ -703,11 +728,14 @@ struct KeyboardWidget {
|
|||||||
memcpy(buf + len, shown, n);
|
memcpy(buf + len, shown, n);
|
||||||
len += n;
|
len += n;
|
||||||
buf[len] = '\0';
|
buf[len] = '\0';
|
||||||
|
if (caps && !caps_lock) caps = false; // one-shot: revert after the letter it capitalised
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
t9_cell = -1; // any special-row action finalizes a pending multi-tap cycle
|
t9_cell = -1; // any special-row action finalizes a pending multi-tap cycle
|
||||||
switch (col) {
|
switch (col) {
|
||||||
case 0: caps = !caps; break;
|
// Tap toggles one-shot caps on/off; while caps_lock is held (Hold-Enter
|
||||||
|
// on this key, see handleInput's top), a tap cancels the lock instead.
|
||||||
|
case 0: if (caps_lock) { caps = false; caps_lock = false; } else { caps = !caps; } break;
|
||||||
case 1:
|
case 1:
|
||||||
if (len < max_len) { buf[len++] = ' '; buf[len] = '\0'; }
|
if (len < max_len) { buf[len++] = ' '; buf[len] = '\0'; }
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1024,8 +1024,14 @@ public:
|
|||||||
});
|
});
|
||||||
int reserve = hs.reserve;
|
int reserve = hs.reserve;
|
||||||
{
|
{
|
||||||
|
// Stack boxes upward from just above the compose row, so item 0 (the
|
||||||
|
// newest message) lands at the bottom of the list and older messages
|
||||||
|
// sit progressively higher — same anchor convention as a typical
|
||||||
|
// messenger, instead of newest-at-top. box_ys[i] still corresponds
|
||||||
|
// to item (_dm_hist_scroll + i), same as before; only its y flips.
|
||||||
const int fixed_bh = 2 * lh + 1;
|
const int fixed_bh = 2 * lh + 1;
|
||||||
int cur_y = hist_start_y;
|
const int box_gap = portrait_expand ? 2 : 1;
|
||||||
|
int cur_y = cby - box_gap; // reserve the same gap against compose as between boxes
|
||||||
for (int ii = 0; ii < MAX_VIS_BOXES && (_dm_hist_scroll + ii) < dm_count; ii++) {
|
for (int ii = 0; ii < MAX_VIS_BOXES && (_dm_hist_scroll + ii) < dm_count; ii++) {
|
||||||
int bh = fixed_bh;
|
int bh = fixed_bh;
|
||||||
if (portrait_expand) {
|
if (portrait_expand) {
|
||||||
@@ -1038,9 +1044,10 @@ public:
|
|||||||
bh = (1 + (nl > 0 ? nl : 1)) * lh + 1;
|
bh = (1 + (nl > 0 ? nl : 1)) * lh + 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (cur_y + bh > cby) break;
|
int box_top = cur_y - bh;
|
||||||
box_ys[n_vis] = cur_y; box_hs[n_vis] = bh; n_vis++;
|
if (box_top < hist_start_y) break;
|
||||||
cur_y += bh + (portrait_expand ? 2 : 1);
|
box_ys[n_vis] = box_top; box_hs[n_vis] = bh; n_vis++;
|
||||||
|
cur_y = box_top - box_gap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_hist_visible = n_vis;
|
_hist_visible = n_vis;
|
||||||
@@ -1085,9 +1092,16 @@ public:
|
|||||||
|
|
||||||
// Scrollbar: track pinned to the full list area; thumb sized/positioned
|
// Scrollbar: track pinned to the full list area; thumb sized/positioned
|
||||||
// from hs's pixel metrics (stable while scrolling the same list).
|
// from hs's pixel metrics (stable while scrolling the same list).
|
||||||
if (hs.need)
|
// hs.scroll_px counts index-space distance from item 0 (newest); since
|
||||||
|
// item 0 now renders at the bottom, invert it here so the thumb sits at
|
||||||
|
// the bottom for the newest and rises as you scroll into older history.
|
||||||
|
if (hs.need) {
|
||||||
|
long span = hs.total_px - hs.view_px;
|
||||||
|
long inv_scroll_px = span - hs.scroll_px;
|
||||||
|
if (inv_scroll_px < 0) inv_scroll_px = 0;
|
||||||
drawScrollIndicatorPx(display, hist_start_y + 1, hs.view_px,
|
drawScrollIndicatorPx(display, hist_start_y + 1, hs.view_px,
|
||||||
hs.total_px, hs.view_px, hs.scroll_px);
|
hs.total_px, hs.view_px, inv_scroll_px);
|
||||||
|
}
|
||||||
|
|
||||||
drawComposeButton(display, cby, lh, _dm_hist_sel == -1);
|
drawComposeButton(display, cby, lh, _dm_hist_sel == -1);
|
||||||
if (_ctx_menu.active) _ctx_menu.render(display);
|
if (_ctx_menu.active) _ctx_menu.render(display);
|
||||||
@@ -1153,8 +1167,12 @@ public:
|
|||||||
});
|
});
|
||||||
int reserve = hs.reserve;
|
int reserve = hs.reserve;
|
||||||
{
|
{
|
||||||
|
// Stack boxes upward from just above the compose row — see the DM
|
||||||
|
// history block above for why (newest at the bottom, like a typical
|
||||||
|
// messenger). box_ys[i] still corresponds to item (_hist_scroll + i).
|
||||||
const int fixed_bh = 2 * lh + 1;
|
const int fixed_bh = 2 * lh + 1;
|
||||||
int cur_y = hist_start_y;
|
const int box_gap = portrait_expand ? 2 : 1;
|
||||||
|
int cur_y = cby - box_gap; // reserve the same gap against compose as between boxes
|
||||||
for (int ii = 0; ii < MAX_VIS_BOXES && (_hist_scroll + ii) < ch_hist_count; ii++) {
|
for (int ii = 0; ii < MAX_VIS_BOXES && (_hist_scroll + ii) < ch_hist_count; ii++) {
|
||||||
int bh = fixed_bh;
|
int bh = fixed_bh;
|
||||||
if (portrait_expand) {
|
if (portrait_expand) {
|
||||||
@@ -1168,9 +1186,10 @@ public:
|
|||||||
bh = (1 + (nl > 0 ? nl : 1)) * lh + 1;
|
bh = (1 + (nl > 0 ? nl : 1)) * lh + 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (cur_y + bh > cby) break;
|
int box_top = cur_y - bh;
|
||||||
box_ys[n_vis] = cur_y; box_hs[n_vis] = bh; n_vis++;
|
if (box_top < hist_start_y) break;
|
||||||
cur_y += bh + (portrait_expand ? 2 : 1);
|
box_ys[n_vis] = box_top; box_hs[n_vis] = bh; n_vis++;
|
||||||
|
cur_y = box_top - box_gap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_hist_visible = n_vis;
|
_hist_visible = n_vis;
|
||||||
@@ -1228,9 +1247,16 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scrollbar: track pinned to the full list area; thumb from hs metrics.
|
// Scrollbar: track pinned to the full list area; thumb from hs metrics.
|
||||||
if (hs.need)
|
// Inverted for the same reason as the DM history block above — item 0
|
||||||
|
// (newest) renders at the bottom, so the thumb should start at the
|
||||||
|
// bottom too and rise as you scroll into older history.
|
||||||
|
if (hs.need) {
|
||||||
|
long span = hs.total_px - hs.view_px;
|
||||||
|
long inv_scroll_px = span - hs.scroll_px;
|
||||||
|
if (inv_scroll_px < 0) inv_scroll_px = 0;
|
||||||
drawScrollIndicatorPx(display, hist_start_y + 1, hs.view_px,
|
drawScrollIndicatorPx(display, hist_start_y + 1, hs.view_px,
|
||||||
hs.total_px, hs.view_px, hs.scroll_px);
|
hs.total_px, hs.view_px, inv_scroll_px);
|
||||||
|
}
|
||||||
|
|
||||||
drawComposeButton(display, cby, lh, _hist_sel == -1);
|
drawComposeButton(display, cby, lh, _hist_sel == -1);
|
||||||
if (_ctx_menu.active) _ctx_menu.render(display);
|
if (_ctx_menu.active) _ctx_menu.render(display);
|
||||||
@@ -1700,16 +1726,12 @@ public:
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// Newest (index 0) now renders at the bottom, oldest at the top (see the
|
||||||
|
// render block above), so UP/DOWN swap which direction walks the index:
|
||||||
|
// UP now climbs toward older (higher index, physically upward); DOWN
|
||||||
|
// now walks toward newer (lower index, physically downward) and off the
|
||||||
|
// bottom (index 0) reaches the compose row, which sits right below it.
|
||||||
if (c == KEY_UP) {
|
if (c == KEY_UP) {
|
||||||
if (_dm_hist_sel > 0) {
|
|
||||||
_dm_hist_sel--;
|
|
||||||
if (_dm_hist_sel < _dm_hist_scroll) _dm_hist_scroll = _dm_hist_sel;
|
|
||||||
} else if (_dm_hist_sel == 0) {
|
|
||||||
_dm_hist_sel = -1;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (c == KEY_DOWN) {
|
|
||||||
if (_dm_hist_sel == -1 && dm_count > 0) { _dm_hist_sel = 0; _dm_hist_scroll = 0; }
|
if (_dm_hist_sel == -1 && dm_count > 0) { _dm_hist_sel = 0; _dm_hist_scroll = 0; }
|
||||||
else if (_dm_hist_sel >= 0 && _dm_hist_sel < dm_count - 1) {
|
else if (_dm_hist_sel >= 0 && _dm_hist_sel < dm_count - 1) {
|
||||||
_dm_hist_sel++;
|
_dm_hist_sel++;
|
||||||
@@ -1718,6 +1740,15 @@ public:
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (c == KEY_DOWN) {
|
||||||
|
if (_dm_hist_sel > 0) {
|
||||||
|
_dm_hist_sel--;
|
||||||
|
if (_dm_hist_sel < _dm_hist_scroll) _dm_hist_scroll = _dm_hist_sel;
|
||||||
|
} else if (_dm_hist_sel == 0) {
|
||||||
|
_dm_hist_sel = -1;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (c == KEY_ENTER) {
|
if (c == KEY_ENTER) {
|
||||||
if (_dm_hist_sel >= 0) {
|
if (_dm_hist_sel >= 0) {
|
||||||
_dm_fs.begin();
|
_dm_fs.begin();
|
||||||
@@ -1774,13 +1805,10 @@ public:
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (c == KEY_CANCEL) { _phase = CHANNEL_PICK; return true; }
|
if (c == KEY_CANCEL) { _phase = CHANNEL_PICK; return true; }
|
||||||
|
// Newest (index 0) now renders at the bottom, oldest at the top (see the
|
||||||
|
// render block above) — UP/DOWN swap direction accordingly, same as the
|
||||||
|
// DM history handler above.
|
||||||
if (c == KEY_UP) {
|
if (c == KEY_UP) {
|
||||||
if (_hist_sel > 0) { _hist_sel--; if (_hist_sel < _hist_scroll) _hist_scroll = _hist_sel; }
|
|
||||||
else if (_hist_sel == 0) _hist_sel = -1;
|
|
||||||
updateChannelUnread();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (c == KEY_DOWN) {
|
|
||||||
if (_hist_sel == -1 && ch_hist_count > 0) { _hist_sel = 0; _hist_scroll = 0; }
|
if (_hist_sel == -1 && ch_hist_count > 0) { _hist_sel = 0; _hist_scroll = 0; }
|
||||||
else if (_hist_sel >= 0 && _hist_sel < ch_hist_count - 1) {
|
else if (_hist_sel >= 0 && _hist_sel < ch_hist_count - 1) {
|
||||||
_hist_sel++;
|
_hist_sel++;
|
||||||
@@ -1789,6 +1817,12 @@ public:
|
|||||||
updateChannelUnread();
|
updateChannelUnread();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (c == KEY_DOWN) {
|
||||||
|
if (_hist_sel > 0) { _hist_sel--; if (_hist_sel < _hist_scroll) _hist_scroll = _hist_sel; }
|
||||||
|
else if (_hist_sel == 0) _hist_sel = -1;
|
||||||
|
updateChannelUnread();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (c == KEY_ENTER) {
|
if (c == KEY_ENTER) {
|
||||||
if (_hist_sel >= 0) {
|
if (_hist_sel >= 0) {
|
||||||
_fs.begin();
|
_fs.begin();
|
||||||
|
|||||||
Reference in New Issue
Block a user