Files
MeshCore/src/helpers/ui/CompanionHomeLayout.h
T
mikecarper 16399a019d Fix Companion pairing tests and nRF52 OTA image size
The extracted inbox renderer was missing HomeScreen's pairing PIN visibility
state. Provide it and check that it agrees with the complete PIN on screen.

Reuse the text drawing result instead of measuring pairing values twice, and
keep the bottom pairing layout separate from the large home renderer. This
saves 32 bytes in the RAK4631 Full Companion and lets its OTA image pass the
existing application limit without removing features or weakening checks.

Fixes the two failures in unit-test run 40174991783.
2026-10-25 09:00:12 -07:00

239 lines
9.0 KiB
C++

#pragma once
#include <helpers/ui/DisplayDriver.h>
#include <helpers/ui/IndicatorRenderProfile.h>
namespace mesh {
namespace ui {
struct DisplayRegion {
int x;
int y;
int width;
int height;
int right() const { return x + width; }
int bottom() const { return y + height; }
};
struct CompanionHomeLayout {
DisplayRegion info;
DisplayRegion pairing;
int instruction_y;
int network_y;
int pairing_label_y;
int pairing_value_y;
};
struct CompactCompanionPairingLayout {
DisplayRegion pairing;
int pairing_label_y;
int pairing_value_y;
};
// Full-size Indicator builds keep the established 160x160 logical coordinate
// system even when they obtain a native 480x480 render buffer. Expose that
// distinction to the home page so it can spend the extra pixel density on
// larger, deliberately reflowed text without changing the 320 BLE profile or
// unrelated displays that happen to have a tall viewport.
inline bool usesExpandedCompanionHomeTypography(
int width, int height, int render_width, int render_height) {
return usesNativeIndicatorTypography(
width, height, render_width, render_height);
}
// The common Companion OLED/TFT viewport is 128x64. Its normal instruction
// and network rows occupy the same lower area needed by the Bluetooth status,
// so an active PIN/connection replaces those rows with this compact block.
// The INBOX title at y=22 with a size-2 fallback font ends at y=38; the block
// begins exactly there and keeps a size-1 label plus size-2 value on-screen.
inline bool usesCompactCompanionPairingLayout(int width, int height) {
return width == 128 && height == 64;
}
inline CompactCompanionPairingLayout makeCompactCompanionPairingLayout(
int width, int height) {
const int pairing_height = height >= 26 ? 26 : height;
const int pairing_y = height - pairing_height;
const int pairing_margin = width >= 16 ? 4 : 0;
return {
{pairing_margin, pairing_y, width - pairing_margin * 2, pairing_height},
pairing_y,
pairing_y + 10,
};
}
// The large Companion home page keeps ordinary instructions/network status in
// the upper half and reserves a visually separate pairing block around four
// fifths of the display height. The Indicator renders a 160x160 logical UI,
// for which this produces a 48-pixel-tall block centered at y=128.
inline CompanionHomeLayout makeLargeCompanionHomeLayout(int width,
int height,
bool expanded = false) {
if (expanded && width == 160 && height == 160) {
// Native 480 uses three large vertical bands: an INBOX title above this
// count region, then one lower region that is either the BLE state or a
// large tap/network hint. Reusing the lower region conditionally gives
// the prominent text room to grow without overlapping another status.
return {
{2, 61, 156, 39},
{8, 102, 144, 58},
61,
61,
102,
131,
};
}
// Ordinary one-line status needs almost the full logical width when the
// Indicator's recovered mono font is active. The PIN block gets the larger
// inset so it remains visibly distinct from the page background.
const int info_margin = width >= 80 ? 2 : 0;
const int pairing_margin = width >= 80 ? 8 : 2;
// Native 480 rendering enlarges non-compact Indicator text by 20 percent.
// Four extra logical pixels keep its size-3 PIN inside this block while the
// 320 fallback retains the same geometry and gains a little more breathing
// room.
const int pairing_height = height >= 128 ? 48 : 28;
const int pairing_center = (height * 4) / 5;
int pairing_y = pairing_center - pairing_height / 2;
if (pairing_y < 0) pairing_y = 0;
if (pairing_y + pairing_height > height) {
pairing_y = height - pairing_height;
}
const int info_y = height >= 128 ? 40 : 18;
int info_bottom = pairing_y - 12;
if (info_bottom < info_y) info_bottom = info_y;
CompanionHomeLayout layout = {
{info_margin, info_y, width - info_margin * 2,
info_bottom - info_y},
{pairing_margin, pairing_y, width - pairing_margin * 2,
pairing_height},
info_y + 2,
info_y + 18,
pairing_y + 4,
pairing_y + 17,
};
return layout;
}
inline bool displayRegionsOverlap(const DisplayRegion& left,
const DisplayRegion& right) {
return left.x < right.right() && right.x < left.right()
&& left.y < right.bottom() && right.y < left.bottom();
}
inline bool displayRegionContainsLine(const DisplayRegion& region, int y,
int line_height) {
return y >= region.y && line_height >= 0
&& y + line_height <= region.bottom();
}
// Explicitly repaint a reserved region. Full frames already clear the
// canvas, but keeping this local clear makes the layout safe if a display
// driver later gains partial home-page redraws or a PIN/status disappears.
inline void clearDisplayRegion(DisplayDriver& display,
const DisplayRegion& region) {
if (region.width > 0 && region.height > 0) {
display.fillRect(region.x, region.y, region.width, region.height);
}
}
// Center short text inside the supplied region and ellipsize long text from
// the region's left edge. In both cases the rendered row is horizontally
// bounded by the region and cannot run into an adjacent UI block.
// Return true when the complete text was drawn, without measuring it again.
inline bool drawTextCenteredEllipsized(DisplayDriver& display,
const DisplayRegion& region, int y,
const char* text) {
if (text == nullptr || text[0] == 0 || region.width <= 0) return false;
const int text_width = display.getTextWidth(text);
if (text_width <= region.width) {
display.setCursor(region.x + (region.width - text_width) / 2, y);
display.print(text);
return true;
} else {
display.drawTextEllipsized(region.x, y, region.width, text);
return false;
}
}
// Stack real font-height rows, never fixed ten-pixel baselines. E-paper's
// smallest font is already taller than an entire OLED row.
class BoundedTextRows {
DisplayDriver& display;
DisplayRegion region;
int next_y;
int gap;
public:
BoundedTextRows(DisplayDriver& d, const DisplayRegion& r, int spacing = 2)
: display(d), region(r), next_y(r.y), gap(spacing) {}
int nextY() const { return next_y; }
bool reserve() {
const int height = display.textLineHeight();
if (height <= 0 || !displayRegionContainsLine(region, next_y, height))
return false;
next_y += height + gap;
return true;
}
bool draw(const char* text, bool centered = true) {
const int height = display.textLineHeight();
if (height <= 0 || !displayRegionContainsLine(region, next_y, height))
return false;
if (centered) {
drawTextCenteredEllipsized(display, region, next_y, text);
} else {
display.drawTextEllipsized(region.x, next_y, region.width, text);
}
next_y += height + gap;
return true;
}
};
// PIN/connection status owns the bottom of every non-Indicator home screen,
// including native 160x80 T096 and 250x122 MeshPocket panels. The caller must
// not draw ordinary instructions/network rows in this same area.
// Return true only when the complete value fits and was drawn.
// Keep this layout separate from the large home renderer to reduce flash use.
__attribute__((noinline)) inline bool drawBottomPairingBlock(
DisplayDriver& display, int content_top, const char* label, const char* value) {
const int bottom = display.height() - (display.height() > 64 ? 2 : 0);
const int available = bottom - content_top;
const int margin = display.width() >= 16 ? 4 : 0;
const int width = display.width() - 2 * margin;
if (available <= 0 || width <= 0) return false;
display.setTextSize(1);
const int label_height = display.textLineHeight();
int value_size = 2;
display.setTextSize(value_size);
if (display.getTextWidth(value) > width
|| display.textLineHeight() > available) {
display.setTextSize(value_size = 1);
}
const int value_height = display.textLineHeight();
if (value_height <= 0 || value_height > available) return false;
// Prefer a readable complete PIN; omit its label on unusually short
// viewports instead of colliding with the title or clipping the digits.
const bool show_label = label_height > 0
&& label_height + 2 + value_height <= available;
const int block_height = value_height + (show_label ? label_height + 2 : 0);
const DisplayRegion region = {margin, bottom - block_height, width, block_height};
display.setColor(UIColor::title_bkg);
clearDisplayRegion(display, region);
display.setColor(UIColor::title_txt);
if (show_label) {
display.setTextSize(1);
drawTextCenteredEllipsized(display, region, region.y, label);
}
display.setTextSize(value_size);
return drawTextCenteredEllipsized(display, region, bottom - value_height, value);
}
} // namespace ui
} // namespace mesh