refactor: improve touch handling and UI element detection
This commit is contained in:
@@ -90,7 +90,8 @@ void DisplayDriverTFT::begin() {
|
|||||||
_tft.setRotation(DISPLAY_ROTATION);
|
_tft.setRotation(DISPLAY_ROTATION);
|
||||||
_tft.fillScreen(TFT_BLACK);
|
_tft.fillScreen(TFT_BLACK);
|
||||||
|
|
||||||
Serial.printf("[GFX] Display OK: %dx%d\n", DISPLAY_WIDTH, DISPLAY_HEIGHT);
|
Serial.printf("[GFX] Display OK: const %dx%d, tft %dx%d\n", DISPLAY_WIDTH, DISPLAY_HEIGHT,
|
||||||
|
_tft.width(), _tft.height());
|
||||||
Serial.flush();
|
Serial.flush();
|
||||||
|
|
||||||
// Debug: check if touch controller is responding
|
// Debug: check if touch controller is responding
|
||||||
@@ -153,16 +154,16 @@ void DisplayDriverTFT::drawBoot(const ScreenState& st) {
|
|||||||
_tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
_tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||||
|
|
||||||
setTitleFont();
|
setTitleFont();
|
||||||
_tft.setCursor(10, 10);
|
_tft.setCursor(10, 28); // y=28 baseline accounts for ~18px font height above baseline
|
||||||
_tft.print("KLUBHAUS");
|
_tft.print("KLUBHAUS");
|
||||||
|
|
||||||
setBodyFont();
|
setBodyFont();
|
||||||
_tft.setCursor(10, 40);
|
_tft.setCursor(10, 55); // y adjusted for ~12px font
|
||||||
_tft.print(BOARD_NAME);
|
_tft.print(BOARD_NAME);
|
||||||
|
|
||||||
// Show boot stage status
|
// Show boot stage status
|
||||||
setLabelFont();
|
setLabelFont();
|
||||||
_tft.setCursor(10, 70);
|
_tft.setCursor(10, 85); // y adjusted for ~9px label font
|
||||||
switch(stage) {
|
switch(stage) {
|
||||||
case BootStage::SPLASH:
|
case BootStage::SPLASH:
|
||||||
_tft.print("Initializing...");
|
_tft.print("Initializing...");
|
||||||
@@ -194,30 +195,34 @@ void DisplayDriverTFT::drawAlert(const ScreenState& st) {
|
|||||||
_tft.setTextColor(TFT_WHITE, bg);
|
_tft.setTextColor(TFT_WHITE, bg);
|
||||||
|
|
||||||
setTitleFont();
|
setTitleFont();
|
||||||
_tft.setCursor(10, 20);
|
_tft.setCursor(10, 28); // y=28 baseline for ~18px font
|
||||||
_tft.print(st.alertTitle.length() > 0 ? st.alertTitle : "ALERT");
|
_tft.print(st.alertTitle.length() > 0 ? st.alertTitle : "ALERT");
|
||||||
|
|
||||||
setBodyFont();
|
setBodyFont();
|
||||||
_tft.setCursor(10, 80);
|
_tft.setCursor(10, 70); // y adjusted for ~12px body font
|
||||||
_tft.print(st.alertBody);
|
_tft.print(st.alertBody);
|
||||||
|
|
||||||
setLabelFont();
|
setLabelFont();
|
||||||
_tft.setCursor(10, DISPLAY_HEIGHT - 20);
|
_tft.setCursor(10, _tft.height() - 10); // y adjusted for ~9px label font
|
||||||
_tft.print("Hold to silence...");
|
_tft.print("Hold to silence...");
|
||||||
}
|
}
|
||||||
|
|
||||||
void DisplayDriverTFT::drawDashboard(const ScreenState& st) {
|
void DisplayDriverTFT::drawDashboard(const ScreenState& st) {
|
||||||
_tft.fillScreen(TFT_BLACK);
|
_tft.fillScreen(TFT_BLACK);
|
||||||
|
|
||||||
|
// Use actual display dimensions (after rotation)
|
||||||
|
int dispW = _tft.width();
|
||||||
|
int dispH = _tft.height();
|
||||||
|
|
||||||
// Header - using standard bitmap font for reliable positioning
|
// Header - using standard bitmap font for reliable positioning
|
||||||
_tft.fillRect(0, 0, DISPLAY_WIDTH, 30, 0x1A1A); // Dark gray header
|
_tft.fillRect(0, 0, dispW, STYLE_HEADER_HEIGHT, 0x1A1A); // Dark gray header
|
||||||
_tft.setTextSize(1);
|
_tft.setTextSize(1);
|
||||||
_tft.setTextColor(TFT_WHITE);
|
_tft.setTextColor(TFT_WHITE);
|
||||||
_tft.setCursor(5, 20); // y=28 is baseline, text sits above this
|
_tft.setCursor(5, 20); // y=28 is baseline, text sits above this
|
||||||
_tft.print("KLUBHAUS");
|
_tft.print("KLUBHAUS");
|
||||||
|
|
||||||
// WiFi indicator
|
// WiFi indicator - right aligned in header
|
||||||
_tft.setCursor(DISPLAY_WIDTH - 60, 20);
|
_tft.setCursor(dispW - 50, 20);
|
||||||
_tft.print(st.wifiSsid.length() > 0 ? "WiFi:ON" : "WiFi:OFF");
|
_tft.print(st.wifiSsid.length() > 0 ? "WiFi:ON" : "WiFi:OFF");
|
||||||
|
|
||||||
// Get tile layouts from library helper
|
// Get tile layouts from library helper
|
||||||
@@ -336,13 +341,15 @@ HoldState DisplayDriverTFT::updateHold(unsigned long holdMs) {
|
|||||||
h.completed = (held >= holdMs);
|
h.completed = (held >= holdMs);
|
||||||
|
|
||||||
// Simple progress bar at bottom of screen
|
// Simple progress bar at bottom of screen
|
||||||
int barW = (int)(DISPLAY_WIDTH * h.progress);
|
int dispW = _tft.width();
|
||||||
_tft.fillRect(0, DISPLAY_HEIGHT - 8, barW, 8, TFT_WHITE);
|
int dispH = _tft.height();
|
||||||
_tft.fillRect(barW, DISPLAY_HEIGHT - 8, DISPLAY_WIDTH - barW, 8, TFT_DARKGREY);
|
int barW = (int)(dispW * h.progress);
|
||||||
|
_tft.fillRect(0, dispH - 8, barW, 8, TFT_WHITE);
|
||||||
|
_tft.fillRect(barW, dispH - 8, dispW - barW, 8, TFT_DARKGREY);
|
||||||
} else {
|
} else {
|
||||||
if(_holdActive) {
|
if(_holdActive) {
|
||||||
// Clear the progress bar when released
|
// Clear the progress bar when released
|
||||||
_tft.fillRect(0, DISPLAY_HEIGHT - 8, DISPLAY_WIDTH, 8, TFT_DARKGREY);
|
_tft.fillRect(0, _tft.height() - 8, _tft.width(), 8, TFT_DARKGREY);
|
||||||
}
|
}
|
||||||
_holdActive = false;
|
_holdActive = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ public:
|
|||||||
void render(const ScreenState& state) override;
|
void render(const ScreenState& state) override;
|
||||||
TouchEvent readTouch() override;
|
TouchEvent readTouch() override;
|
||||||
HoldState updateHold(unsigned long holdMs) override;
|
HoldState updateHold(unsigned long holdMs) override;
|
||||||
int width() override { return _tft.width(); }
|
int width() override {
|
||||||
|
// Use TFT_eSPI's dimensions after rotation - it's more reliable
|
||||||
|
return _tft.width();
|
||||||
|
}
|
||||||
int height() override { return _tft.height(); }
|
int height() override { return _tft.height(); }
|
||||||
|
|
||||||
// Dashboard - uses transform for touch coordinate correction
|
// Dashboard - uses transform for touch coordinate correction
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "IDisplayDriver.h"
|
#include "IDisplayDriver.h"
|
||||||
#include "ScreenState.h"
|
#include "ScreenState.h"
|
||||||
|
#include "Style.h"
|
||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
@@ -172,50 +173,54 @@ public:
|
|||||||
|
|
||||||
/// Handle dashboard touch - returns action for tapped tile, or NONE
|
/// Handle dashboard touch - returns action for tapped tile, or NONE
|
||||||
TileAction handleDashboardTouch(int x, int y) const {
|
TileAction handleDashboardTouch(int x, int y) const {
|
||||||
if(!_drv || _gridCols <= 0)
|
HitResult hr = hitTest(x, y);
|
||||||
return TileAction::NONE;
|
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)
|
/// Perform hit test at coordinates - returns element type, index, and bounds
|
||||||
_drv->transformTouch(&x, &y);
|
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 dispW = _drv->width();
|
||||||
int dispH = _drv->height();
|
int dispH = _drv->height();
|
||||||
int headerH = 30;
|
int headerH = _headerHeight;
|
||||||
|
|
||||||
// Check if in header area
|
// Check header
|
||||||
if(y < headerH)
|
Rect headerRect = UIElements::header(dispW, headerH);
|
||||||
return TileAction::NONE;
|
if(headerRect.contains(tx, ty)) {
|
||||||
|
return HitResult(UIElementType::HEADER, 0, headerRect);
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find which tile occupies this cell
|
// Check tiles
|
||||||
for(int i = 0; i < _tileCount; i++) {
|
for(int i = 0; i < _tileCount; i++) {
|
||||||
const TileLayout& layout = _layouts[i];
|
const TileLayout& lay = _layouts[i];
|
||||||
if(layout.col <= col && col < layout.col + layout.cols && layout.row <= row
|
Rect tileRect(lay.x, lay.y, lay.w, lay.h);
|
||||||
&& row < layout.row + layout.rows) {
|
if(tileRect.contains(tx, ty)) {
|
||||||
return DASHBOARD_TILES[i].action;
|
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
|
/// Calculate and store layouts for dashboard tiles
|
||||||
/// Called by drivers who want to use the layout helper
|
/// Called by drivers who want to use the layout helper
|
||||||
int calculateDashboardLayouts(int headerH = 30, int margin = 8) {
|
int calculateDashboardLayouts(int headerH = 30, int margin = 8) {
|
||||||
if(!_drv)
|
if(!_drv)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
|
_headerHeight = headerH;
|
||||||
_tileCount = TileLayoutHelper::calculateLayouts(
|
_tileCount = TileLayoutHelper::calculateLayouts(
|
||||||
_drv->width(), _drv->height(), headerH, margin, _layouts, &_gridCols, &_gridRows);
|
_drv->width(), _drv->height(), headerH, margin, _layouts, &_gridCols, &_gridRows);
|
||||||
return _tileCount;
|
return _tileCount;
|
||||||
@@ -232,4 +237,5 @@ private:
|
|||||||
int _tileCount = 0;
|
int _tileCount = 0;
|
||||||
int _gridCols = 0;
|
int _gridCols = 0;
|
||||||
int _gridRows = 0;
|
int _gridRows = 0;
|
||||||
|
int _headerHeight = 30;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -306,6 +306,20 @@ void DoorbellLogic::onSerialCommand(const String& cmd) {
|
|||||||
_state.ipAddr.c_str());
|
_state.ipAddr.c_str());
|
||||||
} else if(cmd == "board") {
|
} else if(cmd == "board") {
|
||||||
Serial.printf("[BOARD] %s\n", _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
|
} else
|
||||||
Serial.println(F("[CMD] alert|silence|reboot|dashboard|off|status|board"));
|
Serial.println(F("[CMD] alert|silence|reboot|dashboard|off|status|board"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,3 +144,86 @@ struct TileMetrics {
|
|||||||
return Layout(col * (tileW + gap), row * (tileH + gap), tileW, tileH);
|
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); }
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user