refactor(display): extract tile layout logic to library helper class
This commit is contained in:
@@ -2,6 +2,117 @@
|
||||
|
||||
#include "IDisplayDriver.h"
|
||||
#include "ScreenState.h"
|
||||
#include <cmath>
|
||||
|
||||
/// Layout helper for dashboard tiles - computes positions based on constraints
|
||||
class TileLayoutHelper {
|
||||
public:
|
||||
/// Calculate tile layouts for the given display dimensions
|
||||
/// Returns array of TileLayout (must have capacity for DASHBOARD_TILE_COUNT)
|
||||
/// Returns the number of columns and rows used
|
||||
static int calculateLayouts(
|
||||
int displayW,
|
||||
int displayH,
|
||||
int headerH,
|
||||
int margin,
|
||||
TileLayout* outLayouts,
|
||||
int* outCols,
|
||||
int* outRows
|
||||
) {
|
||||
int contentH = displayH - headerH;
|
||||
int tileCount = DASHBOARD_TILE_COUNT;
|
||||
|
||||
// Determine base grid based on display aspect ratio
|
||||
int cols, rows;
|
||||
calculateGrid(tileCount, displayW, contentH, &cols, &rows);
|
||||
|
||||
// Calculate base cell sizes
|
||||
int cellW = displayW / cols;
|
||||
int cellH = contentH / rows;
|
||||
|
||||
// Simple first-fit: place tiles in order, respecting min sizes
|
||||
// For more complex layouts, tiles could specify preferred positions
|
||||
for(int i = 0; i < tileCount; i++) {
|
||||
const DashboardTile& tile = DASHBOARD_TILES[i];
|
||||
int tileCols = tile.constraint.minCols;
|
||||
int tileRows = tile.constraint.minRows;
|
||||
|
||||
// Find next available position
|
||||
int col = 0, row = 0;
|
||||
findNextPosition(outLayouts, i, cols, rows, &col, &row);
|
||||
|
||||
// Ensure tile fits within grid
|
||||
if(col + tileCols > cols) tileCols = cols - col;
|
||||
if(row + tileRows > rows) tileRows = rows - row;
|
||||
|
||||
// Calculate pixel position
|
||||
int x = col * cellW + margin;
|
||||
int y = headerH + row * cellH + margin;
|
||||
int w = tileCols * cellW - 2 * margin;
|
||||
int h = tileRows * cellH - 2 * margin;
|
||||
|
||||
outLayouts[i] = {x, y, w, h, col, row, tileCols, tileRows};
|
||||
}
|
||||
|
||||
*outCols = cols;
|
||||
*outRows = rows;
|
||||
return tileCount;
|
||||
}
|
||||
|
||||
private:
|
||||
/// Calculate optimal grid dimensions based on display and tile constraints
|
||||
static void calculateGrid(int tileCount, int displayW, int contentH, int* outCols, int* outRows) {
|
||||
// Calculate aspect ratio to determine preferred layout
|
||||
float aspectRatio = (float)displayW / contentH;
|
||||
|
||||
// Start with simple square-ish grid
|
||||
int cols = (int)std::sqrt(tileCount * aspectRatio);
|
||||
if(cols < 1) cols = 1;
|
||||
if(cols > tileCount) cols = tileCount;
|
||||
|
||||
int rows = (tileCount + cols - 1) / cols;
|
||||
|
||||
// For wide displays (landscape), prefer more columns
|
||||
if(aspectRatio > 1.5f && tileCount <= 6) {
|
||||
cols = tileCount;
|
||||
rows = 1;
|
||||
}
|
||||
// For tall displays (portrait), prefer more rows
|
||||
else if(aspectRatio < 0.8f && tileCount <= 6) {
|
||||
rows = tileCount;
|
||||
cols = 1;
|
||||
}
|
||||
|
||||
*outCols = cols;
|
||||
*outRows = rows;
|
||||
}
|
||||
|
||||
/// Find next available grid position
|
||||
static void findNextPosition(const TileLayout* layouts, int count, int gridCols, int gridRows, int* outCol, int* outRow) {
|
||||
// Simple: find first empty cell
|
||||
// Could be enhanced to pack tightly based on tile sizes
|
||||
for(int r = 0; r < gridRows; r++) {
|
||||
for(int c = 0; c < gridCols; c++) {
|
||||
bool occupied = false;
|
||||
for(int i = 0; i < count; i++) {
|
||||
if(layouts[i].col <= c && c < layouts[i].col + layouts[i].cols &&
|
||||
layouts[i].row <= r && r < layouts[i].row + layouts[i].rows) {
|
||||
occupied = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!occupied) {
|
||||
*outCol = c;
|
||||
*outRow = r;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fallback: just use count position
|
||||
*outCol = count % gridCols;
|
||||
*outRow = count / gridCols;
|
||||
}
|
||||
};
|
||||
|
||||
class DisplayManager {
|
||||
public:
|
||||
@@ -19,8 +130,8 @@ public:
|
||||
}
|
||||
|
||||
void render(const ScreenState& st) {
|
||||
if(_drv)
|
||||
_drv->render(st);
|
||||
if(!_drv) return;
|
||||
_drv->render(st);
|
||||
}
|
||||
|
||||
TouchEvent readTouch() { return _drv ? _drv->readTouch() : TouchEvent {}; }
|
||||
@@ -35,75 +146,6 @@ public:
|
||||
int width() { return _drv ? _drv->width() : 0; }
|
||||
int height() { return _drv ? _drv->height() : 0; }
|
||||
|
||||
/// Auto-calculate grid dimensions for dashboard tiles
|
||||
/// Prefers landscape (more columns than rows) for wide displays
|
||||
void getTileGrid(int tileCount, int* outRows, int* outCols) const {
|
||||
if(tileCount <= 0) {
|
||||
*outRows = *outCols = 0;
|
||||
return;
|
||||
}
|
||||
// Calculate cols: try to make it wider than tall
|
||||
int cols = (int)sqrt(tileCount);
|
||||
if(cols * cols < tileCount) cols++;
|
||||
|
||||
// Make cols >= rows for landscape preference
|
||||
while(cols * ((tileCount + cols - 1) / cols) < tileCount) {
|
||||
cols++;
|
||||
}
|
||||
|
||||
int rows = (tileCount + cols - 1) / cols;
|
||||
|
||||
// If display is wider, prefer more columns
|
||||
if(_drv) {
|
||||
int w = _drv->width();
|
||||
int h = _drv->height();
|
||||
if(w > h) {
|
||||
// Landscape display - maximize columns
|
||||
cols = tileCount;
|
||||
rows = 1;
|
||||
}
|
||||
}
|
||||
|
||||
*outRows = rows;
|
||||
*outCols = cols;
|
||||
}
|
||||
|
||||
/// Render all dashboard tiles with auto-calculated grid
|
||||
void renderDashboard(const ScreenState& st) {
|
||||
if(!_drv) return;
|
||||
|
||||
int rows, cols;
|
||||
getTileGrid(DASHBOARD_TILE_COUNT, &rows, &cols);
|
||||
|
||||
int dispW = _drv->width();
|
||||
int dispH = _drv->height();
|
||||
|
||||
// Reserve space for header
|
||||
int headerH = 30;
|
||||
int contentH = dispH - headerH;
|
||||
|
||||
// Calculate tile sizes
|
||||
int tileW = dispW / cols;
|
||||
int tileH = contentH / rows;
|
||||
int margin = 8;
|
||||
|
||||
for(int i = 0; i < DASHBOARD_TILE_COUNT; i++) {
|
||||
int row = i / cols;
|
||||
int col = i % cols;
|
||||
|
||||
int x = col * tileW + margin;
|
||||
int y = headerH + row * tileH + margin;
|
||||
int w = tileW - 2 * margin;
|
||||
int h = tileH - 2 * margin;
|
||||
|
||||
_drv->drawTileAt(x, y, w, h, DASHBOARD_TILES[i].label, DASHBOARD_TILES[i].bgColor);
|
||||
}
|
||||
|
||||
// Store grid for touch calculations
|
||||
_gridRows = rows;
|
||||
_gridCols = cols;
|
||||
}
|
||||
|
||||
/// Handle dashboard touch - returns action for tapped tile, or NONE
|
||||
TileAction handleDashboardTouch(int x, int y) const {
|
||||
if(!_drv || _gridCols <= 0) return TileAction::NONE;
|
||||
@@ -118,29 +160,56 @@ public:
|
||||
// Check if in header area
|
||||
if(y < headerH) return TileAction::NONE;
|
||||
|
||||
// Calculate which tile was touched
|
||||
int tileW = dispW / _gridCols;
|
||||
int contentH = dispH - headerH;
|
||||
int tileH = contentH / _gridRows;
|
||||
// Calculate which tile was touched using grid
|
||||
int cellW = dispW / _gridCols;
|
||||
int cellH = (dispH - headerH) / _gridRows;
|
||||
|
||||
int col = x / tileW;
|
||||
int row = (y - headerH) / tileH;
|
||||
int col = x / cellW;
|
||||
int row = (y - headerH) / cellH;
|
||||
|
||||
// Bounds check
|
||||
if(col < 0 || col >= _gridCols || row < 0 || row >= _gridRows) {
|
||||
return TileAction::NONE;
|
||||
}
|
||||
|
||||
int index = row * _gridCols + col;
|
||||
if(index < 0 || index >= DASHBOARD_TILE_COUNT) {
|
||||
return TileAction::NONE;
|
||||
// Find which tile occupies this cell
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return DASHBOARD_TILES[index].action;
|
||||
return TileAction::NONE;
|
||||
}
|
||||
|
||||
/// 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;
|
||||
|
||||
_tileCount = TileLayoutHelper::calculateLayouts(
|
||||
_drv->width(),
|
||||
_drv->height(),
|
||||
headerH,
|
||||
margin,
|
||||
_layouts,
|
||||
&_gridCols,
|
||||
&_gridRows
|
||||
);
|
||||
return _tileCount;
|
||||
}
|
||||
|
||||
/// Get calculated layout for a specific tile
|
||||
const TileLayout* getTileLayouts() const { return _layouts; }
|
||||
int getGridCols() const { return _gridCols; }
|
||||
int getGridRows() const { return _gridRows; }
|
||||
|
||||
private:
|
||||
IDisplayDriver* _drv;
|
||||
mutable int _gridRows = 0;
|
||||
mutable int _gridCols = 0;
|
||||
TileLayout _layouts[DASHBOARD_TILE_COUNT];
|
||||
int _tileCount = 0;
|
||||
int _gridCols = 0;
|
||||
int _gridRows = 0;
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@ struct HoldState {
|
||||
};
|
||||
|
||||
/// Abstract display driver — implemented per-board.
|
||||
/// Drivers own all rendering. Library provides tile layout helper via TileLayoutHelper.
|
||||
class IDisplayDriver {
|
||||
public:
|
||||
virtual ~IDisplayDriver() = default;
|
||||
@@ -27,17 +28,11 @@ public:
|
||||
|
||||
// ── Touch ──
|
||||
virtual TouchEvent readTouch() = 0;
|
||||
/// Track a long-press gesture; returns progress/completion.
|
||||
virtual HoldState updateHold(unsigned long holdMs) = 0;
|
||||
/// Idle hint animation (e.g. pulsing ring) while alert is showing.
|
||||
/// @param active If true, show "holding" animation; if false, show "idle" animation.
|
||||
virtual void updateHint(int x, int y, bool active) = 0;
|
||||
virtual int width() = 0;
|
||||
virtual int height() = 0;
|
||||
|
||||
// ── Dashboard tiles ──
|
||||
/// Transform raw touch coordinates (for rotated touch panels)
|
||||
// ── Touch transform (for rotated panels) ──
|
||||
virtual void transformTouch(int* x, int* y) { /* default: no transform */ }
|
||||
/// Draw a tile at specified position (library handles grid math)
|
||||
virtual void drawTileAt(int x, int y, int w, int h, const char* label, uint16_t bgColor) = 0;
|
||||
};
|
||||
|
||||
@@ -16,19 +16,35 @@ enum class TileAction {
|
||||
REBOOT, // Reboot device
|
||||
};
|
||||
|
||||
/// Dashboard tile layout constraints for flexible grid
|
||||
struct TileConstraint {
|
||||
uint8_t minCols = 1; // minimum columns this tile needs
|
||||
uint8_t minRows = 1; // minimum rows this tile needs
|
||||
uint8_t weight = 1; // priority for growing/shrinking (higher = more flexible)
|
||||
};
|
||||
|
||||
/// Computed tile position in the grid
|
||||
struct TileLayout {
|
||||
int x, y; // pixel position
|
||||
int w, h; // pixel size
|
||||
int col, row; // grid position
|
||||
int cols, rows; // grid span
|
||||
};
|
||||
|
||||
/// Dashboard tile definitions — shared across all boards
|
||||
struct DashboardTile {
|
||||
const char* label;
|
||||
uint16_t bgColor; // RGB565
|
||||
TileAction action;
|
||||
TileConstraint constraint;
|
||||
};
|
||||
|
||||
/// Standard dashboard tiles (auto-gridded based on count)
|
||||
/// Standard dashboard tiles (auto-gridded based on count and constraints)
|
||||
static constexpr DashboardTile DASHBOARD_TILES[] = {
|
||||
{ "Alert", 0x0280, TileAction::ALERT },
|
||||
{ "Silent", 0x0400, TileAction::SILENCE },
|
||||
{ "Status", 0x0440, TileAction::STATUS },
|
||||
{ "Reboot", 0x0100, TileAction::REBOOT },
|
||||
{ "Alert", 0x0280, TileAction::ALERT, {1, 1, 1} },
|
||||
{ "Silent", 0x0400, TileAction::SILENCE, {1, 1, 1} },
|
||||
{ "Status", 0x0440, TileAction::STATUS, {1, 1, 1} },
|
||||
{ "Reboot", 0x0100, TileAction::REBOOT, {1, 1, 1} },
|
||||
};
|
||||
static constexpr int DASHBOARD_TILE_COUNT = sizeof(DASHBOARD_TILES) / sizeof(DASHBOARD_TILES[0]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user