diff --git a/boards/esp32-32e-4/DisplayDriverTFT.cpp b/boards/esp32-32e-4/DisplayDriverTFT.cpp index 9ad6b50..54f1573 100644 --- a/boards/esp32-32e-4/DisplayDriverTFT.cpp +++ b/boards/esp32-32e-4/DisplayDriverTFT.cpp @@ -90,7 +90,8 @@ void DisplayDriverTFT::begin() { _tft.setRotation(DISPLAY_ROTATION); _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(); // Debug: check if touch controller is responding @@ -153,16 +154,16 @@ void DisplayDriverTFT::drawBoot(const ScreenState& st) { _tft.setTextColor(TFT_WHITE, TFT_BLACK); setTitleFont(); - _tft.setCursor(10, 10); + _tft.setCursor(10, 28); // y=28 baseline accounts for ~18px font height above baseline _tft.print("KLUBHAUS"); setBodyFont(); - _tft.setCursor(10, 40); + _tft.setCursor(10, 55); // y adjusted for ~12px font _tft.print(BOARD_NAME); // Show boot stage status setLabelFont(); - _tft.setCursor(10, 70); + _tft.setCursor(10, 85); // y adjusted for ~9px label font switch(stage) { case BootStage::SPLASH: _tft.print("Initializing..."); @@ -194,30 +195,34 @@ void DisplayDriverTFT::drawAlert(const ScreenState& st) { _tft.setTextColor(TFT_WHITE, bg); setTitleFont(); - _tft.setCursor(10, 20); + _tft.setCursor(10, 28); // y=28 baseline for ~18px font _tft.print(st.alertTitle.length() > 0 ? st.alertTitle : "ALERT"); setBodyFont(); - _tft.setCursor(10, 80); + _tft.setCursor(10, 70); // y adjusted for ~12px body font _tft.print(st.alertBody); setLabelFont(); - _tft.setCursor(10, DISPLAY_HEIGHT - 20); + _tft.setCursor(10, _tft.height() - 10); // y adjusted for ~9px label font _tft.print("Hold to silence..."); } void DisplayDriverTFT::drawDashboard(const ScreenState& st) { _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 - _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.setTextColor(TFT_WHITE); _tft.setCursor(5, 20); // y=28 is baseline, text sits above this _tft.print("KLUBHAUS"); - // WiFi indicator - _tft.setCursor(DISPLAY_WIDTH - 60, 20); + // WiFi indicator - right aligned in header + _tft.setCursor(dispW - 50, 20); _tft.print(st.wifiSsid.length() > 0 ? "WiFi:ON" : "WiFi:OFF"); // Get tile layouts from library helper @@ -336,13 +341,15 @@ HoldState DisplayDriverTFT::updateHold(unsigned long holdMs) { h.completed = (held >= holdMs); // Simple progress bar at bottom of screen - int barW = (int)(DISPLAY_WIDTH * h.progress); - _tft.fillRect(0, DISPLAY_HEIGHT - 8, barW, 8, TFT_WHITE); - _tft.fillRect(barW, DISPLAY_HEIGHT - 8, DISPLAY_WIDTH - barW, 8, TFT_DARKGREY); + int dispW = _tft.width(); + int dispH = _tft.height(); + 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 { if(_holdActive) { // 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; } diff --git a/boards/esp32-32e-4/DisplayDriverTFT.h b/boards/esp32-32e-4/DisplayDriverTFT.h index bfaa807..e0c3d6e 100644 --- a/boards/esp32-32e-4/DisplayDriverTFT.h +++ b/boards/esp32-32e-4/DisplayDriverTFT.h @@ -12,7 +12,10 @@ public: void render(const ScreenState& state) override; TouchEvent readTouch() 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(); } // Dashboard - uses transform for touch coordinate correction diff --git a/libraries/KlubhausCore/src/DisplayManager.h b/libraries/KlubhausCore/src/DisplayManager.h index 93e12c1..aa254a7 100644 --- a/libraries/KlubhausCore/src/DisplayManager.h +++ b/libraries/KlubhausCore/src/DisplayManager.h @@ -2,6 +2,7 @@ #include "IDisplayDriver.h" #include "ScreenState.h" +#include "Style.h" #include @@ -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; }; diff --git a/libraries/KlubhausCore/src/DoorbellLogic.cpp b/libraries/KlubhausCore/src/DoorbellLogic.cpp index a8ad3c9..3e1cb5e 100644 --- a/libraries/KlubhausCore/src/DoorbellLogic.cpp +++ b/libraries/KlubhausCore/src/DoorbellLogic.cpp @@ -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")); } diff --git a/libraries/KlubhausCore/src/Style.h b/libraries/KlubhausCore/src/Style.h index 0a05544..9d67159 100644 --- a/libraries/KlubhausCore/src/Style.h +++ b/libraries/KlubhausCore/src/Style.h @@ -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); } +};