feat: Add touch coordinate mapping and RTOS task yielding (#2481)

Co-authored-by: Julia Nguyen <julia@uxj.io>
This commit is contained in:
Justin Mitchell
2026-07-20 16:31:07 -04:00
committed by GitHub
co-authored by Julia Nguyen
parent c9188a7347
commit f42fab1c66
123 changed files with 3564 additions and 1380 deletions
+66 -163
View File
@@ -1,23 +1,21 @@
#pragma once
#include <FreeInkUIGfxRenderer.h>
#include <GfxRenderer.h>
#include <atomic>
#include <cstdint>
#include <functional>
#include <string>
#include <utility>
#include "activities/Activity.h"
#include "util/ButtonNavigator.h"
struct KeyDef {
char primary;
char secondary;
};
enum class SpecialKeyType { Shift, Mode, Space, Del, Ok };
enum class InputType { Text, Password, Url };
// Text entry on the FreeInkUI keyboard component: the SDK layout tables and
// keyboard() do the key rendering and hit-rect registration, InteractionBuffer
// routes taps/long-presses, and this activity owns the text field, cursor
// editing, and the URL snippet layouts.
class KeyboardEntryActivity : public Activity {
public:
explicit KeyboardEntryActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
@@ -43,16 +41,30 @@ class KeyboardEntryActivity : public Activity {
ButtonNavigator buttonNavigator;
int selectedRow = 0;
int selectedCol = 0;
int shiftState = 0;
bool symMode = false;
// Keyboard layers. The letter/symbol layers come from the SDK's builtin
// layouts (with the always-visible number row); the URL layers are
// app-defined tables in the .cpp.
freeink::ui::KeyboardLayoutId layoutId = freeink::ui::KeyboardLayoutId::QwertyEn;
bool shifted = false;
bool symbols = false;
bool urlPanel = false; // URL snippet panel replaces the letter layer
// Key hit rects registered by the keyboard component during render();
// loop() routes touch snapshots against them. 5-row EN layout registers 41
// keys, so 48 leaves headroom.
freeink::ui::InteractionBuffer<48> interactions;
// GPIO selection over the current layout grid (row/col in layout terms;
// the bottom action row is just the last row).
int selRow = 0;
int selCol = 0;
bool confirmHeld = false;
bool confirmLongHandled = false;
bool cursorMode = false;
bool togglePos = false;
size_t cursorPos = 0;
size_t cursorPos = 0; // byte offset into text (always on a code point boundary)
bool upHeld = false;
bool upLongHandled = false;
bool downHeld = false;
@@ -62,10 +74,15 @@ class KeyboardEntryActivity : public Activity {
size_t savedCursorPos = 0;
size_t rightStartCursorPos = 0;
bool urlMode = false;
static constexpr int URL_SNIPPET_COUNT = 9;
static constexpr const char* const urlSnippets[URL_SNIPPET_COUNT] = {
"https://", "www.", ".com", "http://", "192.168.", ".org", "/opds", ":8080", ".net"};
// Tap/hold routing (threshold long-press, release swallow, slide re-arm)
// lives in the SDK; loop() feeds it the level-triggered touch state.
freeink::ui::TouchHoldRouter touchRouter;
// loop() runs on the main task while render() rebuilds the interaction
// table on the render task; routing against a half-built table would read
// torn entries, so taps are dropped during the rebuild window. atomic (not
// volatile) so the flag also orders the table writes on dual-core targets.
std::atomic<bool> interactionsReady{false};
int delPressCount = 0;
bool hintVisible = false;
@@ -73,154 +90,40 @@ class KeyboardEntryActivity : public Activity {
void onComplete(std::string text);
void onCancel();
bool cursorPositionFromPoint(int x, int y, size_t& position) const;
std::string displayTextForCurrentState() const;
// Advance of s[start, end) measured in place by temporarily null-terminating
// at `end` — avoids a substr temporary per measurement.
int measureRange(std::string& s, int start, int end) const;
// Largest line end in (start, s.length()] whose advance fits maxWidth.
// Binary search over the monotonic prefix advance; always advances at least
// one byte so an oversized glyph cannot stall the wrap loop.
int lineBreakEnd(std::string& s, int start, int maxWidth) const;
const freeink::ui::KeyboardLayout& currentLayout() const;
const freeink::ui::KeyboardKey* selectedKey() const;
int selectedLogicalIndex() const;
void clampSelection();
void moveSelectionRow(int delta);
void moveSelectionCol(int delta);
bool syncSelectionToValue(int16_t value);
// Handles one key activation (by stable key id). Returns true when the
// screen needs a repaint; OK/cancel finish the activity instead.
bool activateValue(int16_t value, bool longPress);
bool clearAllOrAltOnSelected();
void insertUtf8(const char* out);
bool backspaceUtf8();
static size_t utf8Prev(const std::string& s, size_t pos);
static size_t utf8Next(const std::string& s, size_t pos);
freeink::ui::Rect keyboardRect() const;
static constexpr uint16_t LONG_PRESS_MS = 500;
static constexpr uint16_t DEL_LONG_PRESS_MS = 1500;
static constexpr uint16_t TOUCH_LONG_PRESS_MS = 350;
static constexpr uint16_t TOUCH_DEL_LONG_PRESS_MS = 900;
static constexpr int COLS = 10;
static constexpr int ABC_ROWS = 4;
static constexpr int SYM_ROWS = 4;
static constexpr int BOTTOM_KEY_COUNT = 5;
static constexpr KeyDef abcLayout[ABC_ROWS][COLS] = {
{{'1', '!'},
{'2', '@'},
{'3', '#'},
{'4', '$'},
{'5', '%'},
{'6', '^'},
{'7', '&'},
{'8', '*'},
{'9', '('},
{'0', ')'}},
{{'q', 'Q'},
{'w', 'W'},
{'e', 'E'},
{'r', 'R'},
{'t', 'T'},
{'y', 'Y'},
{'u', 'U'},
{'i', 'I'},
{'o', 'O'},
{'p', 'P'}},
{{'a', 'A'},
{'s', 'S'},
{'d', 'D'},
{'f', 'F'},
{'g', 'G'},
{'h', 'H'},
{'j', 'J'},
{'k', 'K'},
{'l', 'L'},
{'-', '_'}},
{{'z', 'Z'},
{'x', 'X'},
{'c', 'C'},
{'v', 'V'},
{'b', 'B'},
{'n', 'N'},
{'m', 'M'},
{'=', '+'},
{'.', '>'},
{',', '<'}},
};
static constexpr KeyDef urlLayout[ABC_ROWS][COLS] = {
{{'1', '!'},
{'2', '@'},
{'3', '#'},
{'4', '$'},
{'5', '%'},
{'6', '^'},
{'7', '&'},
{'8', '*'},
{'9', '('},
{'0', ')'}},
{{'q', 'Q'},
{'w', 'W'},
{'e', 'E'},
{'r', 'R'},
{'t', 'T'},
{'y', 'Y'},
{'u', 'U'},
{'i', 'I'},
{'o', 'O'},
{'p', 'P'}},
{{'a', 'A'},
{'s', 'S'},
{'d', 'D'},
{'f', 'F'},
{'g', 'G'},
{'h', 'H'},
{'j', 'J'},
{'k', 'K'},
{'l', 'L'},
{'-', '_'}},
{{'z', 'Z'},
{'x', 'X'},
{'c', 'C'},
{'v', 'V'},
{'b', 'B'},
{'n', 'N'},
{'m', 'M'},
{':', '+'},
{'.', '>'},
{'/', '<'}},
};
static constexpr KeyDef symLayout[SYM_ROWS][COLS] = {
{{'1', '\0'},
{'2', '\0'},
{'3', '\0'},
{'4', '\0'},
{'5', '\0'},
{'6', '\0'},
{'7', '\0'},
{'8', '\0'},
{'9', '\0'},
{'0', '\0'}},
{{'!', '\0'},
{'@', '\0'},
{'#', '\0'},
{'$', '\0'},
{'%', '\0'},
{'^', '\0'},
{'&', '\0'},
{'*', '\0'},
{'(', '\0'},
{')', '\0'}},
{{'-', '\0'},
{'_', '\0'},
{'=', '\0'},
{'+', '\0'},
{'[', '\0'},
{']', '\0'},
{'{', '\0'},
{'}', '\0'},
{';', '\0'},
{':', '\0'}},
{{'\'', '\0'},
{'"', '\0'},
{'/', '\0'},
{'\\', '\0'},
{'|', '\0'},
{'?', '\0'},
{'.', '\0'},
{',', '\0'},
{'~', '\0'},
{'`', '\0'}},
};
static const char* const shiftString[2];
int getContentRowCount() const;
int getContentColCount() const;
int getTotalRowCount() const;
bool isBottomRow(int row) const;
char getSelectedChar() const;
char getAlternativeChar() const;
bool handleKeyPress();
bool insertChar(char c);
void insertString(const std::string& str);
void mapColContentBottom(int& col, bool goingUp) const;
// App-specific key id: toggles the URL snippet panel (URL fields only).
static constexpr int16_t URL_PANEL_KEY = -3;
};