mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-01 10:21:59 +00:00
404 lines
18 KiB
C++
404 lines
18 KiB
C++
#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 <helpers/ui/DisplayDriver.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <math.h>
|
|
#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 <int N>
|
|
using History = uicore::History<N>;
|
|
|
|
// 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 <int N>
|
|
inline void spark(DisplayDriver& d, int x, int y, int w, int h, const History<N>& 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 <int N>
|
|
inline void chart(DisplayDriver& d, int x, int y, int w, int h, const History<N>& 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
|