#ifndef UI_H #define UI_H #include #include #include #include #include #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