Files
firmware/src/graphics/niche/InkHUD/Tile.cpp
scobert969 7bbfe99fbe Add on-screen keyboard to InkHUD (#9445)
* Added keyboard option to menu. Shows a keyboard layout but does not type.

* Keyboard types into text box and wraps.

* send FreeText messages from the send submenu

- renamed `KEYBOARD` action to `FREE_TEXT` and moved its menu location
to the send submenu
- opening the FreeText applet from the menu keeps the menu open and
disabled the timeout
- the FreeText applet writes to inkhud->freetext
- the sending a canned message checks inkhud->freetext and if it isn't
empty, sends and clears the inkhud->freetext

* Text scrolls along with input

* handle free text message completion as an event

implements `handleFreeText` and `OnFreeText()` for system applets to
interface with the FreeText Applet

The FreeText Applet generates an `OnFreeText` event when completing a
message which is handled by the first system applet with the
`handleFreeText` flag set to true.

The Menu Applet now handles this event.

* call `onFreeText` whenever the FreeText Applet exits

allows the menu to consistently restart its auto-close timeout

* Add text cursor

* Change UI to remove the header and make text box longer
Keyboard displays captial letters for legibility
Keyboard types captial letters with long press

* center FreeText keys and draw symbolic buttons

Move input field and keyboard drawing to their own functions:
- `drawInputField()`
- `drawKeyboard()`

Store the keys in a 1-dimensional array

Implement a matching array, `keyWidths`, to set key widths relative to
the font size

* Add character limit and counter

* Fix softlock when hitting character limit

* Move text box as its own menu page

* rework FreeTextApplet into KeyboardApplet

- The Keyboard Applet renders an on-screen keyboard at the lower portion
of the screen.
- Calling `inkhud->openKeyboard()` sends all the user applets to the
background and resizes the first system applet with `handleFreeText` set
to True to fit above the on-screen keyboard
- `inkhud->closeKeyboard()` reverses this layout change

* Fix input box rendering and add character limit to menu free text

* remove FREE_TEXT menu page and use the FREE_TEXT menu action solely

* force update when changing the free text message

* reorganize KeyboardApplet

- add comments after each row of `key[]` and `keyWidths[]` to preserve
formatting

- The selected key is now set using the key index directly

- rowWidths are pre-calculated in the KeyboardApplet constructor

- removed `drawKeyboard()` and implemented `drawKeyLabel()`

* implement `Renderer::clearTile()` to clear the region below a tile

* add parameter to forceUpdate() for re-rendering the full screen

setting the `all` parameter to true in `inkhud->forceUpdate()` now
causes the full screen buffer to clear an re-render. This is helpful for
when sending applets to the background and the UI needs a clean canvas.

System Applets can now set the `alwaysRender` flag true which causes it
to re-render on every screen update. This is set to true in the Battery
Icon Applet.

* clean up tile clearing loops

* implement dirty rendering to let applets draw over their previous render

- `Applet::requestUpdate()` now has an optional flag to keep the old
canvas

- If honored, the renderer calls `render(true)` which runs
`onDirtyRender()` instead of `onRender()` for said applet

- The renderer will not call a dirty render if the full screen is
getting re-rendered

* simplify arithmetic in clearTile for better understanding

* combine Applet::onRender() and Applet::onDirtyRender() into Applet::onRender(bool full)

- add new `full` parameter to onRender() in every applet. This parameter
can be ignored by most applets.

- `Applet::requestUpdate()` has an optional flag that requests a full
render by default

* implement tile and partial rendering in KeyboardApplet

* add comment for drawKeyLabel()

* improve clarity of byte operations in clearTile()

* remove typo and commented code

* fix inaccurate comments

* add null check to openKeyboard() and closeKeyboard()

---------

Co-authored-by: zeropt <ferr0fluidmann@gmail.com>
Co-authored-by: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com>
2026-01-30 13:35:10 -05:00

253 lines
7.0 KiB
C++

#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./Tile.h"
#include "concurrency/Periodic.h"
using namespace NicheGraphics;
// Static members of Tile class (for linking)
InkHUD::Tile *InkHUD::Tile::highlightTarget;
bool InkHUD::Tile::highlightShown;
// For dismissing the highlight indicator, after a few seconds
// Highlighting is used to inform user of which tile is now focused
static concurrency::Periodic *taskHighlight;
static int32_t runtaskHighlight()
{
LOG_DEBUG("Dismissing Highlight");
InkHUD::Tile::highlightShown = false;
InkHUD::Tile::highlightTarget = nullptr;
InkHUD::InkHUD::getInstance()->forceUpdate(Drivers::EInk::UpdateTypes::FAST, true); // Re-render, clearing the highlighting
return taskHighlight->disable();
}
static void inittaskHighlight()
{
static bool doneOnce = false;
if (!doneOnce) {
taskHighlight = new concurrency::Periodic("Highlight", runtaskHighlight);
taskHighlight->disable();
doneOnce = true;
}
}
InkHUD::Tile::Tile()
{
inkhud = InkHUD::getInstance();
inittaskHighlight();
Tile::highlightTarget = nullptr;
Tile::highlightShown = false;
}
InkHUD::Tile::Tile(int16_t left, int16_t top, uint16_t width, uint16_t height)
{
assert(width > 0 && height > 0);
this->left = left;
this->top = top;
this->width = width;
this->height = height;
}
// Set the region of the tile automatically, based on the user's chosen layout
// This method places tiles which will host user applets
// The WindowManager multiplexes the applets to these tiles automatically
void InkHUD::Tile::setRegion(uint8_t userTileCount, uint8_t tileIndex)
{
uint16_t displayWidth = inkhud->width();
uint16_t displayHeight = inkhud->height();
bool landscape = displayWidth > displayHeight;
// Check for any stray tiles
if (tileIndex > (userTileCount - 1)) {
// Dummy values to prevent rendering
LOG_WARN("Tile index out of bounds");
left = -2;
top = -2;
width = 1;
height = 1;
return;
}
// Todo: special handling for 3 tile layout
// Gutters between tiles
const uint16_t spacing = 4;
switch (userTileCount) {
// One tile only
case 1:
left = 0;
top = 0;
width = displayWidth;
height = displayHeight;
break;
// Two tiles
case 2:
if (landscape) {
// Side by side
left = ((displayWidth / 2) + (spacing / 2)) * tileIndex;
top = 0;
width = (displayWidth / 2) - (spacing / 2);
height = displayHeight;
} else {
// Above and below
left = 0;
top = 0 + (((displayHeight / 2) + (spacing / 2)) * tileIndex);
width = displayWidth;
height = (displayHeight / 2) - (spacing / 2);
}
break;
// Four tiles
case 4:
width = (displayWidth / 2) - (spacing / 2);
height = (displayHeight / 2) - (spacing / 2);
switch (tileIndex) {
case 0:
left = 0;
top = 0;
break;
case 1:
left = 0 + (width - 1) + spacing;
top = 0;
break;
case 2:
left = 0;
top = 0 + (height - 1) + spacing;
break;
case 3:
left = 0 + (width - 1) + spacing;
top = 0 + (height - 1) + spacing;
break;
}
break;
default:
LOG_ERROR("Unsupported tile layout");
assert(0);
}
assert(width > 0 && height > 0);
}
// Manually set the region for a tile
// This is only done for tiles which will host certain "System Applets", which have unique position / sizes:
// Things like the NotificationApplet, BatteryIconApplet, etc
void InkHUD::Tile::setRegion(int16_t left, int16_t top, uint16_t width, uint16_t height)
{
assert(width > 0 && height > 0);
this->left = left;
this->top = top;
this->width = width;
this->height = height;
}
// Place an applet onto a tile
// Creates a reciprocal link between applet and tile
// The tile should always know which applet is displayed
// The applet should always know which tile it is display on
// This is enforced with asserts
// Assigning a new applet will break a previous link
// Link may also be broken by assigning a nullptr
void InkHUD::Tile::assignApplet(Applet *a)
{
// Break the link between old applet and this tile
if (assignedApplet)
assignedApplet->setTile(nullptr);
// Store the new applet
assignedApplet = a;
// Create the reciprocal link between the new applet and this tile
if (a)
a->setTile(this);
}
// Get pointer to whichever applet is displayed on this tile
InkHUD::Applet *InkHUD::Tile::getAssignedApplet()
{
return assignedApplet;
}
// Receive drawing output from the assigned applet,
// and translate it from "applet-space" coordinates, to it's true location.
// The final "rotation" step is performed by the windowManager
void InkHUD::Tile::handleAppletPixel(int16_t x, int16_t y, Color c)
{
// Move pixels from applet-space to tile-space
x += left;
y += top;
// Crop to tile borders
if (x >= left && x < (left + width) && y >= top && y < (top + height)) {
// Pass to the renderer
inkhud->drawPixel(x, y, c);
}
}
// Used in Renderer for clearing the tile
int16_t InkHUD::Tile::getLeft()
{
return left;
}
// Used in Renderer for clearing the tile
int16_t InkHUD::Tile::getTop()
{
return top;
}
// Called by Applet base class, when setting applet dimensions, immediately before render
uint16_t InkHUD::Tile::getWidth()
{
return width;
}
// Called by Applet base class, when setting applet dimensions, immediately before render
uint16_t InkHUD::Tile::getHeight()
{
return height;
}
// Longest edge of the display, in pixels
// A 296px x 250px display will return 296, for example
// Maximum possible size of any tile's width / height
// Used by some components to allocate resources for the "worst possible situation"
// "Sizing the cathedral for christmas eve"
uint16_t InkHUD::Tile::maxDisplayDimension()
{
InkHUD *inkhud = InkHUD::getInstance();
return max(inkhud->height(), inkhud->width());
}
// Ask for this tile to be highlighted
// Used to indicate which tile is now indicated after focus changes
// Only used for aux button focus changes, not changes via menu
void InkHUD::Tile::requestHighlight()
{
Tile::highlightTarget = this;
Tile::highlightShown = false;
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FAST, true);
}
// Starts the timer which will automatically dismiss the highlighting, if the tile doesn't organically redraw first
void InkHUD::Tile::startHighlightTimeout()
{
taskHighlight->setIntervalFromNow(5 * 1000UL);
taskHighlight->enabled = true;
}
// Stop the timer which would automatically dismiss the highlighting
// Called if the tile organically renders before the timer is up
void InkHUD::Tile::cancelHighlightTimeout()
{
if (taskHighlight->enabled)
taskHighlight->disable();
}
#endif