234 lines
8.7 KiB
C++
234 lines
8.7 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.
|
|
inline void drawTextCenteredEllipsized(DisplayDriver& display,
|
|
const DisplayRegion& region, int y,
|
|
const char* text) {
|
|
if (text == nullptr || text[0] == 0 || region.width <= 0) return;
|
|
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);
|
|
} else {
|
|
display.drawTextEllipsized(region.x, y, region.width, text);
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
inline void 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;
|
|
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;
|
|
// 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);
|
|
drawTextCenteredEllipsized(display, region, bottom - value_height, value);
|
|
}
|
|
|
|
} // namespace ui
|
|
} // namespace mesh
|