2025-11-15 09:07:39 +08:00

192 lines
6.1 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#ifndef UI_H
#define UI_H
#include <Arduino.h>
#include <Adafruit_ST7735.h>
#include <Adafruit_GFX.h>
#include <LittleFS.h>
#include <U8g2_for_Adafruit_GFX.h>
#include "utils.h"
namespace esp32 {
namespace ui {
// 简易图标RGB565 像素数组,内存持有
struct Icon {
uint16_t width = 0;
uint16_t height = 0;
uint16_t* pixels = nullptr; // width*height 大小的 RGB565 数组
};
// 显示操作类型
enum class OpType : uint8_t { FillRect = 0, DrawText = 1, DrawIcon = 2, Line = 3, Circle = 4 };
// 显示项Display List 条目)
struct DisplayItem {
OpType op;
uint16_t key; // 用于跨帧识别(简化版,当前全量重放)
int16_t x;
int16_t y;
int16_t w; // FillRect 使用 或 Line/Circle 半径/终点 x
int16_t h; // FillRect 使用 或 Line 终点 y
uint16_t color; // FillRect/DrawText 使用
const char* text; // 指向 textBuf 或字符串字面量
const Icon* icon; // DrawIcon 使用
char textBuf[64]; // 小型文本缓冲存放动态生成的字符串例如时间、lap
};
// 固定容量的显示列表
class DisplayList {
public:
static const int MAX_ITEMS = 256;
DisplayList() : _count(0) {}
void clear() { _count = 0; }
int size() const { return _count; }
const DisplayItem& at(int i) const { return _items[i]; }
bool addFillRect(uint16_t key, int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color) {
if (_count >= MAX_ITEMS) return false;
auto& it = _items[_count++];
it.op = OpType::FillRect;
it.key = key;
it.x = x; it.y = y; it.w = w; it.h = h; it.color = color;
it.text = nullptr; it.icon = nullptr;
return true;
}
bool addText(uint16_t key, int16_t x, int16_t y, uint16_t color, const char* text, bool centered = false) {
if (_count >= MAX_ITEMS) return false;
auto& it = _items[_count++];
it.op = OpType::DrawText;
it.key = key;
it.x = x; it.y = y;
it.w = centered ? 1 : 0; // w=1 表示需要居中
it.h = 0;
it.color = color;
// 复制文本到缓冲,保证作用域结束后仍可用
if (text) {
strncpy(it.textBuf, text, sizeof(it.textBuf) - 1);
it.textBuf[sizeof(it.textBuf) - 1] = '\0';
it.text = it.textBuf;
} else {
it.textBuf[0] = '\0';
it.text = it.textBuf;
}
it.icon = nullptr;
return true;
}
bool addIcon(uint16_t key, int16_t x, int16_t y, const Icon* icon) {
if (_count >= MAX_ITEMS) return false;
auto& it = _items[_count++];
it.op = OpType::DrawIcon;
it.key = key;
it.x = x; it.y = y; it.w = 0; it.h = 0; it.color = 0;
it.text = nullptr; it.icon = icon;
return true;
}
bool addLine(uint16_t key, int16_t x1, int16_t y1, int16_t x2, int16_t y2, uint16_t color) {
if (_count >= MAX_ITEMS) return false;
auto& it = _items[_count++];
it.op = OpType::Line;
it.key = key;
it.x = x1; it.y = y1; it.w = x2; it.h = y2; it.color = color;
it.text = nullptr; it.icon = nullptr;
return true;
}
bool addCircle(uint16_t key, int16_t cx, int16_t cy, int16_t radius, uint16_t color) {
if (_count >= MAX_ITEMS) return false;
auto& it = _items[_count++];
it.op = OpType::Circle;
it.key = key;
it.x = cx; it.y = cy; it.w = radius; it.h = 0; it.color = color;
it.text = nullptr; it.icon = nullptr;
return true;
}
private:
int _count;
DisplayItem _items[MAX_ITEMS];
};
// Tween补间动画
struct Tween {
float* target = nullptr;
float from = 0.0f;
float to = 0.0f;
uint32_t startMs = 0;
uint32_t durationMs = 0;
float x1 = 0.42f, y1 = 0.0f, x2 = 0.58f, y2 = 1.0f; // 贝塞尔参数
bool active = false;
};
// UI 框架主体
class UI {
public:
UI() : _tft(nullptr), _u8g2(nullptr), _builder(nullptr), _builderCtx(nullptr) {}
void begin(Adafruit_ST7735* tft, U8G2_FOR_ADAFRUIT_GFX* u8g2 = nullptr) {
_tft = tft;
_u8g2 = u8g2;
}
// builder: 负责把状态转为显示列表
using BuilderFn = void (*)(void* ctx, DisplayList& out);
void setScene(BuilderFn builder, void* ctx) {
_builder = builder;
_builderCtx = ctx;
}
// 每帧调用:更新动画 + 生成显示列表 + 渲染
void tick(uint32_t nowMs);
// 添加一个 float 补间(从当前值到目标值)
int addTweenFloat(float* target, float to, uint32_t durationMs,
float x1 = 0.42f, float y1 = 0.0f, float x2 = 0.58f, float y2 = 1.0f);
bool hasActiveTweens() const;
bool isTweenActive(int id) const;
// 图标加载:文件格式 (little-endian): [2字节宽][2字节高][像素: width*height*2]
static bool loadIconFromFS(const char* path, Icon& out);
static void freeIcon(Icon& icon);
// 启用内存帧缓冲(双缓冲),第一次调用会分配两块 16 位色的画布
bool enableBackBuffer(uint16_t width, uint16_t height);
bool isBackBufferEnabled() const { return _useBackBuffer; }
private:
void _updateTweens(uint32_t nowMs);
void _render(const DisplayList& list);
void _drawIcon(int16_t x, int16_t y, const Icon& icon);
void _renderToCanvas(const DisplayList& list); // 使用内存画布绘制
void _flushCanvas(); // 将当前画布推送到屏幕
void _swapCanvas(); // 交换前后缓冲
Adafruit_ST7735* _tft;
U8G2_FOR_ADAFRUIT_GFX* _u8g2;
BuilderFn _builder;
void* _builderCtx;
DisplayList _list;
static const int MAX_TWEENS = 8;
Tween _tweens[MAX_TWEENS];
// 双缓冲相关
bool _useBackBuffer = false;
GFXcanvas16* _canvasA = nullptr; // 当前绘制目标
GFXcanvas16* _canvasB = nullptr; // 上一帧
GFXcanvas16* _canvasCurrent = nullptr;
GFXcanvas16* _canvasPrev = nullptr;
uint16_t _bufW = 0;
uint16_t _bufH = 0;
};
} // namespace ui
} // namespace esp32
#endif // UI_H