refactor: improve touch handling and UI element detection

This commit is contained in:
2026-02-20 00:36:29 -08:00
parent 6d51234f21
commit 9c8f67dccb
5 changed files with 153 additions and 40 deletions

View File

@@ -2,6 +2,7 @@
#include "IDisplayDriver.h"
#include "ScreenState.h"
#include "Style.h"
#include <cmath>
@@ -172,50 +173,54 @@ public:
/// Handle dashboard touch - returns action for tapped tile, or NONE
TileAction handleDashboardTouch(int x, int y) const {
if(!_drv || _gridCols <= 0)
return TileAction::NONE;
HitResult hr = hitTest(x, y);
if(hr.type == UIElementType::TILE && hr.index >= 0 && hr.index < DASHBOARD_TILE_COUNT) {
return DASHBOARD_TILES[hr.index].action;
}
return TileAction::NONE;
}
// Transform touch coordinates (handles rotated touch panels)
_drv->transformTouch(&x, &y);
/// Perform hit test at coordinates - returns element type, index, and bounds
HitResult hitTest(int x, int y) const {
if(!_drv)
return HitResult();
int tx = x, ty = y;
_drv->transformTouch(&tx, &ty);
int dispW = _drv->width();
int dispH = _drv->height();
int headerH = 30;
int headerH = _headerHeight;
// Check if in header area
if(y < headerH)
return TileAction::NONE;
// Calculate which tile was touched using grid
int cellW = dispW / _gridCols;
int cellH = (dispH - headerH) / _gridRows;
int col = x / cellW;
int row = (y - headerH) / cellH;
// Bounds check
if(col < 0 || col >= _gridCols || row < 0 || row >= _gridRows) {
return TileAction::NONE;
// Check header
Rect headerRect = UIElements::header(dispW, headerH);
if(headerRect.contains(tx, ty)) {
return HitResult(UIElementType::HEADER, 0, headerRect);
}
// Find which tile occupies this cell
// Check tiles
for(int i = 0; i < _tileCount; i++) {
const TileLayout& layout = _layouts[i];
if(layout.col <= col && col < layout.col + layout.cols && layout.row <= row
&& row < layout.row + layout.rows) {
return DASHBOARD_TILES[i].action;
const TileLayout& lay = _layouts[i];
Rect tileRect(lay.x, lay.y, lay.w, lay.h);
if(tileRect.contains(tx, ty)) {
return HitResult(UIElementType::TILE, i, tileRect);
}
}
return TileAction::NONE;
return HitResult();
}
/// Set header height for hit testing (call after calculateDashboardLayouts or manually)
void setHeaderHeight(int h) { _headerHeight = h; }
int getHeaderHeight() const { return _headerHeight; }
/// Calculate and store layouts for dashboard tiles
/// Called by drivers who want to use the layout helper
int calculateDashboardLayouts(int headerH = 30, int margin = 8) {
if(!_drv)
return 0;
_headerHeight = headerH;
_tileCount = TileLayoutHelper::calculateLayouts(
_drv->width(), _drv->height(), headerH, margin, _layouts, &_gridCols, &_gridRows);
return _tileCount;
@@ -232,4 +237,5 @@ private:
int _tileCount = 0;
int _gridCols = 0;
int _gridRows = 0;
int _headerHeight = 30;
};

View File

@@ -306,6 +306,20 @@ void DoorbellLogic::onSerialCommand(const String& cmd) {
_state.ipAddr.c_str());
} else if(cmd == "board") {
Serial.printf("[BOARD] %s\n", _board);
} else if(cmd.startsWith("hittest ")) {
String args = cmd.substring(8);
int comma = args.indexOf(',');
if(comma > 0) {
int x = args.substring(0, comma).toInt();
int y = args.substring(comma + 1).toInt();
HitResult hr = _display->hitTest(x, y);
Serial.printf("[Hittest] raw:(%d,%d) type:%d index:%d bounds:(%d,%d,%d,%d)\n", x, y,
(int)hr.type, hr.index, hr.bounds.x, hr.bounds.y, hr.bounds.w, hr.bounds.h);
Serial.printf("[Display] w:%d h:%d headerH:%d\n", _display->width(), _display->height(),
_display->getHeaderHeight());
} else {
Serial.println("[Hittest] Usage: hittest x,y");
}
} else
Serial.println(F("[CMD] alert|silence|reboot|dashboard|off|status|board"));
}

View File

@@ -144,3 +144,86 @@ struct TileMetrics {
return Layout(col * (tileW + gap), row * (tileH + gap), tileW, tileH);
}
};
struct Rect {
int16_t x, y;
int16_t w, h;
Rect()
: x(0)
, y(0)
, w(0)
, h(0) { }
Rect(int16_t x_, int16_t y_, int16_t w_, int16_t h_)
: x(x_)
, y(y_)
, w(w_)
, h(h_) { }
int16_t left() const { return x; }
int16_t top() const { return y; }
int16_t right() const { return x + w; }
int16_t bottom() const { return y + h; }
bool contains(int16_t px, int16_t py) const {
return px >= x && px < x + w && py >= y && py < y + h;
}
bool intersects(const Rect& other) const {
return !(
right() <= other.x || other.right() <= x || bottom() <= other.y || other.bottom() <= y);
}
Rect expanded(int16_t dx, int16_t dy) const {
return Rect(x - dx, y - dy, w + 2 * dx, h + 2 * dy);
}
Rect translated(int16_t dx, int16_t dy) const { return Rect(x + dx, y + dy, w, h); }
};
enum class UIElementType { NONE, HEADER, TILE, BUTTON, ICON };
struct HitResult {
UIElementType type;
int16_t index;
Rect bounds;
HitResult()
: type(UIElementType::NONE)
, index(-1)
, bounds() { }
HitResult(UIElementType t, int16_t i, const Rect& b)
: type(t)
, index(i)
, bounds(b) { }
bool isValid() const { return type != UIElementType::NONE; }
operator bool() const { return isValid(); }
};
class UIElements {
public:
static Rect header(uint16_t screenW, uint16_t headerH) { return Rect(0, 0, screenW, headerH); }
static Rect contentArea(
uint16_t screenW, uint16_t screenH, uint16_t headerH, uint16_t padding = 10) {
return Rect(
padding, headerH + padding, screenW - 2 * padding, screenH - headerH - 2 * padding);
}
static Rect tile(uint8_t col, uint8_t row, uint8_t cols, uint8_t rows, uint16_t contentX,
uint16_t contentY, uint16_t contentW, uint16_t contentH, uint8_t gap = 8) {
uint16_t tileW = (contentW - (cols - 1) * gap) / cols;
uint16_t tileH = (contentH - (rows - 1) * gap) / rows;
int16_t x = contentX + col * (tileW + gap);
int16_t y = contentY + row * (tileH + gap);
return Rect(x, y, tileW, tileH);
}
static Rect button(int16_t x, int16_t y, int16_t w, int16_t h, int16_t padding = 8) {
return Rect(x - padding, y - padding, w + 2 * padding, h + 2 * padding);
}
static Rect icon(int16_t x, int16_t y, int16_t size) { return Rect(x, y, size, size); }
};