Add touch-to-logical coordinate mapping for UI
Implements tapToLogical() to convert normalized touch coordinates to orientation-aware logical screen coordinates. Adds TouchRegistry hit testing for interactive UI elements and header back button. Touch gestures now work correctly across all screen orientations (Portrait, Landscape, etc.) by inverting the rotateCoordinates transform.
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
#include "TouchRegistry.h"
|
||||
|
||||
TouchRegistry& TouchRegistry::getInstance() {
|
||||
static TouchRegistry instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
void TouchRegistry::beginFrame() {
|
||||
if (!enabled_) return;
|
||||
counts_[backIndex()] = 0;
|
||||
}
|
||||
|
||||
void TouchRegistry::add(const Rect& rect, int id, Kind kind) {
|
||||
if (!enabled_) return;
|
||||
const uint8_t b = backIndex();
|
||||
size_t& n = counts_[b];
|
||||
if (n >= CAPACITY) return; // silently drop overflow; CAPACITY sized for a screen
|
||||
buffers_[b][n] = Target{rect, static_cast<int16_t>(id), static_cast<uint8_t>(kind)};
|
||||
++n;
|
||||
}
|
||||
|
||||
void TouchRegistry::publish() {
|
||||
if (!enabled_) return;
|
||||
// Flip live to the buffer we just filled. Release so the reader sees the writes.
|
||||
live_.store(backIndex(), std::memory_order_release);
|
||||
}
|
||||
|
||||
bool TouchRegistry::hitTest(int x, int y, Kind kind, int& outId) const {
|
||||
if (!enabled_) return false;
|
||||
const uint8_t b = live_.load(std::memory_order_acquire);
|
||||
const size_t n = counts_[b];
|
||||
const auto& buf = buffers_[b];
|
||||
// Last-registered wins (topmost / most recently drawn).
|
||||
for (size_t i = n; i-- > 0;) {
|
||||
if (buf[i].kind == kind && buf[i].rect.contains(x, y)) {
|
||||
outId = buf[i].id;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "components/themes/BaseTheme.h" // Rect
|
||||
|
||||
// Frame-scoped registry of tappable UI elements. Theme draw methods record each
|
||||
// interactive element's LOGICAL rect + id during render() (on the render task);
|
||||
// the input layer hit-tests a tap against it during the next loop() (main task).
|
||||
//
|
||||
// Lock-free single-writer (render) / single-reader (loop) via double buffering:
|
||||
// render writes the back buffer and publish() atomically flips it to live, so the
|
||||
// reader never observes a half-built frame. No per-frame heap allocation.
|
||||
//
|
||||
// Runtime-gated: disabled on boards without touch (setEnabled(gpio.hasTouch())),
|
||||
// so add()/hitTest() are a single branch on the C3.
|
||||
class TouchRegistry {
|
||||
public:
|
||||
enum Kind : uint8_t { Item = 0, Back = 1 };
|
||||
|
||||
static TouchRegistry& getInstance();
|
||||
|
||||
void setEnabled(bool enabled) { enabled_ = enabled; }
|
||||
bool isEnabled() const { return enabled_; }
|
||||
|
||||
// Render task: clear the back buffer, append targets, then publish.
|
||||
void beginFrame();
|
||||
void add(const Rect& rect, int id, Kind kind);
|
||||
void publish();
|
||||
|
||||
// Main/loop task: find the topmost target of `kind` containing the logical point.
|
||||
// Returns true and writes its id to outId on a hit.
|
||||
bool hitTest(int x, int y, Kind kind, int& outId) const;
|
||||
|
||||
private:
|
||||
static constexpr size_t CAPACITY = 48; // worst case excl. keyboard grid (Phase 2)
|
||||
|
||||
struct Target {
|
||||
Rect rect;
|
||||
int16_t id;
|
||||
uint8_t kind;
|
||||
};
|
||||
|
||||
uint8_t backIndex() const { return live_.load(std::memory_order_relaxed) ^ 1u; }
|
||||
|
||||
std::array<std::array<Target, CAPACITY>, 2> buffers_{};
|
||||
std::array<size_t, 2> counts_{0, 0};
|
||||
std::atomic<uint8_t> live_{0};
|
||||
bool enabled_ = false;
|
||||
};
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
#include "I18n.h"
|
||||
#include "RecentBooksStore.h"
|
||||
#include "components/TouchRegistry.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
@@ -303,6 +304,7 @@ void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
|
||||
const auto pageStartIndex = selectedIndex / pageItems * pageItems;
|
||||
for (int i = pageStartIndex; i < itemCount && i < pageStartIndex + pageItems; i++) {
|
||||
const int itemY = rect.y + (i % pageItems) * rowHeight;
|
||||
TouchRegistry::getInstance().add(Rect{rect.x, itemY - 2, rect.width, rowHeight}, i, TouchRegistry::Item);
|
||||
|
||||
int rowTextWidth = contentWidth - BaseMetrics::values.contentSidePadding * 2;
|
||||
std::string valueText;
|
||||
@@ -353,6 +355,11 @@ void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
|
||||
}
|
||||
|
||||
void BaseTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* title, const char* subtitle) const {
|
||||
// Left strip of the header is a tap-to-go-back zone (orientation-mapped; the
|
||||
// title is centered so this area is normally empty). Mirrors the legacy corner
|
||||
// gesture but as a real, theme-positioned target.
|
||||
TouchRegistry::getInstance().add(Rect{rect.x, rect.y, 64, rect.height + 8}, -1, TouchRegistry::Back);
|
||||
|
||||
// Hide last battery draw
|
||||
constexpr int maxBatteryWidth = 80;
|
||||
renderer.fillRect(rect.x + rect.width - maxBatteryWidth, rect.y + 5, maxBatteryWidth,
|
||||
@@ -669,6 +676,10 @@ void BaseTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount
|
||||
for (int i = 0; i < buttonCount; ++i) {
|
||||
const int tileY = BaseMetrics::values.verticalSpacing + rect.y +
|
||||
static_cast<int>(i) * (BaseMetrics::values.menuRowHeight + BaseMetrics::values.menuSpacing);
|
||||
TouchRegistry::getInstance().add(
|
||||
Rect{rect.x + BaseMetrics::values.contentSidePadding, tileY,
|
||||
rect.width - BaseMetrics::values.contentSidePadding * 2, BaseMetrics::values.menuRowHeight},
|
||||
i, TouchRegistry::Item);
|
||||
|
||||
const bool selected = selectedIndex == i;
|
||||
|
||||
|
||||
@@ -16,6 +16,9 @@ struct Rect {
|
||||
int height;
|
||||
|
||||
explicit Rect(int x = 0, int y = 0, int width = 0, int height = 0) : x(x), y(y), width(width), height(height) {}
|
||||
|
||||
// Logical-coordinate point hit-test (used for touch target hit-testing).
|
||||
bool contains(int px, int py) const { return px >= x && px < x + width && py >= y && py < y + height; }
|
||||
};
|
||||
|
||||
struct TabInfo {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "RecentBooksStore.h"
|
||||
#include "components/TouchRegistry.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "components/icons/book.h"
|
||||
#include "components/icons/book24.h"
|
||||
@@ -106,6 +107,7 @@ void LyraTheme::fillBatteryIcon(const GfxRenderer& renderer, Rect rect, uint16_t
|
||||
|
||||
void LyraTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* title, const char* subtitle) const {
|
||||
renderer.fillRect(rect.x, rect.y, rect.width, rect.height, false);
|
||||
TouchRegistry::getInstance().add(Rect{rect.x, rect.y, 64, rect.height + 8}, -1, TouchRegistry::Back);
|
||||
|
||||
const bool showBatteryPercentage =
|
||||
SETTINGS.hideBatteryPercentage != CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_ALWAYS;
|
||||
@@ -258,6 +260,7 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
|
||||
int iconY = (rowSubtitle != nullptr) ? 16 : 10;
|
||||
for (int i = pageStartIndex; i < itemCount && i < pageStartIndex + pageItems; i++) {
|
||||
const int itemY = rect.y + (i % pageItems) * rowHeight;
|
||||
TouchRegistry::getInstance().add(Rect{rect.x, itemY, rect.width, rowHeight}, i, TouchRegistry::Item);
|
||||
int rowTextWidth = textWidth;
|
||||
|
||||
// Draw name
|
||||
@@ -517,6 +520,7 @@ void LyraTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount
|
||||
Rect tileRect = Rect{rect.x + LyraMetrics::values.contentSidePadding,
|
||||
rect.y + i * (LyraMetrics::values.menuRowHeight + LyraMetrics::values.menuSpacing), tileWidth,
|
||||
LyraMetrics::values.menuRowHeight};
|
||||
TouchRegistry::getInstance().add(tileRect, i, TouchRegistry::Item);
|
||||
|
||||
const bool selected = selectedIndex == i;
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "RecentBooksStore.h"
|
||||
#include "components/TouchRegistry.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "components/icons/cover.h"
|
||||
#include "fontIds.h"
|
||||
@@ -216,6 +217,7 @@ void RoundedRaffTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int butt
|
||||
renderer.truncatedText(kTitleFontId, label.c_str(), maxLabelWidth, EpdFontFamily::BOLD);
|
||||
const int rowWidth = std::min(
|
||||
menuMaxWidth, renderer.getTextWidth(kTitleFontId, truncatedLabel.c_str(), EpdFontFamily::BOLD) + kRowPaddingX);
|
||||
TouchRegistry::getInstance().add(Rect{rowX, rowY, rowWidth, rowHeight}, i, TouchRegistry::Item);
|
||||
const bool isSelected = selectedIndex == i;
|
||||
renderer.fillRoundedRect(rowX, rowY, rowWidth, rowHeight, kMenuRadius, isSelected ? Color::Black : Color::White);
|
||||
const int textY = rowY + (rowHeight - textLineHeight) / 2;
|
||||
@@ -329,6 +331,7 @@ void RoundedRaffTheme::drawList(const GfxRenderer& renderer, Rect rect, int item
|
||||
|
||||
for (int i = pageStartIndex; i < itemCount && i < pageStartIndex + pageItems; i++) {
|
||||
const int rowY = rect.y + (i % pageItems) * rowStep;
|
||||
TouchRegistry::getInstance().add(Rect{rowX, rowY, rowWidth, rowHeight}, i, TouchRegistry::Item);
|
||||
const bool isSelected = i == selectedIndex;
|
||||
renderer.fillRoundedRect(rowX, rowY, rowWidth, rowHeight, kRowRadius, isSelected ? Color::Black : Color::White);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user