feat: scrolling
This commit is contained in:
+115
-7
@@ -36,6 +36,8 @@ enum Flag : uint8_t {
|
||||
INTERACTIVE = 1 << 2, // has an on_press
|
||||
DIRTY = 1 << 3,
|
||||
PRESSED = 1 << 4,
|
||||
SCROLL_X = 1 << 5, // children measure unbounded across, and pan horizontally
|
||||
SCROLL_Y = 1 << 6,
|
||||
};
|
||||
|
||||
enum SizeMode : uint8_t { AUTO, PX, FRACTION, FILL };
|
||||
@@ -118,6 +120,15 @@ struct Spec {
|
||||
uint16_t last = NONE; // tail of the child list, so append is not a walk
|
||||
};
|
||||
|
||||
// A scroll container's pan offset and the size of what it holds. Sparse like
|
||||
// styles, because a screen has one or two of these and Node has no room: the
|
||||
// offsets are what setScroll() clamps against, and the content size is the only
|
||||
// thing measure() learns that place() would otherwise throw away.
|
||||
struct Scroll {
|
||||
int16_t x = 0, y = 0;
|
||||
int16_t contentW = 0, contentH = 0;
|
||||
};
|
||||
|
||||
// Resistive panels land a few pixels off, so a hit box is larger than what was
|
||||
// painted.
|
||||
constexpr int SLOP = 4;
|
||||
@@ -145,6 +156,7 @@ public:
|
||||
labelAt.clear();
|
||||
labels.clear();
|
||||
styles.clear();
|
||||
scrollState.clear();
|
||||
error = nullptr;
|
||||
}
|
||||
|
||||
@@ -215,6 +227,64 @@ public:
|
||||
return (n.flags & INTERACTIVE) ? id : NONE;
|
||||
}
|
||||
|
||||
bool scrolls(uint16_t id) const {
|
||||
return (nodes[id].flags & (SCROLL_X | SCROLL_Y)) != 0;
|
||||
}
|
||||
|
||||
const Scroll* scrollOf(uint16_t id) const {
|
||||
for (size_t at = 0; at < scrollState.size(); at++) {
|
||||
if (scrollState[at].first == id)
|
||||
return &scrollState[at].second;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// How far each axis can pan before the content's far edge reaches the box.
|
||||
void scrollRange(uint16_t id, int& maxX, int& maxY) const {
|
||||
maxX = maxY = 0;
|
||||
const Scroll* s = scrollOf(id);
|
||||
if (!s)
|
||||
return;
|
||||
if (nodes[id].flags & SCROLL_X)
|
||||
maxX = s->contentW - nodes[id].w;
|
||||
if (nodes[id].flags & SCROLL_Y)
|
||||
maxY = s->contentH - nodes[id].h;
|
||||
if (maxX < 0)
|
||||
maxX = 0;
|
||||
if (maxY < 0)
|
||||
maxY = 0;
|
||||
}
|
||||
|
||||
// Pans the subtree, clamped to the content. Applied as a delta to the stored
|
||||
// coordinates rather than by placing again: place() reads Spec, which
|
||||
// dropScratch() has already thrown away, and shifting keeps x/y in screen
|
||||
// space so hit testing, getRect and every paint routine stay unchanged.
|
||||
void setScroll(uint16_t id, int x, int y) {
|
||||
if (!scrolls(id))
|
||||
return;
|
||||
Scroll* state = mutableScroll(id);
|
||||
if (!state)
|
||||
return;
|
||||
int maxX = 0, maxY = 0;
|
||||
scrollRange(id, maxX, maxY);
|
||||
if (x < 0)
|
||||
x = 0;
|
||||
if (y < 0)
|
||||
y = 0;
|
||||
if (x > maxX)
|
||||
x = maxX;
|
||||
if (y > maxY)
|
||||
y = maxY;
|
||||
const int dx = x - state->x, dy = y - state->y;
|
||||
if (dx == 0 && dy == 0)
|
||||
return;
|
||||
state->x = static_cast<int16_t>(x);
|
||||
state->y = static_cast<int16_t>(y);
|
||||
for (uint16_t c = nodes[id].first; c != NONE; c = nodes[c].next)
|
||||
shift(c, -dx, -dy);
|
||||
nodes[id].flags |= DIRTY;
|
||||
}
|
||||
|
||||
// Layout inputs are dead once place() has run. Callers drop them here rather
|
||||
// than carrying ~20 bytes a node for the lifetime of a screen.
|
||||
void dropScratch() {
|
||||
@@ -302,6 +372,25 @@ private:
|
||||
std::vector<uint16_t> labelAt;
|
||||
std::vector<char> labels;
|
||||
std::vector<std::pair<uint16_t, Style>> styles;
|
||||
// Linear rather than the sorted search styles use: a screen has one or two
|
||||
// scroll containers, so the binary search would cost more than the scan.
|
||||
std::vector<std::pair<uint16_t, Scroll>> scrollState;
|
||||
|
||||
Scroll* mutableScroll(uint16_t id) {
|
||||
for (size_t at = 0; at < scrollState.size(); at++) {
|
||||
if (scrollState[at].first == id)
|
||||
return &scrollState[at].second;
|
||||
}
|
||||
scrollState.push_back(std::make_pair(id, Scroll()));
|
||||
return &scrollState.back().second;
|
||||
}
|
||||
|
||||
void shift(uint16_t id, int dx, int dy) {
|
||||
nodes[id].x = static_cast<int16_t>(nodes[id].x + dx);
|
||||
nodes[id].y = static_cast<int16_t>(nodes[id].y + dy);
|
||||
for (uint16_t c = nodes[id].first; c != NONE; c = nodes[c].next)
|
||||
shift(c, dx, dy);
|
||||
}
|
||||
|
||||
void fail(const char* message) {
|
||||
if (!error)
|
||||
@@ -345,6 +434,22 @@ private:
|
||||
// by its content cannot tell a child what fraction of it to take.
|
||||
int innerH = h != UNKNOWN ? h - s.padT - s.padB : UNKNOWN;
|
||||
|
||||
// A scrolled axis is unbounded for the children, so they take their natural
|
||||
// size and the content is free to overflow the box. The box itself still
|
||||
// needs a size of its own on that axis -- one derived from the content
|
||||
// would grow to fit it and never scroll.
|
||||
const uint8_t scrollFlags = nodes[id].flags & (SCROLL_X | SCROLL_Y);
|
||||
if (scrollFlags & SCROLL_X) {
|
||||
if (w == UNKNOWN)
|
||||
fail("scroll-x needs a width");
|
||||
innerW = UNKNOWN;
|
||||
}
|
||||
if (scrollFlags & SCROLL_Y) {
|
||||
if (h == UNKNOWN)
|
||||
fail("scroll-y needs a height");
|
||||
innerH = UNKNOWN;
|
||||
}
|
||||
|
||||
int main = 0, cross = 0, count = 0;
|
||||
for (uint16_t c = nodes[id].first; c != NONE; c = nodes[c].next) {
|
||||
measure(c, innerW, innerH);
|
||||
@@ -373,16 +478,19 @@ private:
|
||||
cross = row ? s.intrinsicH : s.intrinsicW;
|
||||
}
|
||||
|
||||
Node& self = nodes[id];
|
||||
int along = main + (row ? s.padL + s.padR : s.padT + s.padB);
|
||||
int across = cross + (row ? s.padT + s.padB : s.padL + s.padR);
|
||||
if (row) {
|
||||
self.w = static_cast<int16_t>(w != UNKNOWN ? w : along);
|
||||
self.h = static_cast<int16_t>(h != UNKNOWN ? h : across);
|
||||
} else {
|
||||
self.w = static_cast<int16_t>(w != UNKNOWN ? w : across);
|
||||
self.h = static_cast<int16_t>(h != UNKNOWN ? h : along);
|
||||
const int contentW = row ? along : across;
|
||||
const int contentH = row ? across : along;
|
||||
if (scrollFlags) {
|
||||
Scroll* state = mutableScroll(id);
|
||||
state->contentW = static_cast<int16_t>(contentW);
|
||||
state->contentH = static_cast<int16_t>(contentH);
|
||||
}
|
||||
|
||||
Node& self = nodes[id];
|
||||
self.w = static_cast<int16_t>(w != UNKNOWN ? w : contentW);
|
||||
self.h = static_cast<int16_t>(h != UNKNOWN ? h : contentH);
|
||||
}
|
||||
|
||||
void place(uint16_t id, int x, int y, int w, int h) {
|
||||
|
||||
@@ -137,6 +137,36 @@ public:
|
||||
// clean up ghosting all stay here. A live LCD has nothing pending and does
|
||||
// nothing.
|
||||
virtual void commit() = 0;
|
||||
// Offscreen band for composited repaints. beginBuffer opens a RAM surface
|
||||
// covering the screen rectangle (x, y, w, h); every subsequent draw keeps its
|
||||
// screen coordinates (the driver translates into the band) and clips to it,
|
||||
// so a caller re-walks the same tree per band without renumbering anything.
|
||||
// present() blits the band back to (x, y) and returns drawing to the panel.
|
||||
// Re-rasterizing into RAM and pushing once collapses a per-primitive bus
|
||||
// transaction storm into a streamed write, which is what direct-to-panel
|
||||
// drawing cannot make smooth. A driver with no spare RAM (or an e-ink panel
|
||||
// that gains nothing) refuses the band, so the painter draws to the panel.
|
||||
virtual bool beginBuffer(int32_t x, int32_t y, int32_t w, int32_t h) {
|
||||
(void)x;
|
||||
(void)y;
|
||||
(void)w;
|
||||
(void)h;
|
||||
return false;
|
||||
}
|
||||
virtual void present() {}
|
||||
// Clips every draw to this screen rectangle until clearClip(). The painter
|
||||
// wraps a custom node in its own box, because a node owns its box and nothing
|
||||
// else's: a band is usually taller than the node it covers, so a painter that
|
||||
// draws outside itself -- a list row half scrolled off the top -- would
|
||||
// otherwise land on whatever else that band covers. Not clipping is only safe
|
||||
// while no custom node ever overdraws.
|
||||
virtual void setClip(int32_t x, int32_t y, int32_t w, int32_t h) {
|
||||
(void)x;
|
||||
(void)y;
|
||||
(void)w;
|
||||
(void)h;
|
||||
}
|
||||
virtual void clearClip() {}
|
||||
virtual void fillPolygon(const int32_t* xs, const int32_t* ys, size_t count,
|
||||
int32_t color) = 0;
|
||||
// Null coordinates centre the image; null bounds fall back to the panel size.
|
||||
|
||||
Reference in New Issue
Block a user