#pragma once // Building blocks for screens that show data (Status, the tool screens): one // look for a value row, a section break, a meter, a chip, a big number with // its unit, a switch and a history line. The 128x64 counterpart of the L2's // info cards. Sizes follow the font (line height, miniIconScale), never a // fixed 128x64, so a larger-font e-ink layout gets them scaled for free. #include #include #include #include #include "icons.h" #include "../ui-core/StatusHistory.h" #include "../ui-core/Lettering.h" namespace info { // Whether a value fits on its label's line (else rowH() gives it a line below). // `reserve` keeps the value clear of a scroll column. inline bool rowFits(DisplayDriver& d, const char* label, const char* value, int reserve = 0, int x0 = 1) { if (!value || !*value) return true; const int room = d.width() - reserve - 2 - (x0 + d.getTextWidth(label) + d.getCharWidth()); return (int)d.getTextWidth(value) <= room; } inline int rowH(DisplayDriver& d, const char* label, const char* value, int reserve = 0) { return rowFits(d, label, value, reserve) ? d.lineStep() : d.lineStep() + d.getLineHeight(); } // A row of rowH(): the value beside the label, or right-aligned on the line // under it when it's too long for that (ending in an ellipsis only past the // full width). inline void rowWrapped(DisplayDriver& d, int y, const char* label, const char* value, int reserve = 0, int x0 = 1) { if (rowFits(d, label, value, reserve, x0)) { d.setCursor(x0, y); d.print(label); if (value && *value) d.drawTextRightAlign(d.width() - reserve - 2, y, value); return; } d.setCursor(x0, y); d.print(label); const int right = d.width() - reserve - 2, room = right - x0; // the whole line is its own const int vy = y + d.getLineHeight(); if ((int)d.getTextWidth(value) <= room) d.drawTextRightAlign(right, vy, value); else d.drawTextEllipsized(right - room, vy, room, value); } // A dotted rule across [x, x + w), the separator between categories. inline void rule(DisplayDriver& d, int x, int y, int w) { const int s = miniIconScale(d); for (int i = 0; i < w; i += 2 * s) d.fillRect(x + i, y, s, s); } inline void vrule(DisplayDriver& d, int x, int y, int h) { const int s = miniIconScale(d); for (int i = 0; i < h; i += 2 * s) d.fillRect(x, y + i, s, s); } // A section title: the label centred between two dotted rules. One line tall. inline void section(DisplayDriver& d, int y, const char* label, int reserve = 0) { const int right = d.width() - reserve - 1, lw = d.getTextWidth(label); const int lx = (right - lw) / 2, ry = y + d.getLineHeight() / 2, gap = 2 * d.getCharWidth() / 3; rule(d, 1, ry, lx - gap - 1); d.setCursor(lx, y); d.print(label); rule(d, lx + lw + gap, ry, right - (lx + lw + gap)); } // A battery glyph the size of a mini icon (7 x 5 at 1x, 9 x 7 in the large // set, 12 x 9 in the 9 px one), its corners softly rounded, filled to frac. static constexpr int BATT_BODY_W = MINI_ICONS_LARGE == 2 ? 11 : MINI_ICONS_LARGE ? 8 : 6, BATT_BODY_H = MINI_ICONS_LARGE == 2 ? 9 : MINI_ICONS_LARGE ? 7 : 5; inline int batteryW(DisplayDriver& d) { return (BATT_BODY_W + 1) * miniIconScale(d); } inline int batteryH(DisplayDriver& d) { return BATT_BODY_H * miniIconScale(d); } inline void battery(DisplayDriver& d, int x, int y, float frac) { const int s = miniIconScale(d), w = BATT_BODY_W * s, h = BATT_BODY_H * s; if (frac < 0) frac = 0; if (frac > 1) frac = 1; d.drawSoftRect(x, y, w, h); // the outline without its corner pixels d.fillRect(x + w, y + s, s, h - 2 * s); const int in = MINI_ICONS_LARGE ? 2 * s : s; // the large one keeps a gap inside its outline const int fw = (int)((w - 2 * in) * frac + 0.5f); if (fw > 0) d.fillRect(x + in, y + in, fw, h - 2 * in); } // A soft-cornered meter, filled to frac (0..1). inline void meter(DisplayDriver& d, int x, int y, int w, int h, float frac) { if (frac < 0) frac = 0; if (frac > 1) frac = 1; d.drawSoftRect(x, y, w, h); const int in = d.sepH() + 1, fw = (int)((w - 2 * in) * frac + 0.5f); if (fw > 0) d.fillRect(x + in, y + in, fw, h - 2 * in); } // One point of a line or circle, sepH() square (2px on a doubled panel). inline void plot(DisplayDriver& d, int x, int y) { const int t = d.sepH(); d.fillRect(x - t / 2, y - t / 2, t, t); } // Midpoint circle; `step` > 1 dots it (every step-th point of each octant). inline void circle(DisplayDriver& d, int cx, int cy, int r, int step = 1) { int x = r, y = 0, err = 1 - r, i = 0; while (x >= y) { if (i++ % step == 0) { plot(d, cx + x, cy + y); plot(d, cx - x, cy + y); plot(d, cx + x, cy - y); plot(d, cx - x, cy - y); plot(d, cx + y, cy + x); plot(d, cx - y, cy + x); plot(d, cx + y, cy - x); plot(d, cx - y, cy - x); } y++; if (err < 0) err += 2 * y + 1; else { x--; err += 2 * (y - x) + 1; } } } // A line (Bresenham), sepH() thick. inline void line(DisplayDriver& d, int x0, int y0, int x1, int y1) { const int dx = x1 > x0 ? x1 - x0 : x0 - x1, dy = y1 > y0 ? y0 - y1 : y1 - y0; const int sx = x0 < x1 ? 1 : -1, sy = y0 < y1 ? 1 : -1; int err = dx + dy; for (;;) { plot(d, x0, y0); if (x0 == x1 && y0 == y1) break; const int e2 = 2 * err; if (e2 >= dy) { err += dy; x0 += sx; } if (e2 <= dx) { err += dx; y0 += sy; } } } // An arrow of length 2r centred on (cx, cy), pointing to `deg` (0 = up / // north, clockwise): a shaft and a two-stroke head at the tip. inline void arrow(DisplayDriver& d, int cx, int cy, int r, int deg) { const float a = deg * (float)M_PI / 180.0f, sx = sinf(a), sy = -cosf(a); const int tx = cx + (int)lroundf(r * sx), ty = cy + (int)lroundf(r * sy); line(d, cx - (int)lroundf(r * sx), cy - (int)lroundf(r * sy), tx, ty); const float h = r * 0.6f; for (int k = -1; k <= 1; k += 2) { // the head's strokes, 30 degrees off the shaft const float b = a + (float)M_PI + k * 0.52f; line(d, tx, ty, tx + (int)lroundf(h * sinf(b)), ty - (int)lroundf(h * cosf(b))); } } // A compass rose of radius r round (cx, cy), north up: the ring, a tick in at // each cardinal point, N in a gap at the top, and -- for deg >= 0 -- a needle // from the centre to the ring, towards deg. inline void rose(DisplayDriver& d, int cx, int cy, int r, int deg) { const int s = miniIconScale(d), t = 3 * s, lh = d.getLineHeight(), cw = d.getCharWidth(); circle(d, cx, cy, r); circle(d, cx, cy, r / 2, 3); d.fillRect(cx - s / 2, cy + r - t, s, t); // S d.fillRect(cx + r - t, cy - s / 2, t, s); // E d.fillRect(cx - r, cy - s / 2, t, s); // W d.setColor(DisplayDriver::DARK); // N, on a gap in the ring d.fillRect(cx - cw / 2 - 1, cy - r - lh / 2, cw + 2, lh); d.setColor(DisplayDriver::LIGHT); d.setCursor(cx - cw / 2, cy - r - lh / 2); d.print("N"); if (deg >= 0) { const float a = deg * (float)M_PI / 180.0f; const int tx = cx + (int)lroundf((r - t - s) * sinf(a)), ty = cy - (int)lroundf((r - t - s) * cosf(a)); for (int o = -(s / 2); o <= s / 2; o++) line(d, cx + o, cy, tx + o, ty); const float h = r * 0.3f; for (int k = -1; k <= 1; k += 2) { const float b = a + (float)M_PI + k * 0.45f; line(d, tx, ty, tx + (int)lroundf(h * sinf(b)), ty - (int)lroundf(h * cosf(b))); } } d.fillRect(cx - s, cy - s, 2 * s + 1, 2 * s + 1); // the centre: you } // A chip: a state as a filled pill (FIX, REC), a parameter as an outline // (SF8). One line plus 2 px tall; returns the x after it, gap included. inline int chipW(DisplayDriver& d, const char* t) { return d.getTextWidth(t) + 5; } inline int chip(DisplayDriver& d, int x, int y, const char* t, bool filled = false) { const int w = chipW(d, t), h = d.getLineHeight() + 2; if (filled) d.fillSoftRect(x, y, w, h); else d.drawSoftRect(x, y, w, h); if (filled) d.setColor(DisplayDriver::DARK); d.setCursor(x + 3, y + 1); d.print(t); d.setColor(DisplayDriver::LIGHT); return x + w + 3; } // A value at twice the text size with its unit beside it at the normal size, // sharing the baseline. Two lines tall; returns its width. inline int big(DisplayDriver& d, int x, int y, const char* value, const char* unit) { d.setTextSize(2); d.setCursor(x, y); d.print(value); int w = d.getTextWidth(value); d.setTextSize(1); if (unit && *unit) { d.setCursor(x + w + 3, y + d.getLineHeight()); d.print(unit); w += 3 + d.getTextWidth(unit); } return w; } inline int bigH(DisplayDriver& d) { return 2 * d.getLineHeight(); } // An on/off state, right edge at x_right on a text line at y: the words On / // Off, written as every other value. (A drawn pill switch read as a battery // on the small screens.) `inv` draws it in a selected row's colours (dark on // the light bar). inline int switchW(DisplayDriver& d) { return d.getTextWidth("Off"); } inline void toggle(DisplayDriver& d, int x_right, int y, bool on, bool inv = false) { d.setColor(inv ? DisplayDriver::DARK : DisplayDriver::LIGHT); d.drawTextRightAlign(x_right, y, on ? "On" : "Off"); } // ── List rows (call after drawRowSelection(); text is already in the row's ink) // A setting or field: label at x0, value flush right with its unit after a // space ("50 m"). A value too long for the room beside the label is cut, and // scrolls while the row is selected (sel). // Returns that marquee's next-step delay (0: nothing moving). inline int valueRow(DisplayDriver& d, int y, const char* label, const char* value, bool sel, int reserve, int x0 = 2) { d.setCursor(x0, y); d.print(label); if (!value || !*value) return 0; const int right = d.width() - reserve - 2; const int lx = x0 + (*label ? d.getTextWidth(label) + d.getCharWidth() : 0); if (lx + (int)d.getTextWidth(value) <= right) { d.drawTextRightAlign(right, y, value); return 0; } const int r = d.drawTextEllipsized(lx, y, right - lx, value, sel); return sel && r > 0 ? r : 0; } // A row from a screen's value formatter: exactly "ON" / "OFF" (the marker, not // what's shown) is a yes/no setting and gets switchRow(); anything else (an option that can be "Off" // among numbers included) is a value. Returns valueRow()'s marquee delay. inline void switchRow(DisplayDriver& d, int y, const char* label, bool on, bool sel, int reserve, int x0 = 2); inline int listRow(DisplayDriver& d, int y, const char* label, const char* value, bool sel, int reserve, int x0 = 2) { if (value && (!strcmp(value, "ON") || !strcmp(value, "OFF"))) { switchRow(d, y, label, value[1] == 'N', sel, reserve, x0); return 0; } return valueRow(d, y, label, value, sel, reserve, x0); } // An on/off row: the label, then ON / OFF at the right edge. inline void switchRow(DisplayDriver& d, int y, const char* label, bool on, bool sel, int reserve, int x0) { d.setCursor(x0, y); d.print(label); toggle(d, d.width() - reserve - 2, y, on, sel); } // The last N samples of one reading, oldest first, for a history line. template using History = uicore::History; // A history line in a w x h box: a dotted baseline, the samples scaled // between their own min and max (at least `min_span` apart, so a flat // reading stays flat), newest at the right edge. template inline void spark(DisplayDriver& d, int x, int y, int w, int h, const History& hs, int min_span = 4) { const int s = miniIconScale(d); for (int i = 0; i < w; i += 3 * s) d.fillRect(x + i, y + h - s, s, s); if (hs.n < 2) return; int lo = hs.at(0), hi = lo; for (int i = 1; i < hs.n; i++) { int v = hs.at(i); if (v < lo) lo = v; if (v > hi) hi = v; } if (hi - lo < min_span) { int mid = (hi + lo) / 2; lo = mid - min_span / 2; hi = lo + min_span; } int px = -1, py = 0; for (int i = 0; i < hs.n; i++) { const int cx = x + w - 1 - (hs.n - 1 - i) * (w - 1) / (N - 1); const int cy = y + h - 1 - (hs.at(i) - lo) * (h - 1) / (hi - lo); if (px >= 0) { // a step: across, then up or down d.fillRect(px, py, cx - px + 1, s); d.fillRect(cx, py < cy ? py : cy, s, (py < cy ? cy - py : py - cy) + s); } px = cx; py = cy; } } // Ordered (Bayer 4x4) dithering: whether pixel (x, y) is ink at a density of // level/16. For graded fills on large elements only -- it breaks up small // ones. inline bool bayer(int x, int y, int level) { static const uint8_t M[4][4] = { { 0, 8, 2, 10 }, { 12, 4, 14, 6 }, { 3, 11, 1, 9 }, { 15, 7, 13, 5 } }; return M[y & 3][x & 3] < level; } // A column from y0 down to (not including) y1, dithered from dense at the top // to sparse at the bottom: the glow under a chart's line. inline void fadeColumn(DisplayDriver& d, int x, int y0, int y1) { const int h = y1 - y0; for (int y = y0; y < y1; y++) { const int level = 9 - 8 * (y - y0) / (h > 1 ? h : 1); // 9/16 under the line -> 1/16 at the floor if (bayer(x, y, level)) d.fillRect(x, y, 1, 1); } } // A history as a chart, for a screen with the room (e-ink): a soft frame with // a dotted middle line, the samples as a step line over a dithered glow -- // or, with `bars`, one bar each from the bottom, for counts -- the newest at // the right edge, then a caption line under the frame: `left` at its left // (how far back it goes), `right` at its right. chartH() is the whole // block's height. `axis`, on a chart 30 characters wide or more (the 4.2" // e-ink), prints the top, middle and bottom values in a column to the left // of the frame: it writes one sample value in the unit the reader knows. inline int chartH(DisplayDriver& d, int frame_h) { return frame_h + d.getLineHeight() + 3; } typedef void (*ChartAxisFmt)(char* buf, int n, int v); template inline void chart(DisplayDriver& d, int x, int y, int w, int h, const History& hs, int min_span, bool bars, const char* left, const char* right, ChartAxisFmt axis = nullptr) { const bool empty = hs.n < (bars ? 1 : 2); int lo = 0, hi = 0; if (!empty) { lo = bars ? 0 : hs.at(0); hi = lo; for (int i = 0; i < hs.n; i++) { int v = hs.at(i); if (v < lo) lo = v; if (v > hi) hi = v; } if (hi - lo < min_span) { if (bars) hi = lo + min_span; else { int mid = (hi + lo) / 2; lo = mid - min_span / 2; hi = lo + min_span; } } } const int lh = d.getLineHeight(); if (axis && !empty && w >= 30 * d.getCharWidth() && h >= 2 * lh) { char t[3][12]; const int v[3] = { hi, (hi + lo) / 2, lo }; int gw = 0; for (int k = 0; k < 3; k++) { axis(t[k], sizeof(t[k]), v[k]); int tw = d.getTextWidth(t[k]); if (tw > gw) gw = tw; } d.drawTextRightAlign(x + gw, y + 1, t[0]); if (h >= 3 * lh) d.drawTextRightAlign(x + gw, y + (h - lh) / 2, t[1]); d.drawTextRightAlign(x + gw, y + h - lh - 1, t[2]); x += gw + 3; w -= gw + 3; } d.drawSoftRect(x, y, w, h); const int s = miniIconScale(d); for (int i = x + 3; i < x + w - 3; i += 3 * s) d.fillRect(i, y + h / 2, s, s); const int ix = x + 2, iy = y + 2, iw = w - 4, ih = h - 4; if (empty) { // nothing to draw yet: say so over the line const int tw = d.getTextWidth("no data"), tx = x + (w - tw) / 2, ty = y + (h - lh) / 2; d.setColor(DisplayDriver::DARK); d.fillRect(tx - 2, ty, tw + 4, lh); d.setColor(DisplayDriver::LIGHT); d.setCursor(tx, ty); d.print("no data"); } else { const int bw = iw / N > 1 ? iw / N : 1; int px = -1, py = 0; for (int i = 0; i < hs.n; i++) { const int cx = ix + iw - 1 - (hs.n - 1 - i) * (iw - 1) / (N - 1); const int cy = iy + ih - 1 - (hs.at(i) - lo) * (ih - 1) / (hi - lo); if (bars) { if (hs.at(i) > lo) d.fillRect(cx - bw + 1, cy, bw, iy + ih - cy); } else { if (px >= 0) { d.fillRect(px, py, cx - px + 1, 1); d.fillRect(cx, py < cy ? py : cy, 1, (py < cy ? cy - py : py - cy) + 1); for (int c = px + 1; c <= cx; c++) fadeColumn(d, c, (c < cx ? py : cy) + 2, iy + ih); } px = cx; py = cy; } } } const int ty = y + h + 2; if (left && *left) { d.setCursor(x, ty); d.print(left); } if (right && *right) d.drawTextRightAlign(x + w, ty, right); } // Axis formats for chart(): the unit each History is kept in, as read. inline void axisPlain(char* b, int n, int v) { snprintf(b, n, "%d", v); } inline void axisVolts(char* b, int n, int v) { snprintf(b, n, "%.2f", v / 1000.0f); } // mV inline void axisQuarterDb(char* b, int n, int v) { snprintf(b, n, "%.0f", v / 4.0f); } // dB x4 // Text in the splash's slanted lettering (ui-core/Lettering.h: digits, ':', // '.', '-' and a few letters) at `sc` px a pixel, `gap` px between // characters, its top-left at x / y. letteringW() measures it the same way. inline void lettering(DisplayDriver& d, int x, int y, const char* t, int sc, int gap) { for (const char* p = t; *p; p++) { const int cw = ::lettering::charW(*p); if (cw < 0) continue; for (int r = 0; r < ::lettering::LOGO_H; r++) for (int col = 0; col < cw; col++) { if (!::lettering::inked(*p, col, r)) continue; int e = col; while (e + 1 < cw && ::lettering::inked(*p, e + 1, r)) e++; d.fillRect(x + col * sc, y + r * sc, (e - col + 1) * sc, sc); col = e; } x += cw * sc + gap; } } inline int letteringW(const char* t, int sc, int gap) { return ::lettering::textW(t, 0) * sc + gap * ((int)strlen(t) - 1); } // Blocks stacked down from `top`, with the first `skip` left out: the way a // tab scrolls by whole blocks. Call place(h) before drawing each block; it // says whether the block is on screen and where (at). The totals then give // the scroll indicator and whether DOWN has anything left to show. // A measuring Flow (measure()) only adds the heights up and draws nothing. struct Flow { int y, bottom, skip; int idx = 0, at = 0; long total_px = 0, skipped_px = 0; bool more = false, measuring = false; Flow(int top, int bottom_, int skip_) : y(top), bottom(bottom_), skip(skip_) {} static Flow measure() { Flow f(0, 0, 0); f.measuring = true; return f; } bool place(int h) { total_px += h; if (measuring) return false; if (idx++ < skip) { skipped_px += h; return false; } if (more || y + h > bottom) { more = true; return false; } at = y; y += h; return true; } }; } // namespace info