feat: scrolling
This commit is contained in:
@@ -26,7 +26,9 @@ The app image is `.pio/build/esp32-32e/firmware.bin`; `bootloader.bin` and `part
|
||||
|
||||
## Scripted Test
|
||||
|
||||
Use an executable `.e32r40t` script. Assert on the app path the firmware logs rather than assuming which row a tap hits:
|
||||
Use an executable `.e32r40t` script. Every app is loaded through `/.lua/main.lua`, so that is
|
||||
the only path the firmware ever logs -- `wait-log "running Scroll"` matches nothing and burns
|
||||
the whole timeout. Wait for a log the app itself writes in `init()`:
|
||||
|
||||
```text
|
||||
#!/usr/bin/env -S esp-emu --board e32r40t
|
||||
@@ -36,7 +38,7 @@ wait-log "\[lua\] info: home ready" --timeout 180
|
||||
wait-idle 3
|
||||
|
||||
tap 85 127
|
||||
wait-log "running Hello"
|
||||
wait-log "\[lua\] info: hello ready"
|
||||
wait-idle 3
|
||||
|
||||
capture _scratch/emulator-app.png
|
||||
|
||||
@@ -81,8 +81,28 @@ Layout has not run while a builder is running, so an app sizing itself to the fr
|
||||
`ui.frame()`, not `screen.getHeight()` -- the panel is not the box the app was given. Whatever
|
||||
mounts the tree sets the difference with `ui.setInset()`.
|
||||
|
||||
Layout is `-Wall -Wextra` C++ free of Arduino headers, so `test/ui_layout_test.cpp` runs it
|
||||
on the host through `make test-cpp`. Adding a **primitive** (a paint routine, a layout mode)
|
||||
**Scrolling is the tree's, not an app's.** `ui.box{scrollX=true, scrollY=true}` measures its
|
||||
children unbounded on the scrolled axes, so the content overflows the box; `ui.setScroll()`
|
||||
pans it, clamped to `ui.getScrollRange()`. A scrolled axis needs a size of its own -- one
|
||||
derived from the content would grow to fit it and never scroll -- and `"fill"` is the wrong
|
||||
size, because it resolves against the panel rather than the pane the app was given: that is
|
||||
what `ui.frame()` is for. Panning applies a **delta to the stored coordinates** rather than
|
||||
placing again, because `place()` reads the `Spec` that `dropScratch()` already dropped; x/y
|
||||
stay in screen space, so hit testing, `getRect` and every paint routine are unchanged. Rows
|
||||
are ordinary nodes, so the painter culls them to the band and no Lua runs while painting --
|
||||
which is the point. A container does not virtualize, so ~120 nodes is comfortable and a
|
||||
thousand-row list still wants one purpose-built painter.
|
||||
|
||||
**Anything drawing past its own box is scissored to it.** A band is usually taller than the
|
||||
node it covers, so a scroll container's overflowing children, and a `custom` painter that
|
||||
draws where it likes, would otherwise land on whatever else the band holds -- a list row
|
||||
half scrolled off the top painting over the status bar. `gui.setClip`/`clearClip` is that
|
||||
scissor, nested so a custom node inside a scroll container narrows the container's box
|
||||
instead of replacing it. On this panel it is `TFT_eSPI`'s viewport; restore it explicitly,
|
||||
because `resetViewport()` reads a non-virtual `width()` and on a sprite answers the panel's.
|
||||
|
||||
Layout is `-Wall -Wextra` C++ free of Arduino headers, so `native/test/runtime_test.cpp` in
|
||||
the submodule runs it on the host. Adding a **primitive** (a paint routine, a layout mode)
|
||||
means C++ and a reflash; adding **composition** (`ui.confirm`, a new card) is still Lua on
|
||||
the SD card. Custom painting is the seam between them: a `custom` node paints itself through
|
||||
the `screen` bindings.
|
||||
@@ -136,13 +156,58 @@ number page is narrower than the letter page, and the letter page's outer keys s
|
||||
otherwise. Press feedback that must not repaint the whole node is `on_down` drawing one
|
||||
region and `on_unpress` restoring it after the 80 ms hold.
|
||||
|
||||
## Banded Repaint
|
||||
|
||||
**The painter composites, apps do not.** `tree.draw()` unions the dirty nodes into one
|
||||
rectangle and paints it a band at a time: `gui.beginBuffer(x, y, w, h)` opens an offscreen
|
||||
16bpp sprite, the tree is re-walked painting whatever overlaps that band, and `present()`
|
||||
blits it. The tree *is* the display list, so a band re-walks it rather than replaying a
|
||||
recorded command stream, and one push replaces a bus transaction per primitive -- which is
|
||||
what lets a whole pane move smoothly. Apps draw in screen coordinates and call
|
||||
`ui.invalidate(id)`; `apps/Scroll` scrolls with no band code of its own.
|
||||
|
||||
A band that overlaps a node paints the **whole** node clipped to the band, so a node taller
|
||||
than a band is painted once per band it spans -- including a `custom` painter, whose Lua
|
||||
callback is re-invoked per band. Cull inside a long custom painter; a list long enough to
|
||||
matter wants virtualized nodes (build only the visible rows) rather than one tall painter.
|
||||
|
||||
**Band height adapts and must.** How much contiguous heap exists depends on the app, the
|
||||
orientation and the fragmentation already there -- an app launch leaves ~48KB, but ~28KB
|
||||
once its Lua state is live, and a 320x48 band is 30KB. `drawBanded` halves the band on a
|
||||
failed allocation down to `MIN_BAND` before giving up, and a repaint that can allocate
|
||||
nothing falls back to painting dirty nodes straight to the panel (which is what an e-ink
|
||||
provider wants anyway -- the default `beginBuffer` refuses).
|
||||
|
||||
**A primitive drawing into a band must clip every write itself.** `TFT_eSprite` does not
|
||||
clip `pushImage`, and a node straddling a band edge will otherwise write past the sprite and
|
||||
corrupt the heap -- the failure looks like a hang in a later `present()`, not a fault where
|
||||
the write happened. `paintRoundRect` therefore clamps its fills (`fillClip`) and draws
|
||||
corner spans per pixel instead of pushing them. Adding a primitive means doing the same.
|
||||
|
||||
## Drawing Pitfalls
|
||||
|
||||
`screen.*` coordinate args go through `checkInt`, which rejects a non-integral float
|
||||
(`108.5`) with "number has no integer representation" -- and an error raised inside a paint
|
||||
or timer callback aborts silently, leaving a blank pane with no log. Floor computed
|
||||
coordinates with `//` (`(w - tw) // 2`), never `/`.
|
||||
|
||||
A continuously repainting pane cannot be photographed: the emulator's capture returns the
|
||||
last settled frame, so a running animation reads as the *previous* screen. Assert motion
|
||||
from a log (`scrollY=`), and capture with the animation stopped to check the rendering.
|
||||
|
||||
## Touch and Drag
|
||||
|
||||
The firmware fires `onTouchDown` / `onTouchMove` / `onTouchUp` plus the `onTouch` tap
|
||||
alias. `onTouchMove` filters 2px of XPT2046 jitter and nothing else; apps such as Paint
|
||||
consume it directly so a stroke starts at the first real pixel. Keep ordinary toolkit UIs
|
||||
frame-sized. If a real long-list use case appears, prefer one purpose-built painter over a
|
||||
component per row.
|
||||
consume it directly so a stroke starts at the first real pixel. Dragging a scroll container
|
||||
is an app's job for now: C++ owns the offset and its clamping, nothing else.
|
||||
|
||||
**Animation belongs in `draw(deltaMs)`, timers in discrete events.** `Timer::collectDue`
|
||||
reschedules from the moment it fired, so a repeating timer silently drops every tick it
|
||||
misses -- `timer.every(16, ...)` advancing a fixed step per tick gives a speed that
|
||||
collapses with frame time and surges when it recovers, which reads as rubbery motion rather
|
||||
than as slowness. Scale by the elapsed milliseconds `draw` is handed instead, and floor once
|
||||
at the point of use, since `screen.*` rejects a non-integral float.
|
||||
|
||||
## Status Bar
|
||||
|
||||
|
||||
+1
-1
Submodule lib/esp32-lua-api updated: 26f3fdb6e6...e5ed980294
+4
-1
@@ -28,7 +28,10 @@ build_flags =
|
||||
; TFT_BACKLIGHT_ON the panel renders correctly and stays dark.
|
||||
-DTFT_BL=27
|
||||
-DTFT_BACKLIGHT_ON=HIGH
|
||||
-DSPI_FREQUENCY=40000000
|
||||
; 80MHz is what the panel vendor's own ESP-IDF/LVGL port runs this ST7796 at, and the bus
|
||||
; is the floor on a full-pane repaint: 320x436x16bpp is 279KB, which is 56ms at 40MHz and
|
||||
; 28ms at 80. Drop back to 40000000 if a module shows pixel garbage.
|
||||
-DSPI_FREQUENCY=80000000
|
||||
-DSPI_TOUCH_FREQUENCY=2500000
|
||||
-DLOAD_GLCD
|
||||
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
local ui = require "ui"
|
||||
|
||||
-- Rows are ordinary nodes inside a scrolling box, so this app draws nothing itself: the
|
||||
-- tree clips the subtree to the box, the painter culls to the band, and a drag is one
|
||||
-- integer per frame. Both axes pan -- the rows are wider than the pane -- so a diagonal
|
||||
-- drag moves the content diagonally.
|
||||
local ROWS = 60
|
||||
local ROW_H = 64
|
||||
local ROW_W = 420 -- wider than the pane, so there is something to pan across
|
||||
local GAP = 8
|
||||
|
||||
-- Fraction of a flick's speed still left after a second. Applied as DECAY^(dt/1000) rather
|
||||
-- than per frame, so the glide takes the same time whatever the frame rate.
|
||||
local DECAY = 0.02
|
||||
-- Below this the glide is no longer visible and would only keep the pane repainting.
|
||||
local STOP_PX_PER_SEC = 8
|
||||
|
||||
---@class ScrollApp : SlateApp, TouchHandlers
|
||||
local M = {}
|
||||
local list
|
||||
local scrollX, scrollY = 0, 0
|
||||
-- Drag deltas accumulate here and are applied in draw(), because that is the only place
|
||||
-- with a real elapsed time to turn them into a speed. Touch handlers just record.
|
||||
local dragX, dragY = 0, 0
|
||||
local velocityX, velocityY = 0, 0
|
||||
local lastX, lastY
|
||||
local dragging = false
|
||||
|
||||
function M.init()
|
||||
log.info "scroll demo ready"
|
||||
end
|
||||
|
||||
function M.onTouchDown(x, y)
|
||||
dragging = true
|
||||
lastX, lastY = x, y
|
||||
dragX, dragY = 0, 0
|
||||
velocityX, velocityY = 0, 0 -- a finger down catches the glide, like every touch UI
|
||||
end
|
||||
|
||||
function M.onTouchMove(x, y)
|
||||
if not dragging then
|
||||
return
|
||||
end
|
||||
-- Content follows the finger, so dragging down moves the list up.
|
||||
dragX = dragX + (lastX - x)
|
||||
dragY = dragY + (lastY - y)
|
||||
lastX, lastY = x, y
|
||||
end
|
||||
|
||||
function M.onTouchUp()
|
||||
dragging = false
|
||||
end
|
||||
|
||||
---@param deltaMs integer
|
||||
function M.draw(deltaMs)
|
||||
if deltaMs <= 0 then
|
||||
return
|
||||
end
|
||||
local seconds = deltaMs / 1000
|
||||
|
||||
if dragging then
|
||||
scrollX, scrollY = scrollX + dragX, scrollY + dragY
|
||||
-- Speed measured over the frame the movement actually arrived in. A finger that stopped
|
||||
-- moving reports zero, which is what makes a hold-then-lift not flick.
|
||||
velocityX, velocityY = dragX / seconds, dragY / seconds
|
||||
dragX, dragY = 0, 0
|
||||
elseif velocityX ~= 0 or velocityY ~= 0 then
|
||||
scrollX, scrollY = scrollX + velocityX * seconds, scrollY + velocityY * seconds
|
||||
local keep = DECAY ^ seconds
|
||||
velocityX, velocityY = velocityX * keep, velocityY * keep
|
||||
if math.abs(velocityX) < STOP_PX_PER_SEC then
|
||||
velocityX = 0
|
||||
end
|
||||
if math.abs(velocityY) < STOP_PX_PER_SEC then
|
||||
velocityY = 0
|
||||
end
|
||||
else
|
||||
return -- nothing moving, so nothing to repaint
|
||||
end
|
||||
|
||||
ui.setScroll(list, math.floor(scrollX), math.floor(scrollY))
|
||||
-- Read back what the clamp allowed: without this the offset keeps running past the end
|
||||
-- while the content sits still, and the list ignores the first part of the drag back.
|
||||
local clampedX, clampedY = ui.getScroll(list)
|
||||
if clampedX ~= math.floor(scrollX) then
|
||||
scrollX, velocityX = clampedX, 0
|
||||
end
|
||||
if clampedY ~= math.floor(scrollY) then
|
||||
scrollY, velocityY = clampedY, 0
|
||||
end
|
||||
end
|
||||
|
||||
function M.node()
|
||||
-- Sized to the frame, not "fill": fill resolves against the whole panel, and the pane the
|
||||
-- app was given is shorter by the status bar. The difference is the scroll range.
|
||||
local frameW, frameH = ui.frame()
|
||||
local rows = { gap = GAP, pad = GAP, scrollX = true, scrollY = true, w = frameW, h = frameH }
|
||||
for i = 1, ROWS do
|
||||
rows[#rows + 1] = ui.box {
|
||||
w = ROW_W,
|
||||
h = ROW_H - GAP,
|
||||
justify = "center",
|
||||
align = "center",
|
||||
background = (i % 2 == 1) and ui.theme.face or ui.theme.background,
|
||||
border = ui.theme.muted,
|
||||
ui.label("Row " .. i, { font = screen.FONT_LARGE }),
|
||||
}
|
||||
end
|
||||
list = ui.box(rows)
|
||||
return list
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <XPT2046_Touchscreen.h>
|
||||
#include <lua/providers.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
|
||||
class LuaHost;
|
||||
@@ -97,10 +98,28 @@ public:
|
||||
const int32_t* background) override;
|
||||
// The panel is live, so there is nothing pending to apply.
|
||||
void commit() override {}
|
||||
bool beginBuffer(int32_t x, int32_t y, int32_t w, int32_t h) override;
|
||||
void present() override;
|
||||
void setClip(int32_t x, int32_t y, int32_t w, int32_t h) override;
|
||||
void clearClip() override;
|
||||
|
||||
private:
|
||||
// Every draw op targets this: the panel normally, an 8bpp sprite between
|
||||
// beginBuffer and present. A full-panel 16bpp sprite (300KB) will not
|
||||
// allocate on this board's fragmented heap, so the band trades RGB332 banding
|
||||
// for a size that fits, and the painter composites the frame a band at a time.
|
||||
TFT_eSPI& out() { return buffer ? *buffer : tft; }
|
||||
|
||||
TFT_eSPI& tft;
|
||||
LuaHost& host;
|
||||
TFT_eSprite* buffer = nullptr;
|
||||
// The band's screen origin, subtracted from every coordinate so callers draw
|
||||
// in screen space; zero (a no-op) while drawing straight to the panel.
|
||||
int32_t bufX = 0, bufY = 0;
|
||||
// Sprite bounds, so roundRect's corner spans clip to the band. TFT_eSprite
|
||||
// clips fills and text but not pushImage, and an unclipped span past the
|
||||
// sprite corrupts the heap. INT32_MAX on the panel disables the clamp.
|
||||
int32_t clipW = INT32_MAX, clipH = INT32_MAX;
|
||||
};
|
||||
|
||||
class Http : public esp32lua::HttpProvider {
|
||||
|
||||
+108
-27
@@ -18,9 +18,35 @@ constexpr int MAX_SPAN =
|
||||
// alpha, so edge pixels blend against `surface`.
|
||||
void paintRoundRect(TFT_eSPI& tft, int x, int y, int w, int h, float radius,
|
||||
uint16_t surface, bool hasFill, uint16_t top,
|
||||
uint16_t bottom, bool hasBorder, uint16_t border) {
|
||||
uint16_t bottom, bool hasBorder, uint16_t border,
|
||||
int clipW, int clipH) {
|
||||
if (w <= 0 || h <= 0 || w > MAX_SPAN)
|
||||
return;
|
||||
|
||||
// TFT_eSprite clips fills and text but not pushImage, so a corner span past a
|
||||
// band's edge corrupts the heap. Clamp each span to the target bounds; the
|
||||
// panel passes INT32_MAX bounds, so this is a no-op there.
|
||||
// Per-pixel rather than pushImage: pushImage into a TFT_eSprite corrupts the
|
||||
// heap when the span sits near a band edge, whereas drawPixel clips cleanly on
|
||||
// both a sprite and the panel. Spans are corner-sized, so the cost is trivial.
|
||||
auto pushSpan = [&](int px, int py, int pw, uint16_t* pixels) {
|
||||
if (py < 0 || py >= clipH)
|
||||
return;
|
||||
for (int i = 0; i < pw; i++) {
|
||||
const int xx = px + i;
|
||||
if (xx >= 0 && xx < clipW)
|
||||
tft.drawPixel(xx, py, pixels[i]);
|
||||
}
|
||||
};
|
||||
// TFT_eSprite does not reliably clip a fill or line taller than the band, so
|
||||
// a straddling rounded box would run off the buffer. Clamp every write here.
|
||||
auto fillClip = [&](int px, int py, int pw, int ph, uint16_t color) {
|
||||
int x0 = px < 0 ? 0 : px, y0 = py < 0 ? 0 : py;
|
||||
int x1 = px + pw > clipW ? clipW : px + pw;
|
||||
int y1 = py + ph > clipH ? clipH : py + ph;
|
||||
if (x1 > x0 && y1 > y0)
|
||||
tft.fillRect(x0, y0, x1 - x0, y1 - y0, color);
|
||||
};
|
||||
float halfWidth = w * 0.5f, halfHeight = h * 0.5f;
|
||||
if (radius < 0.0f)
|
||||
radius = 0.0f;
|
||||
@@ -48,7 +74,7 @@ void paintRoundRect(TFT_eSPI& tft, int x, int y, int w, int h, float radius,
|
||||
pixel = gfx::blend565(pixel, border, outer - inner);
|
||||
span[column] = pixel;
|
||||
}
|
||||
tft.pushImage(x, y + row, w, 1, span);
|
||||
pushSpan(x, y + row, w, span);
|
||||
}
|
||||
tft.setSwapBytes(previousSwap);
|
||||
return;
|
||||
@@ -58,19 +84,19 @@ void paintRoundRect(TFT_eSPI& tft, int x, int y, int w, int h, float radius,
|
||||
// only at corners. The cost drops from O(w*h) to O(radius^2) per-pixel work.
|
||||
const uint16_t fill = hasFill ? top : surface;
|
||||
if (h > 2 * ir)
|
||||
tft.fillRect(x, y + ir, w, h - 2 * ir, fill);
|
||||
fillClip(x, y + ir, w, h - 2 * ir, fill);
|
||||
if (w > 2 * ir) {
|
||||
tft.fillRect(x + ir, y, w - 2 * ir, ir, fill);
|
||||
tft.fillRect(x + ir, y + h - ir, w - 2 * ir, ir, fill);
|
||||
fillClip(x + ir, y, w - 2 * ir, ir, fill);
|
||||
fillClip(x + ir, y + h - ir, w - 2 * ir, ir, fill);
|
||||
}
|
||||
if (hasBorder) {
|
||||
if (w > 2 * ir) {
|
||||
tft.drawFastHLine(x + ir, y, w - 2 * ir, border);
|
||||
tft.drawFastHLine(x + ir, y + h - 1, w - 2 * ir, border);
|
||||
fillClip(x + ir, y, w - 2 * ir, 1, border);
|
||||
fillClip(x + ir, y + h - 1, w - 2 * ir, 1, border);
|
||||
}
|
||||
if (h > 2 * ir) {
|
||||
tft.drawFastVLine(x, y + ir, h - 2 * ir, border);
|
||||
tft.drawFastVLine(x + w - 1, y + ir, h - 2 * ir, border);
|
||||
fillClip(x, y + ir, 1, h - 2 * ir, border);
|
||||
fillClip(x + w - 1, y + ir, 1, h - 2 * ir, border);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +123,7 @@ void paintRoundRect(TFT_eSPI& tft, int x, int y, int w, int h, float radius,
|
||||
pixel = gfx::blend565(pixel, border, outer - inner);
|
||||
span[col] = pixel;
|
||||
}
|
||||
tft.pushImage(x + colStart, y + rowStart + row, ir, 1, span);
|
||||
pushSpan(x + colStart, y + rowStart + row, ir, span);
|
||||
}
|
||||
}
|
||||
tft.setSwapBytes(previousSwap);
|
||||
@@ -154,24 +180,79 @@ esp32lua::Status Gui::setTheme(const std::string& theme) {
|
||||
int32_t Gui::color(int32_t r, int32_t g, int32_t b) const {
|
||||
return tft.color565(r, g, b);
|
||||
}
|
||||
void Gui::clear(int32_t color) { tft.fillScreen(color); }
|
||||
void Gui::clear(int32_t color) { out().fillScreen(color); }
|
||||
void Gui::fillRect(int32_t x, int32_t y, int32_t w, int32_t h, int32_t color) {
|
||||
tft.fillRect(x, y, w, h, color);
|
||||
out().fillRect(x - bufX, y - bufY, w, h, color);
|
||||
}
|
||||
void Gui::drawRect(int32_t x, int32_t y, int32_t w, int32_t h, int32_t color) {
|
||||
tft.drawRect(x, y, w, h, color);
|
||||
out().drawRect(x - bufX, y - bufY, w, h, color);
|
||||
}
|
||||
void Gui::drawPixel(int32_t x, int32_t y, int32_t color) {
|
||||
tft.drawPixel(x, y, color);
|
||||
out().drawPixel(x - bufX, y - bufY, color);
|
||||
}
|
||||
|
||||
// The painter opens one band at a time and closes it with present(). An 8bpp
|
||||
// sprite of the whole panel (~140KB) will not allocate on this heap, so a band
|
||||
// is a slice of it; refusing a second open keeps the first from being orphaned.
|
||||
bool Gui::beginBuffer(int32_t x, int32_t y, int32_t w, int32_t h) {
|
||||
if (buffer)
|
||||
return false;
|
||||
buffer = new TFT_eSprite(&tft);
|
||||
// 16bpp, not 8: roundRect anti-aliases its corners by pushing 16-bit spans,
|
||||
// which an 8bpp sprite cannot take. Two bytes a pixel means smaller bands,
|
||||
// which the painter already assumes.
|
||||
buffer->setColorDepth(16);
|
||||
if (!buffer->createSprite(w, h)) {
|
||||
delete buffer;
|
||||
buffer = nullptr;
|
||||
return false;
|
||||
}
|
||||
bufX = x;
|
||||
bufY = y;
|
||||
clipW = w;
|
||||
clipH = h;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Gui::present() {
|
||||
if (!buffer)
|
||||
return;
|
||||
buffer->pushSprite(bufX, bufY);
|
||||
buffer->deleteSprite();
|
||||
delete buffer;
|
||||
buffer = nullptr;
|
||||
bufX = bufY = 0;
|
||||
clipW = clipH = INT32_MAX;
|
||||
}
|
||||
|
||||
// TFT_eSPI's viewport is the native scissor: it clips fills, pixels and text on both the
|
||||
// panel and a sprite. A false vpDatum keeps the origin where it already is, so callers go
|
||||
// on drawing in the space the band translated them into rather than a third one.
|
||||
void Gui::setClip(int32_t x, int32_t y, int32_t w, int32_t h) {
|
||||
out().setViewport(x - bufX, y - bufY, w, h, false);
|
||||
}
|
||||
|
||||
// Restored explicitly rather than with resetViewport(), which reads TFT_eSPI::width() --
|
||||
// not virtual, so on a sprite it answers the panel's width and would leave the clip wider
|
||||
// than the band, which is how a write runs off the buffer and corrupts the heap.
|
||||
void Gui::clearClip() {
|
||||
if (buffer)
|
||||
buffer->setViewport(0, 0, clipW, clipH, false);
|
||||
else
|
||||
tft.resetViewport();
|
||||
}
|
||||
|
||||
void Gui::drawLine(int32_t x1, int32_t y1, int32_t x2, int32_t y2,
|
||||
int32_t color, int32_t width) {
|
||||
x1 -= bufX;
|
||||
x2 -= bufX;
|
||||
y1 -= bufY;
|
||||
y2 -= bufY;
|
||||
if (width <= 1) {
|
||||
tft.drawLine(x1, y1, x2, y2, color);
|
||||
out().drawLine(x1, y1, x2, y2, color);
|
||||
return;
|
||||
}
|
||||
tft.drawWideLine(x1, y1, x2, y2, static_cast<float>(width), color, color);
|
||||
out().drawWideLine(x1, y1, x2, y2, static_cast<float>(width), color, color);
|
||||
}
|
||||
|
||||
void Gui::drawCircle(int32_t x, int32_t y, int32_t radius, int32_t color,
|
||||
@@ -179,14 +260,14 @@ void Gui::drawCircle(int32_t x, int32_t y, int32_t radius, int32_t color,
|
||||
for (int32_t ring = 0; ring < (width < 1 ? 1 : width); ring++) {
|
||||
const int32_t r = radius - ring;
|
||||
if (r > 0)
|
||||
tft.drawCircle(x, y, r, color);
|
||||
out().drawCircle(x - bufX, y - bufY, r, color);
|
||||
}
|
||||
}
|
||||
|
||||
void Gui::fillCircle(int32_t x, int32_t y, int32_t radius, int32_t color,
|
||||
const int32_t* background) {
|
||||
tft.fillSmoothCircle(x, y, radius, color,
|
||||
background ? *background : TFT_WHITE);
|
||||
out().fillSmoothCircle(x - bufX, y - bufY, radius, color,
|
||||
background ? *background : TFT_WHITE);
|
||||
}
|
||||
|
||||
void Gui::roundRect(int32_t x, int32_t y, int32_t w, int32_t h, int32_t radius,
|
||||
@@ -194,10 +275,10 @@ void Gui::roundRect(int32_t x, int32_t y, int32_t w, int32_t h, int32_t radius,
|
||||
const int32_t* bottom, const int32_t* border) {
|
||||
const uint16_t fillTop = top ? static_cast<uint16_t>(*top) : 0;
|
||||
const uint16_t fillBottom = bottom ? static_cast<uint16_t>(*bottom) : fillTop;
|
||||
paintRoundRect(tft, x, y, w, h, static_cast<float>(radius),
|
||||
paintRoundRect(out(), x - bufX, y - bufY, w, h, static_cast<float>(radius),
|
||||
static_cast<uint16_t>(background), top != nullptr, fillTop,
|
||||
fillBottom, border != nullptr,
|
||||
border ? static_cast<uint16_t>(*border) : 0);
|
||||
border ? static_cast<uint16_t>(*border) : 0, clipW, clipH);
|
||||
}
|
||||
|
||||
// Scanline fill: TFT_eSPI only offers triangles, and fanning a concave shape
|
||||
@@ -235,8 +316,8 @@ void Gui::fillPolygon(const int32_t* xs, const int32_t* ys, size_t count,
|
||||
}
|
||||
}
|
||||
for (size_t at = 0; at + 1 < found; at += 2) {
|
||||
tft.drawFastHLine(crossings[at], y, crossings[at + 1] - crossings[at] + 1,
|
||||
color);
|
||||
out().drawFastHLine(crossings[at] - bufX, y - bufY,
|
||||
crossings[at + 1] - crossings[at] + 1, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -263,13 +344,13 @@ int32_t Gui::fontHeight(int32_t font, int32_t) const {
|
||||
|
||||
void Gui::drawText(int32_t font, int32_t x, int32_t y, const std::string& text,
|
||||
int32_t color, int32_t, const int32_t* background) {
|
||||
tft.setTextSize(scaleFor(font));
|
||||
out().setTextSize(scaleFor(font));
|
||||
if (background) {
|
||||
tft.setTextColor(color, *background);
|
||||
out().setTextColor(color, *background);
|
||||
} else {
|
||||
tft.setTextColor(color);
|
||||
out().setTextColor(color);
|
||||
}
|
||||
tft.drawString(text.c_str(), x, y);
|
||||
out().drawString(text.c_str(), x - bufX, y - bufY);
|
||||
}
|
||||
|
||||
} // namespace slate
|
||||
|
||||
Reference in New Issue
Block a user