mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-02 19:02:00 +00:00
feat(eink): whole message bubbles on tall screens
A screen with room for nine lines or more shows each message bubble fully wrapped instead of one line. The bubble's height is measured at the width it is drawn at. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -173,7 +173,7 @@ class MessagesScreen : public UIScreen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scrollbar metrics for boxes of varying height (h(idx, reserve) px each,
|
// Scrollbar metrics for boxes of varying height (h(idx, reserve) px each,
|
||||||
// `gap` px apart): the same fit test as the portrait path below.
|
// `gap` px apart): the same fit test as the expanded path below.
|
||||||
template <class GetH>
|
template <class GetH>
|
||||||
HistScroll computeHistScrollH(DisplayDriver& display, int count, int scroll, int hist_start_y, int cby,
|
HistScroll computeHistScrollH(DisplayDriver& display, int count, int scroll, int hist_start_y, int cby,
|
||||||
int gap, GetH h) {
|
int gap, GetH h) {
|
||||||
@@ -200,7 +200,7 @@ class MessagesScreen : public UIScreen {
|
|||||||
// this function doesn't reprocess it, so a mismatch here just means this sizing
|
// this function doesn't reprocess it, so a mismatch here just means this sizing
|
||||||
// pass and the real render disagree on line count.
|
// pass and the real render disagree on line count.
|
||||||
template <class GetBody>
|
template <class GetBody>
|
||||||
HistScroll computeHistScroll(DisplayDriver& display, bool portrait, int count, int scroll,
|
HistScroll computeHistScroll(DisplayDriver& display, bool expand, int count, int scroll,
|
||||||
int hist_start_y, int cby, int lh, GetBody getBody) {
|
int hist_start_y, int cby, int lh, GetBody getBody) {
|
||||||
HistScroll r{};
|
HistScroll r{};
|
||||||
const int fixed_bh = 2 * lh + 1;
|
const int fixed_bh = 2 * lh + 1;
|
||||||
@@ -209,7 +209,7 @@ class MessagesScreen : public UIScreen {
|
|||||||
if (r.view_px < 1) r.view_px = 1;
|
if (r.view_px < 1) r.view_px = 1;
|
||||||
const int col = scrollIndicatorColWidth(display);
|
const int col = scrollIndicatorColWidth(display);
|
||||||
|
|
||||||
if (!portrait) { // uniform boxes → exact pixel math
|
if (!expand) { // uniform boxes → exact pixel math
|
||||||
const int box = fixed_bh + 1;
|
const int box = fixed_bh + 1;
|
||||||
r.total_px = (long)count * box;
|
r.total_px = (long)count * box;
|
||||||
r.scroll_px = (long)scroll * box;
|
r.scroll_px = (long)scroll * box;
|
||||||
@@ -219,13 +219,10 @@ class MessagesScreen : public UIScreen {
|
|||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
const int sp = 2; // portrait inter-box spacing
|
const int sp = 2; // expanded inter-box spacing
|
||||||
auto boxH = [&](int idx, int rsv) -> int {
|
auto boxH = [&](int idx, int rsv) -> int {
|
||||||
const char* body = getBody(idx);
|
const char* body = getBody(idx);
|
||||||
if (!body) return fixed_bh;
|
return body ? expandedBoxH(display, body, display.width() - rsv) : fixed_bh;
|
||||||
display.translateUTF8ToBlocks(s_wrap_trans, body, sizeof(s_wrap_trans));
|
|
||||||
int nl = FullscreenMsgView::wrapLines(display, s_wrap_trans, display.width() - 6 - rsv, s_wrap_lines, 8);
|
|
||||||
return (1 + (nl > 0 ? nl : 1)) * lh + 1;
|
|
||||||
};
|
};
|
||||||
// Scrollbar-needed test at the WIDEST layout (reserve 0 → fewest wrap lines →
|
// Scrollbar-needed test at the WIDEST layout (reserve 0 → fewest wrap lines →
|
||||||
// shortest total). If even this overflows the list area the gutter is truly
|
// shortest total). If even this overflows the list area the gutter is truly
|
||||||
@@ -547,6 +544,17 @@ class MessagesScreen : public UIScreen {
|
|||||||
int w = full_avail - gutter;
|
int w = full_avail - gutter;
|
||||||
return w < 1 ? 1 : w;
|
return w < 1 ? 1 : w;
|
||||||
}
|
}
|
||||||
|
// Whether the history shows each message whole (wrapped, a box as tall as
|
||||||
|
// it needs) rather than as compact fixed boxes: on a screen of nine lines
|
||||||
|
// or more (the e-ink panels either way up), not on a 7-line OLED.
|
||||||
|
static bool expandHistory(DisplayDriver& d) { return d.height() / d.getLineHeight() >= 9; }
|
||||||
|
// An expanded box's height: its header line, then `body` wrapped at the
|
||||||
|
// width it's drawn in -- the bubble's own, inside `full_avail`.
|
||||||
|
static int expandedBoxH(DisplayDriver& d, const char* body, int full_avail) {
|
||||||
|
d.translateUTF8ToBlocks(s_wrap_trans, body, sizeof(s_wrap_trans));
|
||||||
|
int nl = FullscreenMsgView::wrapLines(d, s_wrap_trans, bubbleMaxW(d, full_avail) - 6, s_wrap_lines, 8);
|
||||||
|
return (1 + (nl > 0 ? nl : 1)) * d.getLineHeight() + 1;
|
||||||
|
}
|
||||||
static BubbleBox computeBubbleBox(int full_avail, int max_w, bool outgoing, int header_w, int body_w) {
|
static BubbleBox computeBubbleBox(int full_avail, int max_w, bool outgoing, int header_w, int body_w) {
|
||||||
int w = header_w > body_w ? header_w : body_w;
|
int w = header_w > body_w ? header_w : body_w;
|
||||||
if (w > max_w) w = max_w;
|
if (w > max_w) w = max_w;
|
||||||
@@ -1358,14 +1366,14 @@ public:
|
|||||||
uint32_t now_ts = rtc_clock.getCurrentTime();
|
uint32_t now_ts = rtc_clock.getCurrentTime();
|
||||||
bool is_room = (_sel_contact.type == ADV_TYPE_ROOM);
|
bool is_room = (_sel_contact.type == ADV_TYPE_ROOM);
|
||||||
|
|
||||||
// Portrait e-ink (height > width): variable-height boxes that show the full
|
// On a tall enough screen (expandHistory): boxes as tall as their whole
|
||||||
// wrapped message text. All other displays/orientations: compact 2-line boxes.
|
// wrapped text. Otherwise compact 2-line boxes.
|
||||||
bool portrait_expand = (display.height() > display.width());
|
const bool expand = expandHistory(display);
|
||||||
// A DM bubble on a compact screen carries no sender line: one line of
|
// A DM bubble on a compact screen carries no sender line: one line of
|
||||||
// text with the delivery / hop marker and the age at its end, growing to
|
// text with the delivery / hop marker and the age at its end, growing to
|
||||||
// two lines (marker and age ending the second) when the text is longer.
|
// two lines (marker and age ending the second) when the text is longer.
|
||||||
// Rooms keep the author line above the text.
|
// Rooms keep the author line above the text.
|
||||||
const bool compact_dm = !is_room && !portrait_expand;
|
const bool compact_dm = !is_room && !expand;
|
||||||
const int MAX_VIS_BOXES = 8;
|
const int MAX_VIS_BOXES = 8;
|
||||||
int box_ys[MAX_VIS_BOXES], box_hs[MAX_VIS_BOXES], n_vis = 0;
|
int box_ys[MAX_VIS_BOXES], box_hs[MAX_VIS_BOXES], n_vis = 0;
|
||||||
|
|
||||||
@@ -1402,7 +1410,7 @@ public:
|
|||||||
// don't reflow their width as messages arrive.
|
// don't reflow their width as messages arrive.
|
||||||
HistScroll hs = compact_dm
|
HistScroll hs = compact_dm
|
||||||
? computeHistScrollH(display, dm_count, _dm_hist_scroll, hist_start_y, cby, 1, compactH)
|
? computeHistScrollH(display, dm_count, _dm_hist_scroll, hist_start_y, cby, 1, compactH)
|
||||||
: computeHistScroll(display, portrait_expand, dm_count, _dm_hist_scroll,
|
: computeHistScroll(display, expand, dm_count, _dm_hist_scroll,
|
||||||
hist_start_y, cby, lh,
|
hist_start_y, cby, lh,
|
||||||
[&](int idx) -> const char* {
|
[&](int idx) -> const char* {
|
||||||
// Must match the per-item body extraction below (dmDisplayParts +
|
// Must match the per-item body extraction below (dmDisplayParts +
|
||||||
@@ -1419,18 +1427,16 @@ public:
|
|||||||
// messenger, instead of newest-at-top. box_ys[i] still corresponds
|
// messenger, instead of newest-at-top. box_ys[i] still corresponds
|
||||||
// to item (_dm_hist_scroll + i), same as before; only its y flips.
|
// 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;
|
||||||
const int box_gap = portrait_expand ? 2 : 1;
|
const int box_gap = expand ? 2 : 1;
|
||||||
int cur_y = cby - box_gap; // reserve the same gap against compose as between boxes
|
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 = compact_dm ? compactH(_dm_hist_scroll + ii, reserve) : fixed_bh;
|
int bh = compact_dm ? compactH(_dm_hist_scroll + ii, reserve) : fixed_bh;
|
||||||
if (portrait_expand) {
|
if (expand) {
|
||||||
int rp = _history.dmHistEntryForContact(_sel_contact.id.pub_key, _dm_hist_scroll + ii);
|
int rp = _history.dmHistEntryForContact(_sel_contact.id.pub_key, _dm_hist_scroll + ii);
|
||||||
if (rp >= 0) {
|
if (rp >= 0) {
|
||||||
char hsb[33];
|
char hsb[33];
|
||||||
const char* hbody = skipReplyPrefix(dmDisplayParts(_history.dmAtPos(rp), is_room, filtered_name, hsb, sizeof(hsb)));
|
const char* hbody = skipReplyPrefix(dmDisplayParts(_history.dmAtPos(rp), is_room, filtered_name, hsb, sizeof(hsb)));
|
||||||
display.translateUTF8ToBlocks(s_wrap_trans, hbody, sizeof(s_wrap_trans));
|
bh = expandedBoxH(display, hbody, display.width() - reserve);
|
||||||
int nl = FullscreenMsgView::wrapLines(display, s_wrap_trans, display.width() - 6 - reserve, s_wrap_lines, 8);
|
|
||||||
bh = (1 + (nl > 0 ? nl : 1)) * lh + 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
int box_top = cur_y - bh;
|
int box_top = cur_y - bh;
|
||||||
@@ -1523,7 +1529,7 @@ public:
|
|||||||
int mk_gap = (ack_w > 0 && age_w > 0) ? 2 : 0;
|
int mk_gap = (ack_w > 0 && age_w > 0) ? 2 : 0;
|
||||||
int header_w = 3 + (sender[0] ? display.getTextWidth(sender) : -3) + ack_w + mk_gap + age_w + 3;
|
int header_w = 3 + (sender[0] ? display.getTextWidth(sender) : -3) + ack_w + mk_gap + age_w + 3;
|
||||||
int body_w, nl = 0;
|
int body_w, nl = 0;
|
||||||
if (portrait_expand) {
|
if (expand) {
|
||||||
display.translateUTF8ToBlocks(s_wrap_trans, body, sizeof(s_wrap_trans));
|
display.translateUTF8ToBlocks(s_wrap_trans, body, sizeof(s_wrap_trans));
|
||||||
nl = FullscreenMsgView::wrapLines(display, s_wrap_trans, max_w - 6, s_wrap_lines, 8);
|
nl = FullscreenMsgView::wrapLines(display, s_wrap_trans, max_w - 6, s_wrap_lines, 8);
|
||||||
body_w = 0;
|
body_w = 0;
|
||||||
@@ -1558,7 +1564,7 @@ public:
|
|||||||
if (age[0]) { display.setCursor(box.x + box.w - age_w, y + 1); display.print(age); }
|
if (age[0]) { display.setCursor(box.x + box.w - age_w, y + 1); display.print(age); }
|
||||||
// Ink is already LIGHT (unselected) or DARK (selected) from
|
// Ink is already LIGHT (unselected) or DARK (selected) from
|
||||||
// drawHistRowFrame above, and nothing since has changed it.
|
// drawHistRowFrame above, and nothing since has changed it.
|
||||||
if (portrait_expand) {
|
if (expand) {
|
||||||
for (int li = 0; li < nl; li++) { display.setCursor(box.x + 3, y + (li + 1) * lh + 1); display.print(s_wrap_lines[li]); }
|
for (int li = 0; li < nl; li++) { display.setCursor(box.x + 3, y + (li + 1) * lh + 1); display.print(s_wrap_lines[li]); }
|
||||||
} else {
|
} else {
|
||||||
// Suppress this row's own marquee while _ctx_menu (Path/Relayed by,
|
// Suppress this row's own marquee while _ctx_menu (Path/Relayed by,
|
||||||
@@ -1657,13 +1663,13 @@ public:
|
|||||||
int ch_hist_count = _history.histCountForChannel(_sel_channel_idx);
|
int ch_hist_count = _history.histCountForChannel(_sel_channel_idx);
|
||||||
uint32_t now_ts = rtc_clock.getCurrentTime();
|
uint32_t now_ts = rtc_clock.getCurrentTime();
|
||||||
|
|
||||||
// Portrait e-ink (height > width): variable-height boxes that show the full
|
// On a tall enough screen (expandHistory): boxes as tall as their whole
|
||||||
// wrapped message text. All other displays/orientations: compact 2-line boxes.
|
// wrapped text. Otherwise compact 2-line boxes.
|
||||||
bool portrait_expand = (display.height() > display.width());
|
const bool expand = expandHistory(display);
|
||||||
const int MAX_VIS_BOXES = 8;
|
const int MAX_VIS_BOXES = 8;
|
||||||
int box_ys[MAX_VIS_BOXES], box_hs[MAX_VIS_BOXES], n_vis = 0;
|
int box_ys[MAX_VIS_BOXES], box_hs[MAX_VIS_BOXES], n_vis = 0;
|
||||||
// Fixed-track scrollbar metrics + stable gutter reserve (see DM history above).
|
// Fixed-track scrollbar metrics + stable gutter reserve (see DM history above).
|
||||||
HistScroll hs = computeHistScroll(display, portrait_expand, ch_hist_count, _hist_scroll,
|
HistScroll hs = computeHistScroll(display, expand, ch_hist_count, _hist_scroll,
|
||||||
hist_start_y, cby, lh,
|
hist_start_y, cby, lh,
|
||||||
[&](int idx) -> const char* {
|
[&](int idx) -> const char* {
|
||||||
int rp = _history.histEntryForChannel(_sel_channel_idx, idx);
|
int rp = _history.histEntryForChannel(_sel_channel_idx, idx);
|
||||||
@@ -1678,19 +1684,17 @@ public:
|
|||||||
// history block above for why (newest at the bottom, like a typical
|
// history block above for why (newest at the bottom, like a typical
|
||||||
// messenger). box_ys[i] still corresponds to item (_hist_scroll + i).
|
// 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;
|
||||||
const int box_gap = portrait_expand ? 2 : 1;
|
const int box_gap = expand ? 2 : 1;
|
||||||
int cur_y = cby - box_gap; // reserve the same gap against compose as between boxes
|
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 (expand) {
|
||||||
int rp = _history.histEntryForChannel(_sel_channel_idx, _hist_scroll + ii);
|
int rp = _history.histEntryForChannel(_sel_channel_idx, _hist_scroll + ii);
|
||||||
if (rp >= 0) {
|
if (rp >= 0) {
|
||||||
const char* rtext = _history.chAtPos(rp).text;
|
const char* rtext = _history.chAtPos(rp).text;
|
||||||
const char* rsep = strstr(rtext, ": ");
|
const char* rsep = strstr(rtext, ": ");
|
||||||
const char* rbody = rsep ? rsep + 2 : rtext;
|
const char* rbody = rsep ? rsep + 2 : rtext;
|
||||||
display.translateUTF8ToBlocks(s_wrap_trans, skipReplyPrefix(rbody), sizeof(s_wrap_trans));
|
bh = expandedBoxH(display, skipReplyPrefix(rbody), display.width() - reserve);
|
||||||
int nl = FullscreenMsgView::wrapLines(display, s_wrap_trans, display.width() - 6 - reserve, s_wrap_lines, 8);
|
|
||||||
bh = (1 + (nl > 0 ? nl : 1)) * lh + 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
int box_top = cur_y - bh;
|
int box_top = cur_y - bh;
|
||||||
@@ -1752,7 +1756,7 @@ public:
|
|||||||
int mk_gap = (ack_w > 0 && age_w > 0) ? 2 : 0;
|
int mk_gap = (ack_w > 0 && age_w > 0) ? 2 : 0;
|
||||||
int header_w = 3 + (sender[0] ? display.getTextWidth(sender) : -3) + ack_w + mk_gap + age_w + 3;
|
int header_w = 3 + (sender[0] ? display.getTextWidth(sender) : -3) + ack_w + mk_gap + age_w + 3;
|
||||||
int body_w, nl = 0;
|
int body_w, nl = 0;
|
||||||
if (portrait_expand) {
|
if (expand) {
|
||||||
display.translateUTF8ToBlocks(s_wrap_trans, body, sizeof(s_wrap_trans));
|
display.translateUTF8ToBlocks(s_wrap_trans, body, sizeof(s_wrap_trans));
|
||||||
nl = FullscreenMsgView::wrapLines(display, s_wrap_trans, max_w - 6, s_wrap_lines, 8);
|
nl = FullscreenMsgView::wrapLines(display, s_wrap_trans, max_w - 6, s_wrap_lines, 8);
|
||||||
body_w = 0;
|
body_w = 0;
|
||||||
@@ -1782,7 +1786,7 @@ public:
|
|||||||
if (age[0]) { display.setCursor(box.x + box.w - age_w, y + 1); display.print(age); }
|
if (age[0]) { display.setCursor(box.x + box.w - age_w, y + 1); display.print(age); }
|
||||||
// Ink is already LIGHT (unselected) or DARK (selected) from
|
// Ink is already LIGHT (unselected) or DARK (selected) from
|
||||||
// drawHistRowFrame above, and nothing since has changed it.
|
// drawHistRowFrame above, and nothing since has changed it.
|
||||||
if (portrait_expand) {
|
if (expand) {
|
||||||
for (int li = 0; li < nl; li++) { display.setCursor(box.x + 3, y + (li + 1) * lh + 1); display.print(s_wrap_lines[li]); }
|
for (int li = 0; li < nl; li++) { display.setCursor(box.x + 3, y + (li + 1) * lh + 1); display.print(s_wrap_lines[li]); }
|
||||||
} else {
|
} else {
|
||||||
// Suppress this row's own marquee while _ctx_menu (Path/Relayed by,
|
// Suppress this row's own marquee while _ctx_menu (Path/Relayed by,
|
||||||
|
|||||||
Reference in New Issue
Block a user