Files
firmware/src/graphics/niche/InkHUD/Applets/System/AppSwitcher/AppSwitcherApplet.cpp
T
cd6ac90f7e Add waypoint & geofence support with notifications for BaseUI and InkHUD (#10920)
* Implement GeofenceModule for waypoint crossing notifications and integrate with existing modules

* Waypoint Applet Initial Support on InkHUD

* undo tile change

* Update screen when Waypoint shows or dissapears

* Merge branch 'develop' into waypoint-geofence

* Geofence on InkHUD

* Update MapTile.h

* Update WaypointStore.cpp

* Notifications

* remove GF from waypoint screen

* Prevent Focus from closing the notifiaction banner

* Trunk fix

* cleanup

* undo merge conflix mistake

* Waypoint screen on BaseUI

* Focus preserve fix

* UI bugs

* Allow Inkhud to remove waypoint

* Respect Locked Waypoints

* Trunk fix

* Update WaypointStore.cpp

* Use 8-digit hex formatting for waypoint IDs.

0x%x was inconsistent with the repo's own convention (0x%08x for 32-bit IDs, used elsewhere in this file). Fixed here and in two other spots I found with the same issue (WaypointModule.cpp, GeofenceModule.cpp).

* Update ExternalNotificationModule.cpp

* Reject invalid surrogate codepoints in waypoint icon rendering

* Update WaypointModule.cpp

* Update WaypointStore.cpp

* Update WaypointStore.cpp

* Update WaypointStore.cpp

* trunk fix

* fix warnings

* power.h rename to Power.h

* Update Power.h

* Fix executable bit on bin/lint-ifdef-complexity.sh

Lost during a prior merge from develop (Windows checkout doesn't
preserve file mode), causing "execve failed: Permission denied" in
the Trunk Check Runner CI job. develop has this file at 100755;
restoring that here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Update README.md

* Clean up waypoint and geofence integration

* Minimize waypoint and geofence implementation

* removed unnecessary gating

* Geofence alert

* trunk fix

* Update test_main.cpp

* Update WaypointStore.cpp

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 16:17:28 +00:00

548 lines
22 KiB
C++

#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./AppSwitcherApplet.h"
#include "graphics/niche/InkHUD/InkHUD.h"
#include "graphics/niche/InkHUD/Tile.h"
#include <algorithm>
#include <cctype>
using namespace NicheGraphics;
namespace
{
static constexpr uint16_t BODY_MARGIN_X = 8;
static constexpr uint16_t BODY_MARGIN_Y = 6;
static constexpr uint16_t SLOT_GAP_X = 8;
static constexpr uint16_t SLOT_GAP_Y = 8;
static constexpr uint8_t ICON_RADIUS = 8;
static constexpr uint16_t FOOTER_PAD = 4;
static constexpr uint16_t LABEL_BOTTOM_PAD = 1;
static constexpr uint16_t LABEL_GAP_Y = 1;
static constexpr uint16_t TITLE_H_PAD = 8;
static constexpr uint8_t GRID_COLS = 3;
static constexpr uint8_t GRID_ROWS = 4;
static constexpr uint8_t ICON_NATIVE_SIZE = 48;
static constexpr uint8_t ICON_OUTLINE_STROKE = 1;
enum class IconKind : uint8_t { GENERIC, ALL_MESSAGES, DMS, CHANNEL, POSITIONS, RECENTS, HEARD, FAVORITES };
struct GridLayout {
uint16_t footerH = 0;
uint16_t bodyTop = 0;
uint16_t bodyBottom = 0;
uint16_t slotW = 0;
uint16_t slotH = 0;
uint16_t iconBox = 0;
};
/*
* Icons sourced from Material Design Icons PNG set (Apache 2.0):
* https://github.com/material-icons/material-icons-png
*
* Families used: outline-2x (48x48)
* apps, markunread, chat, forum, place, history, hearing, star_border
*/
static constexpr uint64_t icon_generic_apps[48] = {
0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL,
0x000000000000ULL, 0x000000000000ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL,
0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x000000000000ULL, 0x000000000000ULL,
0x000000000000ULL, 0x000000000000ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL,
0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x000000000000ULL, 0x000000000000ULL,
0x000000000000ULL, 0x000000000000ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL,
0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x000000000000ULL, 0x000000000000ULL,
0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL,
};
static constexpr uint64_t icon_all_messages[48] = {
0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL,
0x000000000000ULL, 0x000000000000ULL, 0x07FFFFFFFFE0ULL, 0x0FFFFFFFFFF0ULL, 0x0FFFFFFFFFF0ULL, 0x0FFFFFFFFFF0ULL,
0x0FC0000003F0ULL, 0x0FE0000007F0ULL, 0x0FF800001FF0ULL, 0x0FFE00007FF0ULL, 0x0FFF0000FFF0ULL, 0x0F7FC003FEF0ULL,
0x0F1FE007F8F0ULL, 0x0F07F81FE0F0ULL, 0x0F03FE7FC0F0ULL, 0x0F00FFFF00F0ULL, 0x0F007FFE00F0ULL, 0x0F001FF800F0ULL,
0x0F0007E000F0ULL, 0x0F0003C000F0ULL, 0x0F00000000F0ULL, 0x0F00000000F0ULL, 0x0F00000000F0ULL, 0x0F00000000F0ULL,
0x0F00000000F0ULL, 0x0F00000000F0ULL, 0x0F00000000F0ULL, 0x0F00000000F0ULL, 0x0F00000000F0ULL, 0x0F00000000F0ULL,
0x0FFFFFFFFFF0ULL, 0x0FFFFFFFFFF0ULL, 0x0FFFFFFFFFF0ULL, 0x07FFFFFFFFE0ULL, 0x000000000000ULL, 0x000000000000ULL,
0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL,
};
static constexpr uint64_t icon_dms[48] = {
0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x07FFFFFFFFE0ULL, 0x0FFFFFFFFFF0ULL,
0x0FFFFFFFFFF0ULL, 0x0FFFFFFFFFF0ULL, 0x0F00000000F0ULL, 0x0F00000000F0ULL, 0x0F00000000F0ULL, 0x0F00000000F0ULL,
0x0F0FFFFFF0F0ULL, 0x0F0FFFFFF0F0ULL, 0x0F0FFFFFF0F0ULL, 0x0F0FFFFFF0F0ULL, 0x0F00000000F0ULL, 0x0F00000000F0ULL,
0x0F0FFFFFF0F0ULL, 0x0F0FFFFFF0F0ULL, 0x0F0FFFFFF0F0ULL, 0x0F0FFFFFF0F0ULL, 0x0F00000000F0ULL, 0x0F00000000F0ULL,
0x0F0FFFF000F0ULL, 0x0F0FFFF000F0ULL, 0x0F0FFFF000F0ULL, 0x0F0FFFF000F0ULL, 0x0F00000000F0ULL, 0x0F00000000F0ULL,
0x0F00000000F0ULL, 0x0F00000000F0ULL, 0x0F7FFFFFFFF0ULL, 0x0FFFFFFFFFF0ULL, 0x0FFFFFFFFFF0ULL, 0x0FFFFFFFFFE0ULL,
0x0FF000000000ULL, 0x0FE000000000ULL, 0x0FC000000000ULL, 0x0F8000000000ULL, 0x0F0000000000ULL, 0x0E0000000000ULL,
0x0C0000000000ULL, 0x080000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL,
};
static constexpr uint64_t icon_channel[48] = {
0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x0FFFFFFFC000ULL, 0x0FFFFFFFC000ULL,
0x0FFFFFFFC000ULL, 0x0FFFFFFFC000ULL, 0x0F000003C000ULL, 0x0F000003C000ULL, 0x0F000003C000ULL, 0x0F000003C000ULL,
0x0F000003C3F0ULL, 0x0F000003C3F0ULL, 0x0F000003C3F0ULL, 0x0F000003C3F0ULL, 0x0F000003C3F0ULL, 0x0F000003C3F0ULL,
0x0F000003C3F0ULL, 0x0F000003C3F0ULL, 0x0F000003C3F0ULL, 0x0F000003C3F0ULL, 0x0F7FFFFFC3F0ULL, 0x0FFFFFFFC3F0ULL,
0x0FFFFFFFC3F0ULL, 0x0FFFFFFFC3F0ULL, 0x0FF0000003F0ULL, 0x0FE0000003F0ULL, 0x0FC0000003F0ULL, 0x0F80000003F0ULL,
0x0F0FFFFFFFF0ULL, 0x0E0FFFFFFFF0ULL, 0x0C0FFFFFFFF0ULL, 0x080FFFFFFFF0ULL, 0x000FFFFFFFF0ULL, 0x000FFFFFFFF0ULL,
0x000000000FF0ULL, 0x0000000007F0ULL, 0x0000000003F0ULL, 0x0000000001F0ULL, 0x0000000000F0ULL, 0x000000000070ULL,
0x000000000030ULL, 0x000000000010ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL,
};
static constexpr uint64_t icon_positions[48] = {
0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x00001FF80000ULL, 0x00007FFE0000ULL,
0x0001FFFF8000ULL, 0x0003FFFFC000ULL, 0x0007FFFFE000ULL, 0x000FF00FF000ULL, 0x000FC003F000ULL, 0x001F8001F800ULL,
0x001F0000F800ULL, 0x003F07E0FC00ULL, 0x003E0FF07C00ULL, 0x003E1FF87C00ULL, 0x003E1FF87C00ULL, 0x003E1FF87C00ULL,
0x003E1FF87C00ULL, 0x003E1FF87C00ULL, 0x003E1FF87C00ULL, 0x003E0FF07C00ULL, 0x003E07E07C00ULL, 0x001F0000F800ULL,
0x001F0000F800ULL, 0x001F8001F800ULL, 0x000F8001F000ULL, 0x000FC003F000ULL, 0x0007C003E000ULL, 0x0007E007E000ULL,
0x0003F00FC000ULL, 0x0003F00FC000ULL, 0x0001F81F8000ULL, 0x0001FC3F8000ULL, 0x0000FC3F0000ULL, 0x00007E7E0000ULL,
0x00003FFC0000ULL, 0x00003FFC0000ULL, 0x00001FF80000ULL, 0x00000FF00000ULL, 0x00000FF00000ULL, 0x000007E00000ULL,
0x000003C00000ULL, 0x000001800000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL,
};
static constexpr uint64_t icon_recents[48] = {
0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL,
0x00000FFF0000ULL, 0x00003FFFC000ULL, 0x0000FFFFF000ULL, 0x0001FFFFF800ULL, 0x0007FFFFFE00ULL, 0x000FF801FF00ULL,
0x000FE0007F00ULL, 0x001FC0003F80ULL, 0x003F00000FC0ULL, 0x003F00000FC0ULL, 0x007E00E007E0ULL, 0x007C00E003E0ULL,
0x00FC00E003F0ULL, 0x00F800E001F0ULL, 0x00F800E001F0ULL, 0x00F800E001F0ULL, 0x00F800E001F0ULL, 0x00F800E001F0ULL,
0x3FFFC0F001F0ULL, 0x1FFF80FC01F0ULL, 0x0FFF00FF01F0ULL, 0x07FE003F81F0ULL, 0x03FC001FC1F0ULL, 0x01F80007C3F0ULL,
0x00F0000183E0ULL, 0x0060000007E0ULL, 0x000000000FC0ULL, 0x000000000FC0ULL, 0x0001C0003F80ULL, 0x0003E0007F00ULL,
0x0007F801FF00ULL, 0x0007FFFFFE00ULL, 0x0001FFFFF800ULL, 0x0000FFFFF000ULL, 0x00003FFFC000ULL, 0x00000FFF0000ULL,
0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL,
};
static constexpr uint64_t icon_heard[48] = {
0x000000000000ULL, 0x000000000000ULL, 0x000800000000ULL, 0x001C00000000ULL, 0x003E01FF8000ULL, 0x007F07FFE000ULL,
0x007E1FFFF800ULL, 0x00FC3FFFFC00ULL, 0x00F87FFFFE00ULL, 0x01F8FF00FF00ULL, 0x01F0FC003F00ULL, 0x01F1F8001F80ULL,
0x03E1F0000F80ULL, 0x03E3F07E0FC0ULL, 0x03E3E0FF07C0ULL, 0x03E3E1FF87C0ULL, 0x03E3E1FF87C0ULL, 0x03E3E1FF87C0ULL,
0x03E3E1FF8000ULL, 0x03E3E1FF8000ULL, 0x03E3E1FF8000ULL, 0x03E3E0FF0000ULL, 0x03E3F07E0000ULL, 0x03E1F0000000ULL,
0x01F1F8000000ULL, 0x01F1F8000000ULL, 0x01F8FC000000ULL, 0x00F87E000000ULL, 0x00FC7F800000ULL, 0x007E3FC00000ULL,
0x007F1FE00000ULL, 0x003E0FF00000ULL, 0x001C03F00000ULL, 0x000801F80000ULL, 0x000000F80000ULL, 0x000000FC0000ULL,
0x0000007C07C0ULL, 0x0000007E07C0ULL, 0x0000003F0FC0ULL, 0x0000003FFFC0ULL, 0x0000001FFF80ULL, 0x0000000FFF00ULL,
0x00000007FE00ULL, 0x00000003FC00ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL,
};
static constexpr uint64_t icon_favorites[48] = {
0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000001800000ULL, 0x000001800000ULL,
0x000003C00000ULL, 0x000003C00000ULL, 0x000003C00000ULL, 0x000007E00000ULL, 0x000007E00000ULL, 0x00000FF00000ULL,
0x00000FF00000ULL, 0x00001FF80000ULL, 0x00001FF80000ULL, 0x00001E780000ULL, 0x00003E7C0000ULL, 0x003FFC3FFC00ULL,
0x0FFFFC3FFFF0ULL, 0x0FFFF81FFFF0ULL, 0x03FFF81FFFC0ULL, 0x01F800001F80ULL, 0x00FC00003F00ULL, 0x007E00007E00ULL,
0x003F8001FC00ULL, 0x001FC003F800ULL, 0x000FE007F000ULL, 0x0003E007C000ULL, 0x0003E007C000ULL, 0x0003C003C000ULL,
0x0003C003C000ULL, 0x0003C3C3C000ULL, 0x0007CFF3E000ULL, 0x00079FF9E000ULL, 0x0007FFFFE000ULL, 0x0007FE7FE000ULL,
0x000FFC3FF000ULL, 0x000FF00FF000ULL, 0x000FC003F000ULL, 0x000F8001F000ULL, 0x001E00007000ULL, 0x001800001800ULL,
0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL,
};
using IconBitmap = const uint64_t *;
IconBitmap iconBitmapForKind(IconKind kind)
{
switch (kind) {
case IconKind::ALL_MESSAGES:
return icon_all_messages;
case IconKind::DMS:
return icon_dms;
case IconKind::CHANNEL:
return icon_channel;
case IconKind::POSITIONS:
return icon_positions;
case IconKind::RECENTS:
return icon_recents;
case IconKind::HEARD:
return icon_heard;
case IconKind::FAVORITES:
return icon_favorites;
case IconKind::GENERIC:
default:
return icon_generic_apps;
}
}
GridLayout computeLayout(const InkHUD::Applet *applet)
{
GridLayout layout;
const uint16_t w = applet->width();
const uint16_t h = applet->height();
layout.footerH = InkHUD::Applet::fontSmall.lineHeight() + (FOOTER_PAD * 2);
layout.bodyTop = BODY_MARGIN_Y;
layout.bodyBottom = (h > (layout.footerH + BODY_MARGIN_Y)) ? (h - layout.footerH - BODY_MARGIN_Y) : layout.bodyTop;
const uint16_t bodyW = (w > (BODY_MARGIN_X * 2)) ? (w - (BODY_MARGIN_X * 2)) : 1;
const uint16_t bodyH = (layout.bodyBottom > layout.bodyTop) ? (layout.bodyBottom - layout.bodyTop) : 1;
const uint16_t gapsX = SLOT_GAP_X * (GRID_COLS - 1);
const uint16_t gapsY = SLOT_GAP_Y * (GRID_ROWS - 1);
layout.slotW = (bodyW > gapsX) ? ((bodyW - gapsX) / GRID_COLS) : 1;
layout.slotH = (bodyH > gapsY) ? ((bodyH - gapsY) / GRID_ROWS) : 1;
const uint16_t maxIconW = (layout.slotW > 6) ? (layout.slotW - 6) : layout.slotW;
const uint16_t maxIconH = (layout.slotH > (InkHUD::Applet::fontSmall.lineHeight() + LABEL_GAP_Y + LABEL_BOTTOM_PAD + 6))
? (layout.slotH - InkHUD::Applet::fontSmall.lineHeight() - LABEL_GAP_Y - LABEL_BOTTOM_PAD - 6)
: layout.slotH / 2;
layout.iconBox = std::max<uint16_t>(20, std::min<uint16_t>(maxIconW, maxIconH));
return layout;
}
std::string lowercase(const char *name)
{
if (!name)
return "";
std::string out(name);
std::transform(out.begin(), out.end(), out.begin(), [](unsigned char c) { return (char)std::tolower(c); });
return out;
}
IconKind iconKindForAppletName(const char *name)
{
const std::string lower = lowercase(name);
if (lower.find("all message") != std::string::npos || lower.find("messages") != std::string::npos)
return IconKind::ALL_MESSAGES;
if (lower.find("dm") != std::string::npos)
return IconKind::DMS;
if (lower.find("channel") != std::string::npos)
return IconKind::CHANNEL;
if (lower.find("position") != std::string::npos)
return IconKind::POSITIONS;
if (lower.find("waypoint") != std::string::npos)
return IconKind::POSITIONS;
if (lower.find("recent") != std::string::npos)
return IconKind::RECENTS;
if (lower.find("heard") != std::string::npos)
return IconKind::HEARD;
if (lower.find("favorite") != std::string::npos)
return IconKind::FAVORITES;
return IconKind::GENERIC;
}
void drawIconBitmapScaled(InkHUD::Applet *applet, IconBitmap bmp48, int16_t left, int16_t top, uint16_t boxSize, uint16_t color)
{
if (!bmp48 || boxSize == 0)
return;
auto srcOn = [bmp48](int16_t sx, int16_t sy) -> bool {
if (sx < 0 || sy < 0 || sx >= ICON_NATIVE_SIZE || sy >= ICON_NATIVE_SIZE)
return false;
const uint64_t rowBits = bmp48[sy];
return (rowBits & (1ULL << (47 - sx))) != 0;
};
for (uint16_t y = 0; y < boxSize; y++) {
const uint8_t srcY = (uint8_t)((y * ICON_NATIVE_SIZE) / boxSize);
for (uint16_t x = 0; x < boxSize; x++) {
const uint8_t srcX = (uint8_t)((x * ICON_NATIVE_SIZE) / boxSize);
if (!srcOn(srcX, srcY))
continue;
const uint16_t w = std::min<uint16_t>(ICON_OUTLINE_STROKE, boxSize - x);
const uint16_t h = std::min<uint16_t>(ICON_OUTLINE_STROKE, boxSize - y);
applet->fillRect(left + x, top + y, w, h, color);
}
}
}
} // namespace
InkHUD::AppSwitcherApplet::AppSwitcherApplet()
{
alwaysRender = true;
}
void InkHUD::AppSwitcherApplet::rebuildActiveAppletList()
{
activeAppletIndices.clear();
const auto &settings = inkhud->persistence->settings;
const uint8_t tileCount = std::min<uint8_t>(settings.userTiles.count, Persistence::MAX_TILES_GLOBAL);
const uint8_t focusedTile = (tileCount > 0) ? std::min<uint8_t>(settings.userTiles.focused, tileCount - 1) : 0;
// Applets displayed on other tiles should not be selectable here.
std::vector<bool> occupiedOnOtherTiles(inkhud->userApplets.size(), false);
for (uint8_t tile = 0; tile < tileCount; tile++) {
if (tile == focusedTile)
continue;
const uint8_t appletIndex = settings.userTiles.displayedUserApplet[tile];
if (appletIndex < occupiedOnOtherTiles.size())
occupiedOnOtherTiles[appletIndex] = true;
}
for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) {
Applet *a = inkhud->userApplets.at(i);
if (a && a->isActive() && !occupiedOnOtherTiles[i])
activeAppletIndices.push_back(i);
}
}
uint8_t InkHUD::AppSwitcherApplet::cardsPerPage() const
{
return GRID_COLS * GRID_ROWS;
}
uint8_t InkHUD::AppSwitcherApplet::currentPage() const
{
const uint8_t cpp = cardsPerPage();
if (cpp == 0)
return 0;
return selectedIndex / cpp;
}
void InkHUD::AppSwitcherApplet::stepPage(int8_t delta)
{
if (activeAppletIndices.empty())
return;
const uint8_t cpp = cardsPerPage();
const uint8_t pageCount = std::max<uint8_t>(1, (activeAppletIndices.size() + cpp - 1) / cpp);
int16_t nextPage = (int16_t)currentPage() + delta;
while (nextPage < 0)
nextPage += pageCount;
while (nextPage >= pageCount)
nextPage -= pageCount;
selectedIndex = std::min<uint8_t>((uint8_t)(nextPage * cpp), activeAppletIndices.size() - 1);
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
void InkHUD::AppSwitcherApplet::clampSelection()
{
if (activeAppletIndices.empty()) {
selectedIndex = 0;
return;
}
if (selectedIndex >= activeAppletIndices.size())
selectedIndex = activeAppletIndices.size() - 1;
}
void InkHUD::AppSwitcherApplet::activateSelectedApplet()
{
if (activeAppletIndices.empty()) {
sendToBackground();
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
return;
}
const uint8_t appletIndex = activeAppletIndices.at(selectedIndex);
sendToBackground();
inkhud->showApplet(appletIndex);
}
void InkHUD::AppSwitcherApplet::onForeground()
{
rebuildActiveAppletList();
clampSelection();
handleInput = true;
lockRequests = true;
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
void InkHUD::AppSwitcherApplet::onBackground()
{
handleInput = false;
lockRequests = false;
if (borrowedTileOwner)
borrowedTileOwner->bringToForeground();
Tile *t = getTile();
if (t)
t->assignApplet(borrowedTileOwner);
borrowedTileOwner = nullptr;
}
void InkHUD::AppSwitcherApplet::show(Tile *t)
{
if (!t)
return;
borrowedTileOwner = t->getAssignedApplet();
if (borrowedTileOwner)
borrowedTileOwner->sendToBackground();
t->assignApplet(this);
bringToForeground();
}
void InkHUD::AppSwitcherApplet::onRender(bool full)
{
(void)full;
const GridLayout layout = computeLayout(this);
const uint8_t cpp = cardsPerPage();
const uint8_t page = currentPage();
const uint8_t pageStart = page * cpp;
setFont(fontMedium);
setTextColor(BLACK);
fillRect(0, 0, width(), height(), WHITE);
drawRect(0, 0, width(), height(), BLACK);
if (activeAppletIndices.empty()) {
setFont(fontSmall);
printAt(width() / 2, height() / 2, "No Available Applets", CENTER, MIDDLE);
return;
}
for (uint8_t i = 0; i < cpp; i++) {
const uint8_t idx = pageStart + i;
if (idx >= activeAppletIndices.size())
break;
const uint8_t row = i / GRID_COLS;
const uint8_t col = i % GRID_COLS;
const int16_t slotL = BODY_MARGIN_X + (col * (layout.slotW + SLOT_GAP_X));
const int16_t slotT = layout.bodyTop + (row * (layout.slotH + SLOT_GAP_Y));
const bool selected = (idx == selectedIndex);
const uint8_t appletIndex = activeAppletIndices.at(idx);
Applet *a = inkhud->userApplets.at(appletIndex);
if (!a)
continue;
const int16_t iconLeft = slotL + ((layout.slotW - layout.iconBox) / 2);
const int16_t iconTop = slotT + 1;
// Requested style: icon in outlined rounded square only (no filled box, no outer app card).
drawRoundRect(iconLeft, iconTop, layout.iconBox, layout.iconBox, ICON_RADIUS, BLACK);
if (selected)
drawRoundRect(iconLeft + 2, iconTop + 2, layout.iconBox - 4, layout.iconBox - 4, ICON_RADIUS, BLACK);
const IconBitmap bmp = iconBitmapForKind(iconKindForAppletName(a->name));
drawIconBitmapScaled(this, bmp, iconLeft + 3, iconTop + 3, layout.iconBox - 6, BLACK);
setFont(fontSmall);
std::string label = a->name ? a->name : "Applet";
const uint16_t maxLabelW = layout.slotW > 4 ? (layout.slotW - 4) : layout.slotW;
if (getTextWidth(label) > maxLabelW) {
while (!label.empty() && getTextWidth(label + "...") > maxLabelW)
label.pop_back();
label = label.empty() ? "..." : label + "...";
}
const int16_t labelY = iconTop + layout.iconBox + LABEL_GAP_Y;
setTextColor(BLACK);
printAt(slotL + (layout.slotW / 2), labelY, label.c_str(), CENTER, TOP);
if (a->isForeground())
fillCircle(iconLeft + layout.iconBox - 4, iconTop + 4, 2, BLACK);
}
const uint8_t pageCount = std::max<uint8_t>(1, (activeAppletIndices.size() + cpp - 1) / cpp);
if (pageCount > 1) {
setFont(fontSmall);
setTextColor(BLACK);
const int16_t footerY = height() - layout.footerH + FOOTER_PAD;
printAt(TITLE_H_PAD, footerY, "<", LEFT, TOP);
printAt(width() - TITLE_H_PAD, footerY, ">", RIGHT, TOP);
const std::string pageText = std::to_string(page + 1) + "/" + std::to_string(pageCount);
printAt(width() / 2, footerY, pageText.c_str(), CENTER, TOP);
}
}
bool InkHUD::AppSwitcherApplet::onTouchPoint(uint16_t x, uint16_t y, bool longPress)
{
(void)longPress;
Tile *t = getTile();
if (!t || activeAppletIndices.empty())
return true;
const uint16_t tileL = t->getLeft();
const uint16_t tileT = t->getTop();
const uint16_t tileR = tileL + t->getWidth();
const uint16_t tileB = tileT + t->getHeight();
if (x < tileL || x >= tileR || y < tileT || y >= tileB)
return false;
const GridLayout layout = computeLayout(this);
const uint8_t cpp = cardsPerPage();
const uint8_t page = currentPage();
const uint8_t pageStart = page * cpp;
const int16_t localX = (int16_t)x - (int16_t)tileL;
const int16_t localY = (int16_t)y - (int16_t)tileT;
for (uint8_t i = 0; i < cpp; i++) {
const uint8_t idx = pageStart + i;
if (idx >= activeAppletIndices.size())
break;
const uint8_t row = i / GRID_COLS;
const uint8_t col = i % GRID_COLS;
const int16_t slotL = BODY_MARGIN_X + (col * (layout.slotW + SLOT_GAP_X));
const int16_t slotT = layout.bodyTop + (row * (layout.slotH + SLOT_GAP_Y));
if (localX < slotL || localX >= (slotL + (int16_t)layout.slotW))
continue;
if (localY < slotT || localY >= (slotT + (int16_t)layout.slotH))
continue;
selectedIndex = idx;
clampSelection();
activateSelectedApplet();
return true;
}
const uint8_t pageCount = std::max<uint8_t>(1, (activeAppletIndices.size() + cpp - 1) / cpp);
if (pageCount <= 1)
return true;
const int16_t footerTop = height() - layout.footerH;
if (localY >= footerTop) {
if (localX < (int16_t)(width() / 3))
stepPage(-1);
else if (localX >= (int16_t)((width() * 2) / 3))
stepPage(1);
}
return true;
}
void InkHUD::AppSwitcherApplet::onButtonShortPress()
{
if (activeAppletIndices.empty())
return;
selectedIndex = (selectedIndex + 1) % activeAppletIndices.size();
clampSelection();
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
void InkHUD::AppSwitcherApplet::onButtonLongPress()
{
activateSelectedApplet();
}
void InkHUD::AppSwitcherApplet::onExitShort()
{
sendToBackground();
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
void InkHUD::AppSwitcherApplet::onNavUp()
{
if (activeAppletIndices.empty())
return;
if (selectedIndex == 0)
selectedIndex = activeAppletIndices.size() - 1;
else
selectedIndex--;
clampSelection();
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
void InkHUD::AppSwitcherApplet::onNavDown()
{
if (activeAppletIndices.empty())
return;
selectedIndex = (selectedIndex + 1) % activeAppletIndices.size();
clampSelection();
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
#endif