BaseUI: Color Support for TFT Nodes (#10233)
* True Colors on TFT (Heltec Mesh Node T114, Heltec Vision Master T190, CardPuter Adv, T-Deck, T-Lora Pager) * Theme support - New and some Classic Themes! * Colored Compass --------- Co-authored-by: Jason P <applewiz@mac.com> Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
This commit is contained in:
co-authored by
GitHub
Jason P
Jonathan Bennett
Ben Meadors
parent
4d4e14600c
commit
8dde4eeee1
+59
-53
@@ -39,6 +39,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#include "draw/NodeListRenderer.h"
|
#include "draw/NodeListRenderer.h"
|
||||||
#include "draw/NotificationRenderer.h"
|
#include "draw/NotificationRenderer.h"
|
||||||
#include "draw/UIRenderer.h"
|
#include "draw/UIRenderer.h"
|
||||||
|
#include "graphics/TFTColorRegions.h"
|
||||||
#include "modules/CannedMessageModule.h"
|
#include "modules/CannedMessageModule.h"
|
||||||
|
|
||||||
#if !MESHTASTIC_EXCLUDE_GPS
|
#if !MESHTASTIC_EXCLUDE_GPS
|
||||||
@@ -54,6 +55,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#include "gps/RTC.h"
|
#include "gps/RTC.h"
|
||||||
#include "graphics/ScreenFonts.h"
|
#include "graphics/ScreenFonts.h"
|
||||||
#include "graphics/SharedUIDisplay.h"
|
#include "graphics/SharedUIDisplay.h"
|
||||||
|
#include "graphics/TFTPalette.h"
|
||||||
#include "graphics/emotes.h"
|
#include "graphics/emotes.h"
|
||||||
#include "graphics/images.h"
|
#include "graphics/images.h"
|
||||||
#include "input/TouchScreenImpl1.h"
|
#include "input/TouchScreenImpl1.h"
|
||||||
@@ -69,12 +71,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#include "target_specific.h"
|
#include "target_specific.h"
|
||||||
extern MessageStore messageStore;
|
extern MessageStore messageStore;
|
||||||
|
|
||||||
#if USE_TFTDISPLAY
|
|
||||||
extern uint16_t TFT_MESH;
|
|
||||||
#else
|
|
||||||
uint16_t TFT_MESH = COLOR565(0x67, 0xEA, 0x94);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
|
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
|
||||||
#include "mesh/wifi/WiFiAPClient.h"
|
#include "mesh/wifi/WiFiAPClient.h"
|
||||||
#endif
|
#endif
|
||||||
@@ -109,6 +105,27 @@ namespace graphics
|
|||||||
// A text message frame + debug frame + all the node infos
|
// A text message frame + debug frame + all the node infos
|
||||||
FrameCallback *normalFrames;
|
FrameCallback *normalFrames;
|
||||||
static uint32_t targetFramerate = IDLE_FRAMERATE;
|
static uint32_t targetFramerate = IDLE_FRAMERATE;
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
static inline void prepareFrameColorRegions()
|
||||||
|
{
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
clearTFTColorRegions();
|
||||||
|
// Full-frame FrameMono inversion for themes that need it (e.g. light themes).
|
||||||
|
if (isThemeFullFrameInvert()) {
|
||||||
|
setAndRegisterTFTColorRole(TFTColorRole::FrameMono, getThemeBodyFg(), getThemeBodyBg(), 0, 0, screen->getWidth(),
|
||||||
|
screen->getHeight());
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static inline void updateUiFrame(OLEDDisplayUi *ui)
|
||||||
|
{
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
prepareFrameColorRegions();
|
||||||
|
#endif
|
||||||
|
ui->update();
|
||||||
|
}
|
||||||
// Global variables for alert banner - explicitly define with extern "C" linkage to prevent optimization
|
// Global variables for alert banner - explicitly define with extern "C" linkage to prevent optimization
|
||||||
|
|
||||||
uint32_t logo_timeout = 5000; // 4 seconds for EACH logo
|
uint32_t logo_timeout = 5000; // 4 seconds for EACH logo
|
||||||
@@ -227,7 +244,7 @@ void Screen::showOverlayBanner(BannerOverlayOptions banner_overlay_options)
|
|||||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||||
ui->setTargetFPS(60);
|
ui->setTargetFPS(60);
|
||||||
ui->update();
|
updateUiFrame(ui);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called to trigger a banner with custom message and duration
|
// Called to trigger a banner with custom message and duration
|
||||||
@@ -249,7 +266,7 @@ void Screen::showNodePicker(const char *message, uint32_t durationMs, std::funct
|
|||||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||||
ui->setTargetFPS(60);
|
ui->setTargetFPS(60);
|
||||||
ui->update();
|
updateUiFrame(ui);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called to trigger a banner with custom message and duration
|
// Called to trigger a banner with custom message and duration
|
||||||
@@ -273,7 +290,7 @@ void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t
|
|||||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||||
ui->setTargetFPS(60);
|
ui->setTargetFPS(60);
|
||||||
ui->update();
|
updateUiFrame(ui);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Screen::showTextInput(const char *header, const char *initialText, uint32_t durationMs,
|
void Screen::showTextInput(const char *header, const char *initialText, uint32_t durationMs,
|
||||||
@@ -296,7 +313,7 @@ void Screen::showTextInput(const char *header, const char *initialText, uint32_t
|
|||||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||||
ui->setTargetFPS(60);
|
ui->setTargetFPS(60);
|
||||||
ui->update();
|
updateUiFrame(ui);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||||
@@ -388,30 +405,6 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
|
|||||||
{
|
{
|
||||||
graphics::normalFrames = new FrameCallback[MAX_NUM_NODES + NUM_EXTRA_FRAMES];
|
graphics::normalFrames = new FrameCallback[MAX_NUM_NODES + NUM_EXTRA_FRAMES];
|
||||||
|
|
||||||
int32_t rawRGB = uiconfig.screen_rgb_color;
|
|
||||||
|
|
||||||
// Only validate the combined value once
|
|
||||||
if (rawRGB > 0 && rawRGB <= 255255255) {
|
|
||||||
LOG_INFO("Setting screen RGB color to user chosen: 0x%06X", rawRGB);
|
|
||||||
// Extract each component as a normal int first
|
|
||||||
int r = (rawRGB >> 16) & 0xFF;
|
|
||||||
int g = (rawRGB >> 8) & 0xFF;
|
|
||||||
int b = rawRGB & 0xFF;
|
|
||||||
if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) {
|
|
||||||
TFT_MESH = COLOR565(static_cast<uint8_t>(r), static_cast<uint8_t>(g), static_cast<uint8_t>(b));
|
|
||||||
}
|
|
||||||
#ifdef TFT_MESH_OVERRIDE
|
|
||||||
} else if (rawRGB == 0) {
|
|
||||||
LOG_INFO("Setting screen RGB color to TFT_MESH_OVERRIDE: 0x%04X", TFT_MESH_OVERRIDE);
|
|
||||||
// Default to TFT_MESH_OVERRIDE if available
|
|
||||||
TFT_MESH = TFT_MESH_OVERRIDE;
|
|
||||||
#endif
|
|
||||||
} else {
|
|
||||||
// Default best readable yellow color
|
|
||||||
LOG_INFO("Setting screen RGB color to default: (255,255,128)");
|
|
||||||
TFT_MESH = COLOR565(255, 255, 128);
|
|
||||||
}
|
|
||||||
|
|
||||||
#if defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SH1107_128_64)
|
#if defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SH1107_128_64)
|
||||||
dispdev = new SH1106Wire(address.address, -1, -1, geometry,
|
dispdev = new SH1106Wire(address.address, -1, -1, geometry,
|
||||||
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
||||||
@@ -474,9 +467,13 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined(USE_ST7789)
|
#if defined(USE_ST7789)
|
||||||
static_cast<ST7789Spi *>(dispdev)->setRGB(TFT_MESH);
|
// Keep firmware and ST7789 driver region structs layout-compatible:
|
||||||
|
// we pass `graphics::colorRegions` through a type cast below.
|
||||||
|
static_assert(sizeof(graphics::TFTColorRegion) == sizeof(::TFTColorRegion),
|
||||||
|
"graphics::TFTColorRegion layout must match ST7789 TFTColorRegion");
|
||||||
|
static_cast<ST7789Spi *>(dispdev)->setRGB(TFTPalette::White, (::TFTColorRegion *)colorRegions);
|
||||||
#elif defined(USE_ST7796)
|
#elif defined(USE_ST7796)
|
||||||
static_cast<ST7796Spi *>(dispdev)->setRGB(TFT_MESH);
|
static_cast<ST7796Spi *>(dispdev)->setRGB(TFTPalette::White);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
ui = new OLEDDisplayUi(dispdev);
|
ui = new OLEDDisplayUi(dispdev);
|
||||||
@@ -663,16 +660,16 @@ void Screen::setup()
|
|||||||
static_cast<SH1106Wire *>(dispdev)->setSubtype(7);
|
static_cast<SH1106Wire *>(dispdev)->setSubtype(7);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined(USE_ST7789) && defined(TFT_MESH)
|
#if defined(USE_ST7789)
|
||||||
// Apply custom RGB color (e.g. Heltec T114/T190)
|
static_assert(sizeof(graphics::TFTColorRegion) == sizeof(::TFTColorRegion),
|
||||||
static_cast<ST7789Spi *>(dispdev)->setRGB(TFT_MESH);
|
"graphics::TFTColorRegion layout must match ST7789 TFTColorRegion");
|
||||||
|
static_cast<ST7789Spi *>(dispdev)->setRGB(TFTPalette::White, (::TFTColorRegion *)colorRegions);
|
||||||
#endif
|
#endif
|
||||||
#if defined(MUZI_BASE)
|
#if defined(MUZI_BASE)
|
||||||
dispdev->delayPoweron = true;
|
dispdev->delayPoweron = true;
|
||||||
#endif
|
#endif
|
||||||
#if defined(USE_ST7796) && defined(TFT_MESH)
|
#if defined(USE_ST7796)
|
||||||
// Custom text color, if defined in variant.h
|
static_cast<ST7796Spi *>(dispdev)->setRGB(TFTPalette::White);
|
||||||
static_cast<ST7796Spi *>(dispdev)->setRGB(TFT_MESH);
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Initialize display and UI system
|
// Initialize display and UI system
|
||||||
@@ -718,7 +715,7 @@ void Screen::setup()
|
|||||||
#endif
|
#endif
|
||||||
{
|
{
|
||||||
const char *region = myRegion ? myRegion->name : nullptr;
|
const char *region = myRegion ? myRegion->name : nullptr;
|
||||||
graphics::UIRenderer::drawIconScreen(region, display, state, x, y);
|
graphics::UIRenderer::drawBootIconScreen(region, display, state, x, y);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
ui->setFrames(alertFrames, 1);
|
ui->setFrames(alertFrames, 1);
|
||||||
@@ -757,9 +754,9 @@ void Screen::setup()
|
|||||||
// Turn on display and trigger first draw
|
// Turn on display and trigger first draw
|
||||||
handleSetOn(true);
|
handleSetOn(true);
|
||||||
graphics::currentResolution = graphics::determineScreenResolution(dispdev->height(), dispdev->width());
|
graphics::currentResolution = graphics::determineScreenResolution(dispdev->height(), dispdev->width());
|
||||||
ui->update();
|
updateUiFrame(ui);
|
||||||
#ifndef USE_EINK
|
#ifndef USE_EINK
|
||||||
ui->update(); // Some SSD1306 clones drop the first draw, so run twice
|
updateUiFrame(ui); // Some SSD1306 clones drop the first draw, so run twice
|
||||||
#endif
|
#endif
|
||||||
serialSinceMsec = millis();
|
serialSinceMsec = millis();
|
||||||
|
|
||||||
@@ -832,7 +829,7 @@ void Screen::forceDisplay(bool forceUiUpdate)
|
|||||||
do {
|
do {
|
||||||
startUpdate = millis(); // Handle impossibly unlikely corner case of a millis() overflow..
|
startUpdate = millis(); // Handle impossibly unlikely corner case of a millis() overflow..
|
||||||
delay(10);
|
delay(10);
|
||||||
ui->update();
|
updateUiFrame(ui);
|
||||||
} while (ui->getUiState()->lastUpdate < startUpdate);
|
} while (ui->getUiState()->lastUpdate < startUpdate);
|
||||||
|
|
||||||
// Return to normal frame rate
|
// Return to normal frame rate
|
||||||
@@ -903,9 +900,9 @@ int32_t Screen::runOnce()
|
|||||||
static FrameCallback bootOEMFrames[] = {graphics::UIRenderer::drawOEMBootScreen};
|
static FrameCallback bootOEMFrames[] = {graphics::UIRenderer::drawOEMBootScreen};
|
||||||
static const int bootOEMFrameCount = sizeof(bootOEMFrames) / sizeof(bootOEMFrames[0]);
|
static const int bootOEMFrameCount = sizeof(bootOEMFrames) / sizeof(bootOEMFrames[0]);
|
||||||
ui->setFrames(bootOEMFrames, bootOEMFrameCount);
|
ui->setFrames(bootOEMFrames, bootOEMFrameCount);
|
||||||
ui->update();
|
updateUiFrame(ui);
|
||||||
#ifndef USE_EINK
|
#ifndef USE_EINK
|
||||||
ui->update();
|
updateUiFrame(ui);
|
||||||
#endif
|
#endif
|
||||||
showingOEMBootScreen = false;
|
showingOEMBootScreen = false;
|
||||||
}
|
}
|
||||||
@@ -996,7 +993,7 @@ int32_t Screen::runOnce()
|
|||||||
|
|
||||||
// this must be before the frameState == FIXED check, because we always
|
// this must be before the frameState == FIXED check, because we always
|
||||||
// want to draw at least one FIXED frame before doing forceDisplay
|
// want to draw at least one FIXED frame before doing forceDisplay
|
||||||
ui->update();
|
updateUiFrame(ui);
|
||||||
|
|
||||||
// Switch to a low framerate (to save CPU) when we are not in transition
|
// Switch to a low framerate (to save CPU) when we are not in transition
|
||||||
// but we should only call setTargetFPS when framestate changes, because
|
// but we should only call setTargetFPS when framestate changes, because
|
||||||
@@ -1058,7 +1055,7 @@ void Screen::setSSLFrames()
|
|||||||
// LOG_DEBUG("Show SSL frames");
|
// LOG_DEBUG("Show SSL frames");
|
||||||
static FrameCallback sslFrames[] = {NotificationRenderer::drawSSLScreen};
|
static FrameCallback sslFrames[] = {NotificationRenderer::drawSSLScreen};
|
||||||
ui->setFrames(sslFrames, 1);
|
ui->setFrames(sslFrames, 1);
|
||||||
ui->update();
|
updateUiFrame(ui);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1094,7 +1091,7 @@ void Screen::setScreensaverFrames(FrameCallback einkScreensaver)
|
|||||||
do {
|
do {
|
||||||
startUpdate = millis(); // Handle impossibly unlikely corner case of a millis() overflow..
|
startUpdate = millis(); // Handle impossibly unlikely corner case of a millis() overflow..
|
||||||
delay(1);
|
delay(1);
|
||||||
ui->update();
|
updateUiFrame(ui);
|
||||||
} while (ui->getUiState()->lastUpdate < startUpdate);
|
} while (ui->getUiState()->lastUpdate < startUpdate);
|
||||||
|
|
||||||
#if defined(USE_EINK_PARALLELDISPLAY)
|
#if defined(USE_EINK_PARALLELDISPLAY)
|
||||||
@@ -1469,9 +1466,15 @@ void Screen::blink()
|
|||||||
dispdev->setBrightness(254);
|
dispdev->setBrightness(254);
|
||||||
while (count > 0) {
|
while (count > 0) {
|
||||||
dispdev->fillRect(0, 0, dispdev->getWidth(), dispdev->getHeight());
|
dispdev->fillRect(0, 0, dispdev->getWidth(), dispdev->getHeight());
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
prepareFrameColorRegions();
|
||||||
|
#endif
|
||||||
dispdev->display();
|
dispdev->display();
|
||||||
delay(50);
|
delay(50);
|
||||||
dispdev->clear();
|
dispdev->clear();
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
prepareFrameColorRegions();
|
||||||
|
#endif
|
||||||
dispdev->display();
|
dispdev->display();
|
||||||
delay(50);
|
delay(50);
|
||||||
count = count - 1;
|
count = count - 1;
|
||||||
@@ -1605,6 +1608,9 @@ void Screen::setFastFramerate()
|
|||||||
{
|
{
|
||||||
#if defined(M5STACK_UNITC6L)
|
#if defined(M5STACK_UNITC6L)
|
||||||
dispdev->clear();
|
dispdev->clear();
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
prepareFrameColorRegions();
|
||||||
|
#endif
|
||||||
dispdev->display();
|
dispdev->display();
|
||||||
#endif
|
#endif
|
||||||
// We are about to start a transition so speed up fps
|
// We are about to start a transition so speed up fps
|
||||||
@@ -1816,7 +1822,7 @@ int Screen::handleInputEvent(const InputEvent *event)
|
|||||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||||
setFastFramerate(); // Draw ASAP
|
setFastFramerate(); // Draw ASAP
|
||||||
ui->update();
|
updateUiFrame(ui);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1831,7 +1837,7 @@ int Screen::handleInputEvent(const InputEvent *event)
|
|||||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||||
setFastFramerate(); // Draw ASAP
|
setFastFramerate(); // Draw ASAP
|
||||||
ui->update();
|
updateUiFrame(ui);
|
||||||
|
|
||||||
menuHandler::handleMenuSwitch(dispdev);
|
menuHandler::handleMenuSwitch(dispdev);
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
#if HAS_SCREEN
|
#if HAS_SCREEN
|
||||||
#include "MeshService.h"
|
#include "MeshService.h"
|
||||||
|
#include "NodeDB.h"
|
||||||
#include "RTC.h"
|
#include "RTC.h"
|
||||||
#include "draw/NodeListRenderer.h"
|
#include "draw/NodeListRenderer.h"
|
||||||
#include "graphics/ScreenFonts.h"
|
#include "graphics/ScreenFonts.h"
|
||||||
#include "graphics/SharedUIDisplay.h"
|
#include "graphics/SharedUIDisplay.h"
|
||||||
|
#include "graphics/TFTColorRegions.h"
|
||||||
|
#include "graphics/TFTPalette.h"
|
||||||
#include "graphics/draw/UIRenderer.h"
|
#include "graphics/draw/UIRenderer.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "meshtastic/config.pb.h"
|
#include "meshtastic/config.pb.h"
|
||||||
#include "modules/ExternalNotificationModule.h"
|
#include "modules/ExternalNotificationModule.h"
|
||||||
#include "power.h"
|
#include "power.h"
|
||||||
#include <OLEDDisplay.h>
|
#include <OLEDDisplay.h>
|
||||||
|
#include <cctype>
|
||||||
#include <graphics/images.h>
|
#include <graphics/images.h>
|
||||||
|
|
||||||
namespace graphics
|
namespace graphics
|
||||||
@@ -65,6 +69,12 @@ uint32_t lastBlinkShared = 0;
|
|||||||
bool isMailIconVisible = true;
|
bool isMailIconVisible = true;
|
||||||
uint32_t lastMailBlink = 0;
|
uint32_t lastMailBlink = 0;
|
||||||
|
|
||||||
|
static inline bool useClockHeaderAccentTheme(uint32_t themeId)
|
||||||
|
{
|
||||||
|
return themeId == ThemeID::Pink || themeId == ThemeID::Creamsicle || themeId == ThemeID::MeshtasticGreen ||
|
||||||
|
themeId == ThemeID::ClassicRed || themeId == ThemeID::MonochromeWhite;
|
||||||
|
}
|
||||||
|
|
||||||
// *********************************
|
// *********************************
|
||||||
// * Rounded Header when inverted *
|
// * Rounded Header when inverted *
|
||||||
// *********************************
|
// *********************************
|
||||||
@@ -85,7 +95,8 @@ void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w,
|
|||||||
// *************************
|
// *************************
|
||||||
// * Common Header Drawing *
|
// * Common Header Drawing *
|
||||||
// *************************
|
// *************************
|
||||||
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert, bool show_date)
|
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert, bool show_date,
|
||||||
|
bool transparent_background, bool use_title_color_override, uint16_t title_color_override)
|
||||||
{
|
{
|
||||||
constexpr int HEADER_OFFSET_Y = 1;
|
constexpr int HEADER_OFFSET_Y = 1;
|
||||||
y += HEADER_OFFSET_Y;
|
y += HEADER_OFFSET_Y;
|
||||||
@@ -100,30 +111,93 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
|||||||
|
|
||||||
const int screenW = display->getWidth();
|
const int screenW = display->getWidth();
|
||||||
const int screenH = display->getHeight();
|
const int screenH = display->getHeight();
|
||||||
|
const int headerHeight = highlightHeight + 2;
|
||||||
|
const uint16_t headerColorForRoles = getThemeHeaderBg();
|
||||||
|
// Color TFT headers use a fixed dark background + white glyphs.
|
||||||
|
// Keep legacy inverted bitmap behavior only for monochrome displays.
|
||||||
|
const bool useInvertedHeaderStyle = (isInverted && !force_no_invert && !isTFTColoringEnabled() && !transparent_background);
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
int statusLeftEndX = 0;
|
||||||
|
int statusRightStartX = screenW;
|
||||||
|
const bool isClockHeader = transparent_background && show_date && (!titleStr || titleStr[0] == '\0');
|
||||||
|
const auto activeThemeId = getActiveTheme().id;
|
||||||
|
const bool useClockHeaderAccent = isClockHeader && useClockHeaderAccentTheme(activeThemeId);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
{
|
||||||
|
const uint16_t headerColor = getThemeHeaderBg();
|
||||||
|
const uint16_t headerTextColor = getThemeHeaderText();
|
||||||
|
const uint16_t headerTitleColorForRole = use_title_color_override ? title_color_override : headerTextColor;
|
||||||
|
uint16_t headerStatusColor = getThemeHeaderStatus();
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
// Clock frame uses transparent header + date + empty title.
|
||||||
|
// For accent clock themes (Pink/Creamsicle + classic monochrome), tint
|
||||||
|
// status items (battery outline, %, date, mail icon) to the header accent.
|
||||||
|
if (useClockHeaderAccent) {
|
||||||
|
headerStatusColor = getThemeHeaderBg();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transparent_background) {
|
||||||
|
// Transparent clock headers should inherit whatever body off-color is
|
||||||
|
// already active under the header (important for light/inverted themes).
|
||||||
|
const uint16_t transparentBgColor = resolveTFTOffColorAt(0, headerHeight + 1, getThemeBodyBg());
|
||||||
|
setAndRegisterTFTColorRole(TFTColorRole::HeaderBackground, transparentBgColor, transparentBgColor, 0, 0, screenW,
|
||||||
|
headerHeight);
|
||||||
|
setTFTColorRole(TFTColorRole::HeaderTitle, headerTitleColorForRole, transparentBgColor);
|
||||||
|
setTFTColorRole(TFTColorRole::HeaderStatus, headerStatusColor, transparentBgColor);
|
||||||
|
} else if (useInvertedHeaderStyle) {
|
||||||
|
setAndRegisterTFTColorRole(TFTColorRole::HeaderBackground, headerColor, TFTPalette::Black, 0, 0, screenW,
|
||||||
|
headerHeight);
|
||||||
|
setTFTColorRole(TFTColorRole::HeaderTitle, headerColor, headerTitleColorForRole);
|
||||||
|
setTFTColorRole(TFTColorRole::HeaderStatus, headerColor, headerStatusColor);
|
||||||
|
} else {
|
||||||
|
setAndRegisterTFTColorRole(TFTColorRole::HeaderBackground, TFTPalette::Black, headerColor, 0, 0, screenW,
|
||||||
|
headerHeight);
|
||||||
|
setTFTColorRole(TFTColorRole::HeaderTitle, headerTitleColorForRole, headerColor);
|
||||||
|
setTFTColorRole(TFTColorRole::HeaderStatus, headerStatusColor, headerColor);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
if (!force_no_invert) {
|
|
||||||
// === Inverted Header Background ===
|
// === Inverted Header Background ===
|
||||||
if (isInverted) {
|
if (useInvertedHeaderStyle) {
|
||||||
display->setColor(BLACK);
|
display->setColor(BLACK);
|
||||||
display->fillRect(0, 0, screenW, highlightHeight + 2);
|
display->fillRect(0, 0, screenW, headerHeight);
|
||||||
display->setColor(WHITE);
|
display->setColor(WHITE);
|
||||||
drawRoundedHighlight(display, x, y, screenW, highlightHeight, 2);
|
drawRoundedHighlight(display, x, y, screenW, highlightHeight, 2);
|
||||||
display->setColor(BLACK);
|
display->setColor(BLACK);
|
||||||
} else {
|
} else {
|
||||||
display->setColor(BLACK);
|
display->setColor(BLACK);
|
||||||
display->fillRect(0, 0, screenW, highlightHeight + 2);
|
display->fillRect(0, 0, screenW, headerHeight);
|
||||||
display->setColor(WHITE);
|
// Keep the legacy white separator for monochrome displays only when header background is visible.
|
||||||
if (currentResolution == ScreenResolution::High) {
|
#if !GRAPHICS_TFT_COLORING_ENABLED
|
||||||
display->drawLine(0, 20, screenW, 20);
|
if (!transparent_background) {
|
||||||
} else {
|
display->setColor(WHITE);
|
||||||
display->drawLine(0, 14, screenW, 14);
|
if (currentResolution == ScreenResolution::High) {
|
||||||
|
display->drawLine(0, 20, screenW, 20);
|
||||||
|
} else {
|
||||||
|
display->drawLine(0, 14, screenW, 14);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (transparent_background) {
|
||||||
|
display->setColor(WHITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
// TFT role coloring expects foreground glyph bits to be "set".
|
||||||
|
display->setColor(WHITE);
|
||||||
|
#endif
|
||||||
|
|
||||||
// === Screen Title ===
|
// === Screen Title ===
|
||||||
const char *headerTitle = titleStr ? titleStr : "";
|
const char *headerTitle = titleStr ? titleStr : "";
|
||||||
const int titleWidth = UIRenderer::measureStringWithEmotes(display, headerTitle);
|
const int titleWidth = UIRenderer::measureStringWithEmotes(display, headerTitle);
|
||||||
const int titleX = (SCREEN_WIDTH - titleWidth) / 2;
|
const int titleX = (SCREEN_WIDTH - titleWidth) / 2;
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
const int titleRegionWidth = titleWidth + (config.display.heading_bold ? 3 : 2);
|
||||||
|
registerTFTColorRegion(TFTColorRole::HeaderTitle, titleX - 1, y, titleRegionWidth, FONT_HEIGHT_SMALL);
|
||||||
|
#endif
|
||||||
UIRenderer::drawStringWithEmotes(display, titleX, y, headerTitle, FONT_HEIGHT_SMALL, 1, config.display.heading_bold);
|
UIRenderer::drawStringWithEmotes(display, titleX, y, headerTitle, FONT_HEIGHT_SMALL, 1, config.display.heading_bold);
|
||||||
}
|
}
|
||||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
@@ -152,6 +226,17 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
|||||||
|
|
||||||
bool useHorizontalBattery = (currentResolution == ScreenResolution::High && screenW >= screenH);
|
bool useHorizontalBattery = (currentResolution == ScreenResolution::High && screenW >= screenH);
|
||||||
const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2;
|
const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2;
|
||||||
|
bool hasBatteryFillRegion = false;
|
||||||
|
int16_t batteryFillRegionX = 0;
|
||||||
|
int16_t batteryFillRegionY = 0;
|
||||||
|
int16_t batteryFillRegionW = 0;
|
||||||
|
int16_t batteryFillRegionH = 0;
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
uint16_t batteryFillColor = getThemeBatteryFillColor(chargePercent);
|
||||||
|
if (useClockHeaderAccent) {
|
||||||
|
batteryFillColor = getThemeHeaderBg();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
int batteryX = 1;
|
int batteryX = 1;
|
||||||
int batteryY = HEADER_OFFSET_Y + 1;
|
int batteryY = HEADER_OFFSET_Y + 1;
|
||||||
@@ -180,6 +265,15 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
|||||||
display->drawLine(batteryX + 5, batteryY + 12, batteryX + 10, batteryY + 12);
|
display->drawLine(batteryX + 5, batteryY + 12, batteryX + 10, batteryY + 12);
|
||||||
int fillWidth = 14 * chargePercent / 100;
|
int fillWidth = 14 * chargePercent / 100;
|
||||||
display->fillRect(batteryX + 1, batteryY + 1, fillWidth, 11);
|
display->fillRect(batteryX + 1, batteryY + 1, fillWidth, 11);
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
if (fillWidth > 0) {
|
||||||
|
hasBatteryFillRegion = true;
|
||||||
|
batteryFillRegionX = batteryX + 1;
|
||||||
|
batteryFillRegionY = batteryY + 1;
|
||||||
|
batteryFillRegionW = fillWidth;
|
||||||
|
batteryFillRegionH = 11;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
batteryX += 18; // Icon + 2 pixels
|
batteryX += 18; // Icon + 2 pixels
|
||||||
} else {
|
} else {
|
||||||
@@ -194,21 +288,41 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
|||||||
int fillHeight = 8 * chargePercent / 100;
|
int fillHeight = 8 * chargePercent / 100;
|
||||||
int fillY = batteryY - fillHeight;
|
int fillY = batteryY - fillHeight;
|
||||||
display->fillRect(batteryX + 1, fillY + 10, 5, fillHeight);
|
display->fillRect(batteryX + 1, fillY + 10, 5, fillHeight);
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
if (fillHeight > 0) {
|
||||||
|
hasBatteryFillRegion = true;
|
||||||
|
batteryFillRegionX = batteryX + 1;
|
||||||
|
batteryFillRegionY = fillY + 10;
|
||||||
|
batteryFillRegionW = 5;
|
||||||
|
batteryFillRegionH = fillHeight;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
batteryX += 9; // Icon + 2 pixels
|
batteryX += 9; // Icon + 2 pixels
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
statusLeftEndX = batteryX + 2;
|
||||||
|
#endif
|
||||||
|
|
||||||
if (chargePercent != 101) {
|
if (chargePercent != 101) {
|
||||||
// === Battery % Display ===
|
// === Battery % Display ===
|
||||||
char chargeStr[4];
|
char chargeStr[4];
|
||||||
snprintf(chargeStr, sizeof(chargeStr), "%d", chargePercent);
|
snprintf(chargeStr, sizeof(chargeStr), "%d", chargePercent);
|
||||||
int chargeNumWidth = display->getStringWidth(chargeStr);
|
int chargeNumWidth = display->getStringWidth(chargeStr);
|
||||||
|
const int percentWidth = display->getStringWidth("%");
|
||||||
|
const int percentX = batteryX + chargeNumWidth - 1;
|
||||||
display->drawString(batteryX, textY, chargeStr);
|
display->drawString(batteryX, textY, chargeStr);
|
||||||
display->drawString(batteryX + chargeNumWidth - 1, textY, "%");
|
display->drawString(percentX, textY, "%");
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
statusLeftEndX = percentX + percentWidth + 2;
|
||||||
|
#endif
|
||||||
if (isBold) {
|
if (isBold) {
|
||||||
display->drawString(batteryX + 1, textY, chargeStr);
|
display->drawString(batteryX + 1, textY, chargeStr);
|
||||||
display->drawString(batteryX + chargeNumWidth, textY, "%");
|
display->drawString(percentX + 1, textY, "%");
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
statusLeftEndX = percentX + percentWidth + 3;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,6 +367,9 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
|||||||
timeStrWidth = display->getStringWidth(timeStr);
|
timeStrWidth = display->getStringWidth(timeStr);
|
||||||
}
|
}
|
||||||
timeX = screenW - xOffset - timeStrWidth + 3;
|
timeX = screenW - xOffset - timeStrWidth + 3;
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
statusRightStartX = timeX - (useHorizontalBattery ? 22 : 16);
|
||||||
|
#endif
|
||||||
|
|
||||||
// === Show Mail or Mute Icon to the Left of Time ===
|
// === Show Mail or Mute Icon to the Left of Time ===
|
||||||
int iconRightEdge = timeX - 2;
|
int iconRightEdge = timeX - 2;
|
||||||
@@ -278,7 +395,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
|||||||
int iconW = 16, iconH = 12;
|
int iconW = 16, iconH = 12;
|
||||||
int iconX = iconRightEdge - iconW;
|
int iconX = iconRightEdge - iconW;
|
||||||
int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1;
|
int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1;
|
||||||
if (isInverted && !force_no_invert) {
|
if (useInvertedHeaderStyle) {
|
||||||
display->setColor(WHITE);
|
display->setColor(WHITE);
|
||||||
display->fillRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2);
|
display->fillRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2);
|
||||||
display->setColor(BLACK);
|
display->setColor(BLACK);
|
||||||
@@ -293,7 +410,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
|||||||
} else {
|
} else {
|
||||||
int iconX = iconRightEdge - (mail_width - 2);
|
int iconX = iconRightEdge - (mail_width - 2);
|
||||||
int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2;
|
int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2;
|
||||||
if (isInverted && !force_no_invert) {
|
if (useInvertedHeaderStyle) {
|
||||||
display->setColor(WHITE);
|
display->setColor(WHITE);
|
||||||
display->fillRect(iconX - 1, iconY - 1, mail_width + 2, mail_height + 2);
|
display->fillRect(iconX - 1, iconY - 1, mail_width + 2, mail_height + 2);
|
||||||
display->setColor(BLACK);
|
display->setColor(BLACK);
|
||||||
@@ -309,7 +426,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
|||||||
int iconX = iconRightEdge - mute_symbol_big_width;
|
int iconX = iconRightEdge - mute_symbol_big_width;
|
||||||
int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2;
|
int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2;
|
||||||
|
|
||||||
if (isInverted && !force_no_invert) {
|
if (useInvertedHeaderStyle) {
|
||||||
display->setColor(WHITE);
|
display->setColor(WHITE);
|
||||||
display->fillRect(iconX - 1, iconY - 1, mute_symbol_big_width + 2, mute_symbol_big_height + 2);
|
display->fillRect(iconX - 1, iconY - 1, mute_symbol_big_width + 2, mute_symbol_big_height + 2);
|
||||||
display->setColor(BLACK);
|
display->setColor(BLACK);
|
||||||
@@ -323,7 +440,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
|||||||
int iconX = iconRightEdge - mute_symbol_width;
|
int iconX = iconRightEdge - mute_symbol_width;
|
||||||
int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2;
|
int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2;
|
||||||
|
|
||||||
if (isInverted && !force_no_invert) {
|
if (useInvertedHeaderStyle) {
|
||||||
display->setColor(WHITE);
|
display->setColor(WHITE);
|
||||||
display->fillRect(iconX - 1, iconY - 1, mute_symbol_width + 2, mute_symbol_height + 2);
|
display->fillRect(iconX - 1, iconY - 1, mute_symbol_width + 2, mute_symbol_height + 2);
|
||||||
display->setColor(BLACK);
|
display->setColor(BLACK);
|
||||||
@@ -351,7 +468,9 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
|||||||
} else {
|
} else {
|
||||||
// === No Time Available: Mail/Mute Icon Moves to Far Right ===
|
// === No Time Available: Mail/Mute Icon Moves to Far Right ===
|
||||||
int iconRightEdge = screenW - xOffset;
|
int iconRightEdge = screenW - xOffset;
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
statusRightStartX = screenW - (useHorizontalBattery ? 22 : 12);
|
||||||
|
#endif
|
||||||
bool showMail = false;
|
bool showMail = false;
|
||||||
|
|
||||||
#ifndef USE_EINK
|
#ifndef USE_EINK
|
||||||
@@ -393,6 +512,16 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
registerTFTColorRegion(TFTColorRole::HeaderStatus, 0, 0, statusLeftEndX, headerHeight);
|
||||||
|
if (statusRightStartX < screenW) {
|
||||||
|
registerTFTColorRegion(TFTColorRole::HeaderStatus, statusRightStartX, 0, screenW - statusRightStartX, headerHeight);
|
||||||
|
}
|
||||||
|
if (hasBatteryFillRegion) {
|
||||||
|
registerTFTColorRegionDirect(batteryFillRegionX, batteryFillRegionY, batteryFillRegionW, batteryFillRegionH,
|
||||||
|
batteryFillColor, headerColorForRoles);
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
display->setColor(WHITE); // Reset for other UI
|
display->setColor(WHITE); // Reset for other UI
|
||||||
}
|
}
|
||||||
@@ -430,14 +559,23 @@ void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y)
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
const int scale = (currentResolution == ScreenResolution::High) ? 2 : 1;
|
const int scale = (currentResolution == ScreenResolution::High) ? 2 : 1;
|
||||||
|
const int footerY = SCREEN_HEIGHT - (1 * scale) - (connection_icon_height * scale);
|
||||||
|
const int footerH = (connection_icon_height * scale) + (2 * scale);
|
||||||
|
const int iconX = 0;
|
||||||
|
const int iconY = SCREEN_HEIGHT - (connection_icon_height * scale);
|
||||||
|
const int iconW = connection_icon_width * scale;
|
||||||
|
const int iconH = connection_icon_height * scale;
|
||||||
|
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
// Only tint the link glyph itself on TFT; keep the footer background black.
|
||||||
|
setAndRegisterTFTColorRole(TFTColorRole::ConnectionIcon, TFTPalette::Blue, TFTPalette::Black, iconX, iconY, iconW, iconH);
|
||||||
|
#endif
|
||||||
|
|
||||||
display->setColor(BLACK);
|
display->setColor(BLACK);
|
||||||
display->fillRect(0, SCREEN_HEIGHT - (1 * scale) - (connection_icon_height * scale), (connection_icon_width * scale),
|
display->fillRect(0, footerY, SCREEN_WIDTH, footerH);
|
||||||
(connection_icon_height * scale) + (2 * scale));
|
|
||||||
display->setColor(WHITE);
|
display->setColor(WHITE);
|
||||||
if (currentResolution == ScreenResolution::High) {
|
if (currentResolution == ScreenResolution::High) {
|
||||||
const int bytesPerRow = (connection_icon_width + 7) / 8;
|
const int bytesPerRow = (connection_icon_width + 7) / 8;
|
||||||
int iconX = 0;
|
|
||||||
int iconY = SCREEN_HEIGHT - (connection_icon_height * 2);
|
|
||||||
|
|
||||||
for (int yy = 0; yy < connection_icon_height; ++yy) {
|
for (int yy = 0; yy < connection_icon_height; ++yy) {
|
||||||
const uint8_t *rowPtr = connection_icon + yy * bytesPerRow;
|
const uint8_t *rowPtr = connection_icon + yy * bytesPerRow;
|
||||||
@@ -451,65 +589,127 @@ void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y)
|
|||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
display->drawXbm(0, SCREEN_HEIGHT - connection_icon_height, connection_icon_width, connection_icon_height,
|
display->drawXbm(iconX, iconY, connection_icon_width, connection_icon_height, connection_icon);
|
||||||
connection_icon);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isAllowedPunctuation(char c)
|
bool isAllowedPunctuation(char c)
|
||||||
{
|
{
|
||||||
const std::string allowed = ".,!?;:-_()[]{}'\"@#$/\\&+=%~^ ";
|
switch (c) {
|
||||||
return allowed.find(c) != std::string::npos;
|
case '.':
|
||||||
|
case ',':
|
||||||
|
case '!':
|
||||||
|
case '?':
|
||||||
|
case ';':
|
||||||
|
case ':':
|
||||||
|
case '-':
|
||||||
|
case '_':
|
||||||
|
case '(':
|
||||||
|
case ')':
|
||||||
|
case '[':
|
||||||
|
case ']':
|
||||||
|
case '{':
|
||||||
|
case '}':
|
||||||
|
case '\'':
|
||||||
|
case '"':
|
||||||
|
case '@':
|
||||||
|
case '#':
|
||||||
|
case '$':
|
||||||
|
case '/':
|
||||||
|
case '\\':
|
||||||
|
case '&':
|
||||||
|
case '+':
|
||||||
|
case '=':
|
||||||
|
case '%':
|
||||||
|
case '~':
|
||||||
|
case '^':
|
||||||
|
case ' ':
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void replaceAll(std::string &s, const std::string &from, const std::string &to)
|
static inline size_t utf8CodePointLength(unsigned char lead)
|
||||||
{
|
{
|
||||||
if (from.empty())
|
if ((lead & 0x80) == 0x00) {
|
||||||
return;
|
return 1;
|
||||||
size_t pos = 0;
|
|
||||||
while ((pos = s.find(from, pos)) != std::string::npos) {
|
|
||||||
s.replace(pos, from.size(), to);
|
|
||||||
pos += to.size();
|
|
||||||
}
|
}
|
||||||
|
if ((lead & 0xE0) == 0xC0) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
if ((lead & 0xF0) == 0xE0) {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
if ((lead & 0xF8) == 0xF0) {
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string sanitizeString(const std::string &input)
|
std::string sanitizeString(const std::string &input)
|
||||||
{
|
{
|
||||||
|
static constexpr char kReplacementChar = static_cast<char>(0xBF); // Inverted question mark in ISO-8859-1.
|
||||||
std::string output;
|
std::string output;
|
||||||
|
output.reserve(input.size());
|
||||||
bool inReplacement = false;
|
bool inReplacement = false;
|
||||||
|
const size_t inputSize = input.size();
|
||||||
// Make a mutable copy so we can normalize UTF-8 “smart punctuation” into ASCII first.
|
size_t i = 0;
|
||||||
std::string s = input;
|
while (i < inputSize) {
|
||||||
|
const unsigned char byte0 = static_cast<unsigned char>(input[i]);
|
||||||
// Curly single quotes: ‘ ’
|
char normalized = '\0';
|
||||||
replaceAll(s, "\xE2\x80\x98", "'"); // U+2018
|
size_t consumed = 0;
|
||||||
replaceAll(s, "\xE2\x80\x99", "'"); // U+2019
|
if (byte0 < 0x80) {
|
||||||
|
normalized = static_cast<char>(byte0);
|
||||||
// Curly double quotes: “ ”
|
consumed = 1;
|
||||||
replaceAll(s, "\xE2\x80\x9C", "\""); // U+201C
|
} else if ((i + 2) < inputSize && byte0 == 0xE2 && static_cast<unsigned char>(input[i + 1]) == 0x80) {
|
||||||
replaceAll(s, "\xE2\x80\x9D", "\""); // U+201D
|
// Smart punctuation: ' ' \" \" - -
|
||||||
|
switch (static_cast<unsigned char>(input[i + 2])) {
|
||||||
// En dash / Em dash: – —
|
case 0x98:
|
||||||
replaceAll(s, "\xE2\x80\x93", "-"); // U+2013
|
case 0x99:
|
||||||
replaceAll(s, "\xE2\x80\x94", "-"); // U+2014
|
normalized = '\'';
|
||||||
|
consumed = 3;
|
||||||
// Non-breaking space
|
break;
|
||||||
replaceAll(s, "\xC2\xA0", " "); // U+00A0
|
case 0x9C:
|
||||||
|
case 0x9D:
|
||||||
// Now do your original sanitize pass over the normalized string.
|
normalized = '\"';
|
||||||
for (unsigned char uc : s) {
|
consumed = 3;
|
||||||
char c = static_cast<char>(uc);
|
break;
|
||||||
if (std::isalnum(uc) || isAllowedPunctuation(c)) {
|
case 0x93:
|
||||||
output += c;
|
case 0x94:
|
||||||
inReplacement = false;
|
normalized = '-';
|
||||||
} else {
|
consumed = 3;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if ((i + 1) < inputSize && byte0 == 0xC2 && static_cast<unsigned char>(input[i + 1]) == 0xA0) {
|
||||||
|
// Non-breaking space.
|
||||||
|
normalized = ' ';
|
||||||
|
consumed = 2;
|
||||||
|
}
|
||||||
|
if (consumed == 0) {
|
||||||
|
size_t seqLen = utf8CodePointLength(byte0);
|
||||||
|
if (seqLen > (inputSize - i)) {
|
||||||
|
seqLen = 1;
|
||||||
|
}
|
||||||
if (!inReplacement) {
|
if (!inReplacement) {
|
||||||
output += static_cast<char>(0xBF); // ISO-8859-1 for inverted question mark
|
output.push_back(kReplacementChar);
|
||||||
inReplacement = true;
|
inReplacement = true;
|
||||||
}
|
}
|
||||||
|
i += seqLen;
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
const unsigned char normalizedUc = static_cast<unsigned char>(normalized);
|
||||||
|
if (std::isalnum(normalizedUc) || isAllowedPunctuation(normalized)) {
|
||||||
|
output.push_back(normalized);
|
||||||
|
inReplacement = false;
|
||||||
|
} else if (!inReplacement) {
|
||||||
|
output.push_back(kReplacementChar);
|
||||||
|
inReplacement = true;
|
||||||
|
}
|
||||||
|
i += consumed;
|
||||||
}
|
}
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <OLEDDisplay.h>
|
#include <OLEDDisplay.h>
|
||||||
|
#include <stdint.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
namespace graphics
|
namespace graphics
|
||||||
@@ -52,7 +53,8 @@ void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w,
|
|||||||
|
|
||||||
// Shared battery/time/mail header
|
// Shared battery/time/mail header
|
||||||
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr = "", bool force_no_invert = false,
|
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr = "", bool force_no_invert = false,
|
||||||
bool show_date = false);
|
bool show_date = false, bool transparent_background = false, bool use_title_color_override = false,
|
||||||
|
uint16_t title_color_override = 0);
|
||||||
|
|
||||||
// Shared battery/time/mail header
|
// Shared battery/time/mail header
|
||||||
void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y);
|
void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y);
|
||||||
|
|||||||
@@ -0,0 +1,819 @@
|
|||||||
|
#include "TFTColorRegions.h"
|
||||||
|
#include "NodeDB.h"
|
||||||
|
#include "TFTPalette.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
namespace graphics
|
||||||
|
{
|
||||||
|
TFTColorRegion colorRegions[MAX_TFT_COLOR_REGIONS];
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
struct TFTRoleColorsBe {
|
||||||
|
uint16_t onColorBe;
|
||||||
|
uint16_t offColorBe;
|
||||||
|
};
|
||||||
|
|
||||||
|
static uint8_t colorRegionCount = 0;
|
||||||
|
static constexpr uint32_t kFnv1aOffsetBasis = 2166136261u;
|
||||||
|
static constexpr uint32_t kFnv1aPrime = 16777619u;
|
||||||
|
|
||||||
|
static constexpr uint16_t toBe565(uint16_t color)
|
||||||
|
{
|
||||||
|
return static_cast<uint16_t>((color >> 8) | (color << 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr bool kRoleIsBody[static_cast<size_t>(TFTColorRole::Count)] = {
|
||||||
|
false, // HeaderBackground
|
||||||
|
false, // HeaderTitle
|
||||||
|
false, // HeaderStatus
|
||||||
|
true, // SignalBars
|
||||||
|
true, // ConnectionIcon
|
||||||
|
true, // UtilizationFill
|
||||||
|
true, // FavoriteNode
|
||||||
|
true, // ActionMenuBorder
|
||||||
|
true, // ActionMenuBody
|
||||||
|
true, // ActionMenuTitle
|
||||||
|
true, // FrameMono
|
||||||
|
false, // BootSplash
|
||||||
|
true, // FavoriteNodeBGHighlight
|
||||||
|
false, // NavigationBar
|
||||||
|
false // NavigationArrow
|
||||||
|
};
|
||||||
|
|
||||||
|
static inline bool isBodyColorRole(TFTColorRole role)
|
||||||
|
{
|
||||||
|
return kRoleIsBody[static_cast<size_t>(role)];
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline bool isMonochromeTheme(uint32_t themeId)
|
||||||
|
{
|
||||||
|
return themeId == ThemeID::MeshtasticGreen || themeId == ThemeID::ClassicRed || themeId == ThemeID::MonochromeWhite;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline uint16_t getMonochromeAccent(uint32_t themeId)
|
||||||
|
{
|
||||||
|
return (themeId == ThemeID::MeshtasticGreen) ? TFTPalette::MeshtasticGreen
|
||||||
|
: (themeId == ThemeID::ClassicRed) ? TFTPalette::ClassicRed
|
||||||
|
: TFTPalette::White;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void replaceColor(uint16_t &value, uint16_t from, uint16_t to)
|
||||||
|
{
|
||||||
|
if (value == from) {
|
||||||
|
value = to;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline uint32_t fnv1aAppendByte(uint32_t hash, uint8_t value)
|
||||||
|
{
|
||||||
|
return (hash ^ value) * kFnv1aPrime;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline uint32_t fnv1aAppendU16(uint32_t hash, uint16_t value)
|
||||||
|
{
|
||||||
|
hash = fnv1aAppendByte(hash, static_cast<uint8_t>(value & 0xFF));
|
||||||
|
hash = fnv1aAppendByte(hash, static_cast<uint8_t>((value >> 8) & 0xFF));
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile-time header color overrides (backward-compatible)
|
||||||
|
#ifdef TFT_HEADER_BG_COLOR_OVERRIDE
|
||||||
|
static constexpr uint16_t kHeaderBackground = TFT_HEADER_BG_COLOR_OVERRIDE;
|
||||||
|
#else
|
||||||
|
static constexpr uint16_t kHeaderBackground = TFTPalette::DarkGray;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef TFT_HEADER_TITLE_COLOR_OVERRIDE
|
||||||
|
static constexpr uint16_t kTitleColor = TFT_HEADER_TITLE_COLOR_OVERRIDE;
|
||||||
|
#else
|
||||||
|
static constexpr uint16_t kTitleColor = TFTPalette::White;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef TFT_HEADER_STATUS_COLOR_OVERRIDE
|
||||||
|
static constexpr uint16_t kStatusColor = TFT_HEADER_STATUS_COLOR_OVERRIDE;
|
||||||
|
#else
|
||||||
|
static constexpr uint16_t kStatusColor = TFTPalette::White;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Theme definitions
|
||||||
|
// Stored in kThemes[] and looked up by matching uiconfig.screen_rgb_color
|
||||||
|
// against each entry's .uniqueIdentifier field.
|
||||||
|
|
||||||
|
static const TFTThemeDef kThemes[] = {
|
||||||
|
|
||||||
|
// Default Dark (ThemeID::DefaultDark = 0)
|
||||||
|
{
|
||||||
|
ThemeID::DefaultDark, // id
|
||||||
|
"Default Dark", // name
|
||||||
|
0, // uniqueIdentifier
|
||||||
|
// roles[TFTColorRole::Count]
|
||||||
|
{
|
||||||
|
{kHeaderBackground, TFTPalette::Black}, // HeaderBackground
|
||||||
|
{kHeaderBackground, kTitleColor}, // HeaderTitle
|
||||||
|
{kHeaderBackground, kStatusColor}, // HeaderStatus
|
||||||
|
{TFTPalette::Good, TFTPalette::Black}, // SignalBars
|
||||||
|
{TFTPalette::Blue, TFTPalette::Black}, // ConnectionIcon
|
||||||
|
{TFTPalette::Good, TFTPalette::Black}, // UtilizationFill
|
||||||
|
{TFTPalette::Yellow, TFTPalette::Black}, // FavoriteNode
|
||||||
|
{TFTPalette::DarkGray, TFTPalette::Black}, // ActionMenuBorder
|
||||||
|
{TFTPalette::White, TFTPalette::Black}, // ActionMenuBody
|
||||||
|
{TFTPalette::DarkGray, TFTPalette::White}, // ActionMenuTitle
|
||||||
|
{TFTPalette::Black, TFTPalette::White}, // FrameMono
|
||||||
|
{TFTPalette::White, TFTPalette::Black}, // BootSplash
|
||||||
|
{TFTPalette::Yellow, TFTPalette::Black}, // FavoriteNodeBGHighlight
|
||||||
|
{kStatusColor, kHeaderBackground}, // NavigationBar (icon fg, bar bg)
|
||||||
|
{kTitleColor, TFTPalette::Black}, // NavigationArrow (arrow fg, body bg)
|
||||||
|
},
|
||||||
|
TFTPalette::Good, // batteryFillGood
|
||||||
|
TFTPalette::Medium, // batteryFillMedium
|
||||||
|
TFTPalette::Bad, // batteryFillBad
|
||||||
|
false, // fullFrameInvert
|
||||||
|
true, // visible
|
||||||
|
},
|
||||||
|
|
||||||
|
// Default Light (ThemeID::DefaultLight = 1)
|
||||||
|
{
|
||||||
|
ThemeID::DefaultLight, // id
|
||||||
|
"Default Light", // name
|
||||||
|
1, // uniqueIdentifier
|
||||||
|
{
|
||||||
|
{TFTPalette::LightGray, TFTPalette::Black}, // HeaderBackground
|
||||||
|
{TFTPalette::LightGray, TFTPalette::Black}, // HeaderTitle
|
||||||
|
{TFTPalette::LightGray, TFTPalette::Black}, // HeaderStatus
|
||||||
|
{TFTPalette::Good, TFTPalette::White}, // SignalBars
|
||||||
|
{TFTPalette::Blue, TFTPalette::White}, // ConnectionIcon
|
||||||
|
{TFTPalette::Good, TFTPalette::White}, // UtilizationFill
|
||||||
|
{TFTPalette::Black, TFTPalette::Yellow}, // FavoriteNode
|
||||||
|
{TFTPalette::DarkGray, TFTPalette::White}, // ActionMenuBorder
|
||||||
|
{TFTPalette::Black, TFTPalette::White}, // ActionMenuBody
|
||||||
|
{TFTPalette::DarkGray, TFTPalette::Black}, // ActionMenuTitle
|
||||||
|
{TFTPalette::Black, TFTPalette::White}, // FrameMono
|
||||||
|
{TFTPalette::White, TFTPalette::Black}, // BootSplash
|
||||||
|
{TFTPalette::Black, TFTPalette::Yellow}, // FavoriteNodeBGHighlight
|
||||||
|
{TFTPalette::Black, TFTPalette::LightGray}, // NavigationBar (icon fg, bar bg)
|
||||||
|
{TFTPalette::Black, TFTPalette::White}, // NavigationArrow (arrow fg, body bg)
|
||||||
|
},
|
||||||
|
TFTPalette::Good, // batteryFillGood
|
||||||
|
TFTPalette::Medium, // batteryFillMedium
|
||||||
|
TFTPalette::Bad, // batteryFillBad
|
||||||
|
true, // fullFrameInvert
|
||||||
|
true, // visible
|
||||||
|
},
|
||||||
|
|
||||||
|
// Christmas (ThemeID::Christmas = 2)
|
||||||
|
{
|
||||||
|
ThemeID::Christmas, // id
|
||||||
|
"Christmas", // name
|
||||||
|
2, // uniqueIdentifier
|
||||||
|
{
|
||||||
|
{TFTPalette::ChristmasRed, TFTPalette::Black}, // HeaderBackground
|
||||||
|
{TFTPalette::ChristmasRed, TFTPalette::Gold}, // HeaderTitle
|
||||||
|
{TFTPalette::ChristmasRed, TFTPalette::Gold}, // HeaderStatus
|
||||||
|
{TFTPalette::ChristmasGreen, TFTPalette::Pine}, // SignalBars
|
||||||
|
{TFTPalette::Gold, TFTPalette::Pine}, // ConnectionIcon
|
||||||
|
{TFTPalette::ChristmasGreen, TFTPalette::Pine}, // UtilizationFill
|
||||||
|
{TFTPalette::Gold, TFTPalette::Pine}, // FavoriteNode
|
||||||
|
{TFTPalette::ChristmasRed, TFTPalette::Pine}, // ActionMenuBorder
|
||||||
|
{TFTPalette::White, TFTPalette::Pine}, // ActionMenuBody
|
||||||
|
{TFTPalette::ChristmasRed, TFTPalette::White}, // ActionMenuTitle
|
||||||
|
{TFTPalette::Pine, TFTPalette::White}, // FrameMono
|
||||||
|
{TFTPalette::White, TFTPalette::ChristmasRed}, // BootSplash
|
||||||
|
{TFTPalette::Gold, TFTPalette::Pine}, // FavoriteNodeBGHighlight
|
||||||
|
{TFTPalette::Gold, TFTPalette::ChristmasRed}, // NavigationBar (icon fg, bar bg)
|
||||||
|
{TFTPalette::Gold, TFTPalette::Pine}, // NavigationArrow (arrow fg, body bg)
|
||||||
|
},
|
||||||
|
TFTPalette::ChristmasGreen, // batteryFillGood
|
||||||
|
TFTPalette::Gold, // batteryFillMedium
|
||||||
|
TFTPalette::ChristmasRed, // batteryFillBad
|
||||||
|
true, // fullFrameInvert
|
||||||
|
false, // visible
|
||||||
|
},
|
||||||
|
|
||||||
|
// Pink (ThemeID::Pink = 3) light variant
|
||||||
|
{
|
||||||
|
ThemeID::Pink, // id
|
||||||
|
"Pink", // name
|
||||||
|
3, // uniqueIdentifier
|
||||||
|
{
|
||||||
|
{TFTPalette::HotPink, TFTPalette::Black}, // HeaderBackground
|
||||||
|
{TFTPalette::HotPink, TFTPalette::White}, // HeaderTitle
|
||||||
|
{TFTPalette::HotPink, TFTPalette::White}, // HeaderStatus
|
||||||
|
{TFTPalette::DeepPink, TFTPalette::PalePink}, // SignalBars
|
||||||
|
{TFTPalette::HotPink, TFTPalette::PalePink}, // ConnectionIcon
|
||||||
|
{TFTPalette::DeepPink, TFTPalette::PalePink}, // UtilizationFill
|
||||||
|
{TFTPalette::Black, TFTPalette::HotPink}, // FavoriteNode
|
||||||
|
{TFTPalette::HotPink, TFTPalette::PalePink}, // ActionMenuBorder
|
||||||
|
{TFTPalette::Black, TFTPalette::PalePink}, // ActionMenuBody
|
||||||
|
{TFTPalette::HotPink, TFTPalette::White}, // ActionMenuTitle
|
||||||
|
{TFTPalette::Black, TFTPalette::White}, // FrameMono
|
||||||
|
{TFTPalette::White, TFTPalette::HotPink}, // BootSplash
|
||||||
|
{TFTPalette::Black, TFTPalette::HotPink}, // FavoriteNodeBGHighlight
|
||||||
|
{TFTPalette::White, TFTPalette::HotPink}, // NavigationBar (icon fg, bar bg)
|
||||||
|
{TFTPalette::HotPink, TFTPalette::PalePink}, // NavigationArrow (arrow fg, body bg)
|
||||||
|
},
|
||||||
|
TFTPalette::DeepPink, // batteryFillGood
|
||||||
|
TFTPalette::HotPink, // batteryFillMedium
|
||||||
|
TFTPalette::Bad, // batteryFillBad
|
||||||
|
true, // fullFrameInvert
|
||||||
|
true, // visible
|
||||||
|
},
|
||||||
|
|
||||||
|
// Blue (ThemeID::Blue = 4) dark variant
|
||||||
|
{
|
||||||
|
ThemeID::Blue, // id
|
||||||
|
"Blue", // name
|
||||||
|
4, // uniqueIdentifier
|
||||||
|
{
|
||||||
|
{TFTPalette::DeepBlue, TFTPalette::Black}, // HeaderBackground
|
||||||
|
{TFTPalette::DeepBlue, TFTPalette::White}, // HeaderTitle
|
||||||
|
{TFTPalette::DeepBlue, TFTPalette::SkyBlue}, // HeaderStatus
|
||||||
|
{TFTPalette::SkyBlue, TFTPalette::Navy}, // SignalBars
|
||||||
|
{TFTPalette::SkyBlue, TFTPalette::Navy}, // ConnectionIcon
|
||||||
|
{TFTPalette::SkyBlue, TFTPalette::Navy}, // UtilizationFill
|
||||||
|
{TFTPalette::SkyBlue, TFTPalette::Navy}, // FavoriteNode
|
||||||
|
{TFTPalette::DeepBlue, TFTPalette::Navy}, // ActionMenuBorder
|
||||||
|
{TFTPalette::White, TFTPalette::Navy}, // ActionMenuBody
|
||||||
|
{TFTPalette::DeepBlue, TFTPalette::White}, // ActionMenuTitle
|
||||||
|
{TFTPalette::Navy, TFTPalette::White}, // FrameMono
|
||||||
|
{TFTPalette::White, TFTPalette::DeepBlue}, // BootSplash
|
||||||
|
{TFTPalette::SkyBlue, TFTPalette::Navy}, // FavoriteNodeBGHighlight
|
||||||
|
{TFTPalette::SkyBlue, TFTPalette::DeepBlue}, // NavigationBar (icon fg, bar bg)
|
||||||
|
{TFTPalette::SkyBlue, TFTPalette::Black}, // NavigationArrow (arrow fg, body bg)
|
||||||
|
},
|
||||||
|
TFTPalette::SkyBlue, // batteryFillGood
|
||||||
|
TFTPalette::Medium, // batteryFillMedium
|
||||||
|
TFTPalette::Bad, // batteryFillBad
|
||||||
|
true, // fullFrameInvert
|
||||||
|
true, // visible
|
||||||
|
},
|
||||||
|
|
||||||
|
// Creamsicle (ThemeID::Creamsicle = 5)light variant
|
||||||
|
{
|
||||||
|
ThemeID::Creamsicle, // id
|
||||||
|
"Creamsicle", // name
|
||||||
|
5, // uniqueIdentifier
|
||||||
|
{
|
||||||
|
{TFTPalette::CreamOrange, TFTPalette::Black}, // HeaderBackground
|
||||||
|
{TFTPalette::CreamOrange, TFTPalette::White}, // HeaderTitle
|
||||||
|
{TFTPalette::CreamOrange, TFTPalette::White}, // HeaderStatus
|
||||||
|
{TFTPalette::DeepOrange, TFTPalette::Cream}, // SignalBars
|
||||||
|
{TFTPalette::CreamOrange, TFTPalette::Cream}, // ConnectionIcon
|
||||||
|
{TFTPalette::DeepOrange, TFTPalette::Cream}, // UtilizationFill
|
||||||
|
{TFTPalette::Black, TFTPalette::CreamOrange}, // FavoriteNode
|
||||||
|
{TFTPalette::CreamOrange, TFTPalette::Cream}, // ActionMenuBorder
|
||||||
|
{TFTPalette::Black, TFTPalette::Cream}, // ActionMenuBody
|
||||||
|
{TFTPalette::CreamOrange, TFTPalette::White}, // ActionMenuTitle
|
||||||
|
{TFTPalette::Black, TFTPalette::White}, // FrameMono
|
||||||
|
{TFTPalette::White, TFTPalette::CreamOrange}, // BootSplash
|
||||||
|
{TFTPalette::Black, TFTPalette::CreamOrange}, // FavoriteNodeBGHighlight
|
||||||
|
{TFTPalette::White, TFTPalette::CreamOrange}, // NavigationBar (icon fg, bar bg)
|
||||||
|
{TFTPalette::CreamOrange, TFTPalette::White}, // NavigationArrow (arrow fg, body bg)
|
||||||
|
},
|
||||||
|
TFTPalette::DeepOrange, // batteryFillGood
|
||||||
|
TFTPalette::Gold, // batteryFillMedium
|
||||||
|
TFTPalette::Bad, // batteryFillBad
|
||||||
|
true, // fullFrameInvert
|
||||||
|
true, // visible
|
||||||
|
},
|
||||||
|
|
||||||
|
// Meshtastic Green (ThemeID::MeshtasticGreen = 6) classic monochrome
|
||||||
|
// Pure single-color-on-black look. Every role maps foreground pixels to
|
||||||
|
// the theme color and background pixels to Black.
|
||||||
|
{
|
||||||
|
ThemeID::MeshtasticGreen, // id
|
||||||
|
"Meshtastic Green", // name
|
||||||
|
6, // uniqueIdentifier
|
||||||
|
{
|
||||||
|
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // HeaderBackground
|
||||||
|
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // HeaderTitle
|
||||||
|
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // HeaderStatus
|
||||||
|
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // SignalBars
|
||||||
|
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // ConnectionIcon
|
||||||
|
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // UtilizationFill
|
||||||
|
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // FavoriteNode
|
||||||
|
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // ActionMenuBorder
|
||||||
|
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // ActionMenuBody
|
||||||
|
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // ActionMenuTitle
|
||||||
|
{TFTPalette::Black, TFTPalette::MeshtasticGreen}, // FrameMono (bodyBg, bodyFg)
|
||||||
|
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // BootSplash
|
||||||
|
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // FavoriteNodeBGHighlight
|
||||||
|
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // NavigationBar
|
||||||
|
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // NavigationArrow
|
||||||
|
},
|
||||||
|
TFTPalette::Black, // batteryFillGood
|
||||||
|
TFTPalette::Black, // batteryFillMedium
|
||||||
|
TFTPalette::Black, // batteryFillBad
|
||||||
|
true, // fullFrameInvert
|
||||||
|
true, // visible
|
||||||
|
},
|
||||||
|
|
||||||
|
// Classic Red (ThemeID::ClassicRed = 7) classic monochrome
|
||||||
|
{
|
||||||
|
ThemeID::ClassicRed, // id
|
||||||
|
"Classic Red", // name
|
||||||
|
7, // uniqueIdentifier
|
||||||
|
{
|
||||||
|
{TFTPalette::ClassicRed, TFTPalette::Black}, // HeaderBackground
|
||||||
|
{TFTPalette::ClassicRed, TFTPalette::Black}, // HeaderTitle
|
||||||
|
{TFTPalette::ClassicRed, TFTPalette::Black}, // HeaderStatus
|
||||||
|
{TFTPalette::ClassicRed, TFTPalette::Black}, // SignalBars
|
||||||
|
{TFTPalette::ClassicRed, TFTPalette::Black}, // ConnectionIcon
|
||||||
|
{TFTPalette::ClassicRed, TFTPalette::Black}, // UtilizationFill
|
||||||
|
{TFTPalette::ClassicRed, TFTPalette::Black}, // FavoriteNode
|
||||||
|
{TFTPalette::ClassicRed, TFTPalette::Black}, // ActionMenuBorder
|
||||||
|
{TFTPalette::ClassicRed, TFTPalette::Black}, // ActionMenuBody
|
||||||
|
{TFTPalette::ClassicRed, TFTPalette::Black}, // ActionMenuTitle
|
||||||
|
{TFTPalette::Black, TFTPalette::ClassicRed}, // FrameMono (bodyBg, bodyFg)
|
||||||
|
{TFTPalette::ClassicRed, TFTPalette::Black}, // BootSplash
|
||||||
|
{TFTPalette::ClassicRed, TFTPalette::Black}, // FavoriteNodeBGHighlight
|
||||||
|
{TFTPalette::ClassicRed, TFTPalette::Black}, // NavigationBar
|
||||||
|
{TFTPalette::ClassicRed, TFTPalette::Black}, // NavigationArrow
|
||||||
|
},
|
||||||
|
TFTPalette::Black, // batteryFillGood
|
||||||
|
TFTPalette::Black, // batteryFillMedium
|
||||||
|
TFTPalette::Black, // batteryFillBad
|
||||||
|
true, // fullFrameInvert
|
||||||
|
true, // visible
|
||||||
|
},
|
||||||
|
|
||||||
|
// Monochrome White (ThemeID::MonochromeWhite = 8) classic monochrome
|
||||||
|
{
|
||||||
|
ThemeID::MonochromeWhite, // id
|
||||||
|
"Monochrome White", // name
|
||||||
|
8, // uniqueIdentifier
|
||||||
|
{
|
||||||
|
{TFTPalette::White, TFTPalette::Black}, // HeaderBackground
|
||||||
|
{TFTPalette::White, TFTPalette::Black}, // HeaderTitle
|
||||||
|
{TFTPalette::White, TFTPalette::Black}, // HeaderStatus
|
||||||
|
{TFTPalette::White, TFTPalette::Black}, // SignalBars
|
||||||
|
{TFTPalette::White, TFTPalette::Black}, // ConnectionIcon
|
||||||
|
{TFTPalette::White, TFTPalette::Black}, // UtilizationFill
|
||||||
|
{TFTPalette::White, TFTPalette::Black}, // FavoriteNode
|
||||||
|
{TFTPalette::White, TFTPalette::Black}, // ActionMenuBorder
|
||||||
|
{TFTPalette::White, TFTPalette::Black}, // ActionMenuBody
|
||||||
|
{TFTPalette::White, TFTPalette::Black}, // ActionMenuTitle
|
||||||
|
{TFTPalette::Black, TFTPalette::White}, // FrameMono (bodyBg, bodyFg)
|
||||||
|
{TFTPalette::White, TFTPalette::Black}, // BootSplash
|
||||||
|
{TFTPalette::White, TFTPalette::Black}, // FavoriteNodeBGHighlight
|
||||||
|
{TFTPalette::White, TFTPalette::Black}, // NavigationBar
|
||||||
|
{TFTPalette::White, TFTPalette::Black}, // NavigationArrow
|
||||||
|
},
|
||||||
|
TFTPalette::Black, // batteryFillGood
|
||||||
|
TFTPalette::Black, // batteryFillMedium
|
||||||
|
TFTPalette::Black, // batteryFillBad
|
||||||
|
true, // fullFrameInvert
|
||||||
|
true, // visible
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr size_t kInternalThemeCount = sizeof(kThemes) / sizeof(kThemes[0]);
|
||||||
|
|
||||||
|
// Resolve the kThemes[] index for the currently persisted theme. Called at
|
||||||
|
// boot (indirectly via getActiveTheme()) and whenever the active theme is
|
||||||
|
// queried, so uiconfig.screen_rgb_color remains the single source of truth.
|
||||||
|
// Matches against .uniqueIdentifier - that's the field whose value is stored
|
||||||
|
// in the user's config. Falls back to 0 (DefaultDark) if no match is found,
|
||||||
|
// which gracefully handles removed or retired themes.
|
||||||
|
static inline size_t resolveThemeIndex()
|
||||||
|
{
|
||||||
|
const uint32_t savedIdentifier = uiconfig.screen_rgb_color & 0x1F;
|
||||||
|
for (size_t i = 0; i < kInternalThemeCount; i++) {
|
||||||
|
if (kThemes[i].uniqueIdentifier == savedIdentifier)
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
return 0; // Default Dark fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline bool normalizeRegion(int16_t &x, int16_t &y, int16_t &width, int16_t &height)
|
||||||
|
{
|
||||||
|
if (width <= 0 || height <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x < 0) {
|
||||||
|
width += x;
|
||||||
|
x = 0;
|
||||||
|
}
|
||||||
|
if (y < 0) {
|
||||||
|
height += y;
|
||||||
|
y = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return width > 0 && height > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void appendColorRegion(int16_t x, int16_t y, int16_t width, int16_t height, uint16_t onColorBe, uint16_t offColorBe)
|
||||||
|
{
|
||||||
|
// Keep the last slot permanently disabled as a sentinel for ST7789 scans.
|
||||||
|
// This leaves MAX_TFT_COLOR_REGIONS - 1 usable entries.
|
||||||
|
if (colorRegionCount >= MAX_TFT_COLOR_REGIONS - 1) {
|
||||||
|
memmove(&colorRegions[0], &colorRegions[1], sizeof(TFTColorRegion) * (MAX_TFT_COLOR_REGIONS - 2));
|
||||||
|
colorRegionCount = MAX_TFT_COLOR_REGIONS - 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
TFTColorRegion ®ion = colorRegions[colorRegionCount++];
|
||||||
|
region.x = x;
|
||||||
|
region.y = y;
|
||||||
|
region.width = width;
|
||||||
|
region.height = height;
|
||||||
|
region.onColorBe = onColorBe;
|
||||||
|
region.offColorBe = offColorBe;
|
||||||
|
region.enabled = true;
|
||||||
|
|
||||||
|
// Keep one disabled sentinel after the active range for ST7789 countColorRegions().
|
||||||
|
if (colorRegionCount < MAX_TFT_COLOR_REGIONS) {
|
||||||
|
colorRegions[colorRegionCount].enabled = false;
|
||||||
|
}
|
||||||
|
colorRegions[MAX_TFT_COLOR_REGIONS - 1].enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Current working role colors (big-endian). Initialised to Dark defaults;
|
||||||
|
// call loadThemeDefaults() after boot / theme change to refresh.
|
||||||
|
static TFTRoleColorsBe roleColors[static_cast<size_t>(TFTColorRole::Count)] = {
|
||||||
|
{toBe565(kHeaderBackground), toBe565(TFTPalette::Black)}, // HeaderBackground
|
||||||
|
{toBe565(kHeaderBackground), toBe565(kTitleColor)}, // HeaderTitle
|
||||||
|
{toBe565(kHeaderBackground), toBe565(kStatusColor)}, // HeaderStatus
|
||||||
|
{toBe565(TFTPalette::Good), toBe565(TFTPalette::Black)}, // SignalBars
|
||||||
|
{toBe565(TFTPalette::Blue), toBe565(TFTPalette::Black)}, // ConnectionIcon
|
||||||
|
{toBe565(TFTPalette::Good), toBe565(TFTPalette::Black)}, // UtilizationFill
|
||||||
|
{toBe565(TFTPalette::Yellow), toBe565(TFTPalette::Black)}, // FavoriteNode
|
||||||
|
{toBe565(TFTPalette::DarkGray), toBe565(TFTPalette::Black)}, // ActionMenuBorder
|
||||||
|
{toBe565(TFTPalette::White), toBe565(TFTPalette::Black)}, // ActionMenuBody
|
||||||
|
{toBe565(TFTPalette::DarkGray), toBe565(TFTPalette::White)}, // ActionMenuTitle
|
||||||
|
{toBe565(TFTPalette::Black), toBe565(TFTPalette::White)}, // FrameMono
|
||||||
|
{toBe565(TFTPalette::White), toBe565(TFTPalette::Black)}, // BootSplash
|
||||||
|
{toBe565(TFTPalette::Yellow), toBe565(TFTPalette::Black)}, // FavoriteNodeBGHighlight
|
||||||
|
{toBe565(kStatusColor), toBe565(kHeaderBackground)}, // NavigationBar
|
||||||
|
{toBe565(kTitleColor), toBe565(TFTPalette::Black)} // NavigationArrow
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// Theme accessors
|
||||||
|
|
||||||
|
const TFTThemeDef &getActiveTheme()
|
||||||
|
{
|
||||||
|
return kThemes[resolveThemeIndex()];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visible-theme accessors
|
||||||
|
// These iterate only themes flagged .visible = true, preserving kThemes[]
|
||||||
|
// order. Menu code should use these so hidden themes don't appear in the
|
||||||
|
// picker while still applying correctly if their ID is persisted.
|
||||||
|
|
||||||
|
size_t getVisibleThemeCount()
|
||||||
|
{
|
||||||
|
size_t count = 0;
|
||||||
|
for (size_t i = 0; i < kInternalThemeCount; i++) {
|
||||||
|
if (kThemes[i].visible)
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TFTThemeDef &getVisibleThemeByIndex(size_t visibleIndex)
|
||||||
|
{
|
||||||
|
size_t seen = 0;
|
||||||
|
for (size_t i = 0; i < kInternalThemeCount; i++) {
|
||||||
|
if (!kThemes[i].visible)
|
||||||
|
continue;
|
||||||
|
if (seen == visibleIndex)
|
||||||
|
return kThemes[i];
|
||||||
|
seen++;
|
||||||
|
}
|
||||||
|
// Fallback: return first theme (never trust a bad index).
|
||||||
|
return kThemes[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t getActiveVisibleThemeIndex()
|
||||||
|
{
|
||||||
|
const size_t active = resolveThemeIndex();
|
||||||
|
if (!kThemes[active].visible)
|
||||||
|
return SIZE_MAX;
|
||||||
|
size_t visibleIdx = 0;
|
||||||
|
for (size_t i = 0; i < active; i++) {
|
||||||
|
if (kThemes[i].visible)
|
||||||
|
visibleIdx++;
|
||||||
|
}
|
||||||
|
return visibleIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t getThemeHeaderBg()
|
||||||
|
{
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
#ifdef TFT_HEADER_BG_COLOR_OVERRIDE
|
||||||
|
return TFT_HEADER_BG_COLOR_OVERRIDE;
|
||||||
|
#else
|
||||||
|
return kThemes[resolveThemeIndex()].roles[static_cast<size_t>(TFTColorRole::HeaderBackground)].onColor;
|
||||||
|
#endif
|
||||||
|
#else
|
||||||
|
return TFTPalette::DarkGray;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t getThemeHeaderText()
|
||||||
|
{
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
#ifdef TFT_HEADER_TITLE_COLOR_OVERRIDE
|
||||||
|
return TFT_HEADER_TITLE_COLOR_OVERRIDE;
|
||||||
|
#else
|
||||||
|
return kThemes[resolveThemeIndex()].roles[static_cast<size_t>(TFTColorRole::HeaderTitle)].offColor;
|
||||||
|
#endif
|
||||||
|
#else
|
||||||
|
return TFTPalette::White;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t getThemeHeaderStatus()
|
||||||
|
{
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
#ifdef TFT_HEADER_STATUS_COLOR_OVERRIDE
|
||||||
|
return TFT_HEADER_STATUS_COLOR_OVERRIDE;
|
||||||
|
#else
|
||||||
|
return kThemes[resolveThemeIndex()].roles[static_cast<size_t>(TFTColorRole::HeaderStatus)].offColor;
|
||||||
|
#endif
|
||||||
|
#else
|
||||||
|
return TFTPalette::White;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t getThemeBodyBg()
|
||||||
|
{
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
return kThemes[resolveThemeIndex()].roles[static_cast<size_t>(TFTColorRole::FrameMono)].onColor;
|
||||||
|
#else
|
||||||
|
return TFTPalette::Black;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t getThemeBodyFg()
|
||||||
|
{
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
return kThemes[resolveThemeIndex()].roles[static_cast<size_t>(TFTColorRole::FrameMono)].offColor;
|
||||||
|
#else
|
||||||
|
return TFTPalette::White;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isThemeFullFrameInvert()
|
||||||
|
{
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
return kThemes[resolveThemeIndex()].fullFrameInvert;
|
||||||
|
#else
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t getThemeBatteryFillColor(int batteryPercent)
|
||||||
|
{
|
||||||
|
const TFTThemeDef &theme = kThemes[resolveThemeIndex()];
|
||||||
|
if (batteryPercent <= 20) {
|
||||||
|
return theme.batteryFillBad;
|
||||||
|
}
|
||||||
|
if (batteryPercent <= 50) {
|
||||||
|
return theme.batteryFillMedium;
|
||||||
|
}
|
||||||
|
return theme.batteryFillGood;
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadThemeDefaults()
|
||||||
|
{
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
const TFTThemeDef &theme = kThemes[resolveThemeIndex()];
|
||||||
|
for (uint8_t i = 0; i < static_cast<uint8_t>(TFTColorRole::Count); i++) {
|
||||||
|
roleColors[i].onColorBe = toBe565(theme.roles[i].onColor);
|
||||||
|
roleColors[i].offColorBe = toBe565(theme.roles[i].offColor);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// Role color assignment with theme-aware transforms
|
||||||
|
|
||||||
|
void setTFTColorRole(TFTColorRole role, uint16_t onColor, uint16_t offColor)
|
||||||
|
{
|
||||||
|
#if !GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
return;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
const uint8_t index = static_cast<uint8_t>(role);
|
||||||
|
if (index >= static_cast<uint8_t>(TFTColorRole::Count)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t themeId = uiconfig.screen_rgb_color & 0x1F;
|
||||||
|
const bool isHighlightRole = (role == TFTColorRole::FavoriteNode || role == TFTColorRole::FavoriteNodeBGHighlight);
|
||||||
|
const bool isBodyRole = !isHighlightRole && isBodyColorRole(role);
|
||||||
|
|
||||||
|
// Classic monochrome themes collapse all non-black accents into one tone.
|
||||||
|
if (isMonochromeTheme(themeId)) {
|
||||||
|
if (onColor != TFTPalette::Black) {
|
||||||
|
onColor = getMonochromeAccent(themeId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (themeId) {
|
||||||
|
case ThemeID::DefaultLight:
|
||||||
|
if (isHighlightRole) {
|
||||||
|
// High-contrast highlight chips on light UI.
|
||||||
|
onColor = TFTPalette::Black;
|
||||||
|
offColor = TFTPalette::Yellow;
|
||||||
|
} else if (isBodyRole) {
|
||||||
|
// Invert body colors for readability on white frames.
|
||||||
|
if (offColor == TFTPalette::Black && role != TFTColorRole::ActionMenuTitle) {
|
||||||
|
offColor = TFTPalette::White;
|
||||||
|
}
|
||||||
|
replaceColor(onColor, TFTPalette::White, TFTPalette::Black);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ThemeID::Christmas:
|
||||||
|
if (isHighlightRole || isBodyRole) {
|
||||||
|
replaceColor(onColor, TFTPalette::Yellow, TFTPalette::Gold);
|
||||||
|
replaceColor(offColor, TFTPalette::Black, TFTPalette::Pine);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ThemeID::Pink:
|
||||||
|
if (isHighlightRole) {
|
||||||
|
onColor = TFTPalette::Black;
|
||||||
|
offColor = TFTPalette::HotPink;
|
||||||
|
} else if (isBodyRole) {
|
||||||
|
replaceColor(offColor, TFTPalette::Black, TFTPalette::PalePink);
|
||||||
|
replaceColor(onColor, TFTPalette::White, TFTPalette::Black);
|
||||||
|
replaceColor(onColor, TFTPalette::Yellow, TFTPalette::DeepPink);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ThemeID::Creamsicle:
|
||||||
|
if (isHighlightRole) {
|
||||||
|
onColor = TFTPalette::Black;
|
||||||
|
offColor = TFTPalette::CreamOrange;
|
||||||
|
} else if (isBodyRole) {
|
||||||
|
replaceColor(offColor, TFTPalette::Black, TFTPalette::Cream);
|
||||||
|
replaceColor(onColor, TFTPalette::White, TFTPalette::Black);
|
||||||
|
replaceColor(onColor, TFTPalette::Yellow, TFTPalette::DeepOrange);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ThemeID::Blue:
|
||||||
|
if (isHighlightRole || isBodyRole) {
|
||||||
|
replaceColor(onColor, TFTPalette::Yellow, TFTPalette::SkyBlue);
|
||||||
|
replaceColor(offColor, TFTPalette::Black, TFTPalette::Navy);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
roleColors[index].onColorBe = toBe565(onColor);
|
||||||
|
roleColors[index].offColorBe = toBe565(offColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Region registration
|
||||||
|
|
||||||
|
void registerTFTColorRegion(TFTColorRole role, int16_t x, int16_t y, int16_t width, int16_t height)
|
||||||
|
{
|
||||||
|
#if !GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
return;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
const uint8_t roleIndex = static_cast<uint8_t>(role);
|
||||||
|
if (roleIndex >= static_cast<uint8_t>(TFTColorRole::Count)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!normalizeRegion(x, y, width, height)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TFTRoleColorsBe &colors = roleColors[roleIndex];
|
||||||
|
appendColorRegion(x, y, width, height, colors.onColorBe, colors.offColorBe);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setAndRegisterTFTColorRole(TFTColorRole role, uint16_t onColor, uint16_t offColor, int16_t x, int16_t y, int16_t width,
|
||||||
|
int16_t height)
|
||||||
|
{
|
||||||
|
#if !GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
(void)role;
|
||||||
|
(void)onColor;
|
||||||
|
(void)offColor;
|
||||||
|
(void)x;
|
||||||
|
(void)y;
|
||||||
|
(void)width;
|
||||||
|
(void)height;
|
||||||
|
return;
|
||||||
|
#else
|
||||||
|
setTFTColorRole(role, onColor, offColor);
|
||||||
|
registerTFTColorRegion(role, x, y, width, height);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void registerTFTColorRegionDirect(int16_t x, int16_t y, int16_t width, int16_t height, uint16_t onColor, uint16_t offColor)
|
||||||
|
{
|
||||||
|
#if !GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
return;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (!normalizeRegion(x, y, width, height))
|
||||||
|
return;
|
||||||
|
|
||||||
|
appendColorRegion(x, y, width, height, toBe565(onColor), toBe565(offColor));
|
||||||
|
}
|
||||||
|
|
||||||
|
void registerTFTActionMenuRegions(int16_t boxLeft, int16_t boxTop, int16_t boxWidth, int16_t boxHeight)
|
||||||
|
{
|
||||||
|
#if !GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
(void)boxLeft;
|
||||||
|
(void)boxTop;
|
||||||
|
(void)boxWidth;
|
||||||
|
(void)boxHeight;
|
||||||
|
return;
|
||||||
|
#else
|
||||||
|
// Use theme-appropriate menu colors.
|
||||||
|
const TFTThemeDef &theme = kThemes[resolveThemeIndex()];
|
||||||
|
const TFTThemeRoleColor &menuBody = theme.roles[static_cast<size_t>(TFTColorRole::ActionMenuBody)];
|
||||||
|
const TFTThemeRoleColor &menuBorder = theme.roles[static_cast<size_t>(TFTColorRole::ActionMenuBorder)];
|
||||||
|
|
||||||
|
// Fill role includes a 1px shadow guard so stale frame edges are overwritten uniformly.
|
||||||
|
setAndRegisterTFTColorRole(TFTColorRole::ActionMenuBody, menuBody.onColor, menuBody.offColor, boxLeft - 1, boxTop - 1,
|
||||||
|
boxWidth + 2, boxHeight + 2);
|
||||||
|
registerTFTColorRegion(TFTColorRole::ActionMenuBody, boxLeft, boxTop - 2, boxWidth, 1);
|
||||||
|
registerTFTColorRegion(TFTColorRole::ActionMenuBody, boxLeft, boxTop + boxHeight + 1, boxWidth, 1);
|
||||||
|
registerTFTColorRegion(TFTColorRole::ActionMenuBody, boxLeft - 2, boxTop, 1, boxHeight);
|
||||||
|
registerTFTColorRegion(TFTColorRole::ActionMenuBody, boxLeft + boxWidth + 1, boxTop, 1, boxHeight);
|
||||||
|
|
||||||
|
setAndRegisterTFTColorRole(TFTColorRole::ActionMenuBorder, menuBorder.onColor, menuBorder.offColor, boxLeft, boxTop, boxWidth,
|
||||||
|
1);
|
||||||
|
registerTFTColorRegion(TFTColorRole::ActionMenuBorder, boxLeft, boxTop + boxHeight - 1, boxWidth, 1);
|
||||||
|
registerTFTColorRegion(TFTColorRole::ActionMenuBorder, boxLeft, boxTop, 1, boxHeight);
|
||||||
|
registerTFTColorRegion(TFTColorRole::ActionMenuBorder, boxLeft + boxWidth - 1, boxTop, 1, boxHeight);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// Frame signature & utilities
|
||||||
|
|
||||||
|
uint32_t getTFTColorFrameSignature()
|
||||||
|
{
|
||||||
|
#if !GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
return 0;
|
||||||
|
#else
|
||||||
|
uint32_t hash = kFnv1aOffsetBasis;
|
||||||
|
hash = fnv1aAppendByte(hash, colorRegionCount);
|
||||||
|
for (uint8_t i = 0; i < colorRegionCount; i++) {
|
||||||
|
const TFTColorRegion &r = colorRegions[i];
|
||||||
|
hash = fnv1aAppendU16(hash, static_cast<uint16_t>(r.x));
|
||||||
|
hash = fnv1aAppendU16(hash, static_cast<uint16_t>(r.y));
|
||||||
|
hash = fnv1aAppendU16(hash, static_cast<uint16_t>(r.width));
|
||||||
|
hash = fnv1aAppendU16(hash, static_cast<uint16_t>(r.height));
|
||||||
|
hash = fnv1aAppendU16(hash, r.onColorBe);
|
||||||
|
hash = fnv1aAppendU16(hash, r.offColorBe);
|
||||||
|
}
|
||||||
|
|
||||||
|
return hash;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t getTFTColorRegionCount()
|
||||||
|
{
|
||||||
|
#if !GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
return 0;
|
||||||
|
#else
|
||||||
|
return colorRegionCount;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearTFTColorRegions()
|
||||||
|
{
|
||||||
|
for (uint8_t i = 0; i < colorRegionCount; i++) {
|
||||||
|
colorRegions[i].enabled = false;
|
||||||
|
}
|
||||||
|
if (colorRegionCount < MAX_TFT_COLOR_REGIONS) {
|
||||||
|
colorRegions[colorRegionCount].enabled = false;
|
||||||
|
}
|
||||||
|
colorRegionCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t resolveTFTColorPixel(int16_t x, int16_t y, bool isset, uint16_t defaultOnColor, uint16_t defaultOffColor)
|
||||||
|
{
|
||||||
|
for (int i = static_cast<int>(colorRegionCount) - 1; i >= 0; i--) {
|
||||||
|
const TFTColorRegion &r = colorRegions[i];
|
||||||
|
if (x >= r.x && x < r.x + r.width && y >= r.y && y < r.y + r.height) {
|
||||||
|
return isset ? r.onColorBe : r.offColorBe;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return isset ? defaultOnColor : defaultOffColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t resolveTFTOffColorAt(int16_t x, int16_t y, uint16_t defaultOffColor)
|
||||||
|
{
|
||||||
|
#if !GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
(void)x;
|
||||||
|
(void)y;
|
||||||
|
return defaultOffColor;
|
||||||
|
#else
|
||||||
|
const uint16_t defaultOffBe = toBe565(defaultOffColor);
|
||||||
|
const uint16_t sampledBe = resolveTFTColorPixel(x, y, false, defaultOffBe, defaultOffBe);
|
||||||
|
return static_cast<uint16_t>((sampledBe >> 8) | (sampledBe << 8));
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace graphics
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "configuration.h"
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
namespace graphics
|
||||||
|
{
|
||||||
|
|
||||||
|
struct TFTColorRegion {
|
||||||
|
int16_t x;
|
||||||
|
int16_t y;
|
||||||
|
int16_t width;
|
||||||
|
int16_t height;
|
||||||
|
uint16_t onColorBe;
|
||||||
|
uint16_t offColorBe;
|
||||||
|
// Required by ST7789 driver: it scans until the first disabled entry.
|
||||||
|
bool enabled = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr size_t MAX_TFT_COLOR_REGIONS = 48;
|
||||||
|
extern TFTColorRegion colorRegions[MAX_TFT_COLOR_REGIONS];
|
||||||
|
|
||||||
|
enum class TFTColorRole : uint8_t {
|
||||||
|
HeaderBackground = 0,
|
||||||
|
HeaderTitle,
|
||||||
|
HeaderStatus,
|
||||||
|
SignalBars,
|
||||||
|
ConnectionIcon,
|
||||||
|
UtilizationFill,
|
||||||
|
FavoriteNode,
|
||||||
|
ActionMenuBorder,
|
||||||
|
ActionMenuBody,
|
||||||
|
ActionMenuTitle,
|
||||||
|
FrameMono,
|
||||||
|
BootSplash,
|
||||||
|
FavoriteNodeBGHighlight,
|
||||||
|
NavigationBar,
|
||||||
|
NavigationArrow,
|
||||||
|
Count
|
||||||
|
};
|
||||||
|
|
||||||
|
#if HAS_TFT || defined(ST7701_CS) || defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \
|
||||||
|
defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) || \
|
||||||
|
defined(USE_ST7796) || defined(HACKADAY_COMMUNICATOR)
|
||||||
|
#define GRAPHICS_TFT_COLORING_ENABLED 1
|
||||||
|
#else
|
||||||
|
#define GRAPHICS_TFT_COLORING_ENABLED 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static constexpr bool kTFTColoringEnabled = GRAPHICS_TFT_COLORING_ENABLED != 0;
|
||||||
|
constexpr bool isTFTColoringEnabled()
|
||||||
|
{
|
||||||
|
return kTFTColoringEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setTFTColorRole(TFTColorRole role, uint16_t onColor, uint16_t offColor);
|
||||||
|
void registerTFTColorRegion(TFTColorRole role, int16_t x, int16_t y, int16_t width, int16_t height);
|
||||||
|
// Convenience helper for the common "set role then register one region" flow.
|
||||||
|
void setAndRegisterTFTColorRole(TFTColorRole role, uint16_t onColor, uint16_t offColor, int16_t x, int16_t y, int16_t width,
|
||||||
|
int16_t height);
|
||||||
|
// Register a region using explicit colors (no role lookup). Use when the
|
||||||
|
// color comes from a theme field rather than a role (e.g. battery fill).
|
||||||
|
void registerTFTColorRegionDirect(int16_t x, int16_t y, int16_t width, int16_t height, uint16_t onColor, uint16_t offColor);
|
||||||
|
void registerTFTActionMenuRegions(int16_t boxLeft, int16_t boxTop, int16_t boxWidth, int16_t boxHeight);
|
||||||
|
uint32_t getTFTColorFrameSignature();
|
||||||
|
uint8_t getTFTColorRegionCount();
|
||||||
|
void clearTFTColorRegions();
|
||||||
|
uint16_t resolveTFTColorPixel(int16_t x, int16_t y, bool isset, uint16_t defaultOnColor, uint16_t defaultOffColor);
|
||||||
|
// Resolve effective region-mapped OFF color at a coordinate in native-endian RGB565.
|
||||||
|
uint16_t resolveTFTOffColorAt(int16_t x, int16_t y, uint16_t defaultOffColor);
|
||||||
|
|
||||||
|
// -- Theme engine ------------------------------------------------------
|
||||||
|
// Each theme has four fields that work together:
|
||||||
|
//
|
||||||
|
// id - ThemeID:: constant, used for in-code references.
|
||||||
|
// name - human-readable label shown in the theme picker.
|
||||||
|
// uniqueIdentifier - the stable numeric value persisted to
|
||||||
|
// uiconfig.screen_rgb_color and restored at boot.
|
||||||
|
// This is a CONTRACT with saved configs on disk - once
|
||||||
|
// assigned, never reuse or renumber, even if the theme is
|
||||||
|
// deleted or the kThemes[] array is reordered.
|
||||||
|
// visible - controls whether a theme appears in the picker menu.
|
||||||
|
// Hidden themes can still be restored and applied if their
|
||||||
|
// uniqueIdentifier is persisted.
|
||||||
|
//
|
||||||
|
// Display order in the menu is controlled by kThemes[] array order among
|
||||||
|
// themes where visible == true, NOT by any numeric value above.
|
||||||
|
//
|
||||||
|
// To add a new theme:
|
||||||
|
// 1. Add a unique constant in ThemeID below (next unused value).
|
||||||
|
// 2. Add a kThemes[] entry at the desired menu position, with a unique
|
||||||
|
// uniqueIdentifier that has never been used by any prior theme.
|
||||||
|
// 3. Set visible=true if it should appear in the picker.
|
||||||
|
//
|
||||||
|
// To retire a theme without breaking saved configs:
|
||||||
|
// - Preferred: keep the entry and set visible=false so existing saved
|
||||||
|
// uniqueIdentifier values still resolve to the same theme.
|
||||||
|
// - If you remove the entry, resolveThemeIndex() falls back to DefaultDark
|
||||||
|
// when the persisted uniqueIdentifier no longer matches any theme.
|
||||||
|
// - Do NOT reuse a retired uniqueIdentifier for a future theme.
|
||||||
|
namespace ThemeID
|
||||||
|
{
|
||||||
|
constexpr uint32_t DefaultDark = 0;
|
||||||
|
constexpr uint32_t DefaultLight = 1;
|
||||||
|
constexpr uint32_t Christmas = 2;
|
||||||
|
constexpr uint32_t Pink = 3;
|
||||||
|
constexpr uint32_t Blue = 4;
|
||||||
|
constexpr uint32_t Creamsicle = 5;
|
||||||
|
constexpr uint32_t MeshtasticGreen = 6;
|
||||||
|
constexpr uint32_t ClassicRed = 7;
|
||||||
|
constexpr uint32_t MonochromeWhite = 8;
|
||||||
|
} // namespace ThemeID
|
||||||
|
|
||||||
|
// Per-role color pair stored in native (little-endian) RGB565 format.
|
||||||
|
struct TFTThemeRoleColor {
|
||||||
|
uint16_t onColor;
|
||||||
|
uint16_t offColor;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Complete theme definition.
|
||||||
|
struct TFTThemeDef {
|
||||||
|
uint32_t id; // ThemeID constant - in-code identifier for this theme.
|
||||||
|
const char *name; // Human-readable label shown in the theme picker.
|
||||||
|
uint32_t uniqueIdentifier; // Stable persisted value copied into uiconfig.screen_rgb_color.
|
||||||
|
// Never reuse or renumber - see file-level notes above.
|
||||||
|
TFTThemeRoleColor roles[static_cast<size_t>(TFTColorRole::Count)];
|
||||||
|
uint16_t batteryFillGood;
|
||||||
|
uint16_t batteryFillMedium;
|
||||||
|
uint16_t batteryFillBad;
|
||||||
|
bool fullFrameInvert; // Apply full-frame FrameMono inversion (ST7789 light themes)
|
||||||
|
bool visible; // Show in the theme picker menu. Hidden themes still apply
|
||||||
|
// correctly if their uniqueIdentifier is persisted (dev/legacy themes).
|
||||||
|
};
|
||||||
|
|
||||||
|
// Count of themes whose .visible flag is true. Use this when building menus.
|
||||||
|
size_t getVisibleThemeCount();
|
||||||
|
|
||||||
|
// Access the Nth visible theme (0 .. getVisibleThemeCount()-1). Hidden themes
|
||||||
|
// are skipped, preserving kThemes[] order among the visible entries.
|
||||||
|
const TFTThemeDef &getVisibleThemeByIndex(size_t visibleIndex);
|
||||||
|
|
||||||
|
// Return the theme that matches uiconfig.screen_rgb_color (falls back to Dark).
|
||||||
|
const TFTThemeDef &getActiveTheme();
|
||||||
|
|
||||||
|
// Return the visible-theme index for the currently active theme, or SIZE_MAX
|
||||||
|
// if the active theme is hidden (so menus can show "no selection").
|
||||||
|
size_t getActiveVisibleThemeIndex();
|
||||||
|
|
||||||
|
// Convenience accessors - safe to call even when coloring is compiled out.
|
||||||
|
uint16_t getThemeHeaderBg();
|
||||||
|
uint16_t getThemeHeaderText();
|
||||||
|
uint16_t getThemeHeaderStatus();
|
||||||
|
uint16_t getThemeBodyBg();
|
||||||
|
uint16_t getThemeBodyFg();
|
||||||
|
bool isThemeFullFrameInvert();
|
||||||
|
uint16_t getThemeBatteryFillColor(int batteryPercent);
|
||||||
|
|
||||||
|
// Reinitialise default roleColors from the active theme. Call after a
|
||||||
|
// theme change so that any role registered without a prior setTFTColorRole()
|
||||||
|
// picks up theme-appropriate defaults.
|
||||||
|
void loadThemeDefaults();
|
||||||
|
|
||||||
|
} // namespace graphics
|
||||||
+142
-43
@@ -16,12 +16,6 @@
|
|||||||
extern SX1509 gpioExtender;
|
extern SX1509 gpioExtender;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef TFT_MESH_OVERRIDE
|
|
||||||
uint16_t TFT_MESH = TFT_MESH_OVERRIDE;
|
|
||||||
#else
|
|
||||||
uint16_t TFT_MESH = COLOR565(0x67, 0xEA, 0x94);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(ST7735S)
|
#if defined(ST7735S)
|
||||||
#include <LovyanGFX.hpp> // Graphics and font library for ST7735 driver chip
|
#include <LovyanGFX.hpp> // Graphics and font library for ST7735 driver chip
|
||||||
|
|
||||||
@@ -1140,7 +1134,9 @@ static LGFX *tft = nullptr;
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include "SPILock.h"
|
#include "SPILock.h"
|
||||||
|
#include "TFTColorRegions.h"
|
||||||
#include "TFTDisplay.h"
|
#include "TFTDisplay.h"
|
||||||
|
#include "TFTPalette.h"
|
||||||
#include <SPI.h>
|
#include <SPI.h>
|
||||||
|
|
||||||
#ifdef UNPHONE
|
#ifdef UNPHONE
|
||||||
@@ -1150,6 +1146,25 @@ extern unPhone unphone;
|
|||||||
|
|
||||||
GpioPin *TFTDisplay::backlightEnable = NULL;
|
GpioPin *TFTDisplay::backlightEnable = NULL;
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
static constexpr uint8_t kFullRepaintChunkRows = 8;
|
||||||
|
|
||||||
|
static inline uint16_t getThemeDefaultOnColor()
|
||||||
|
{
|
||||||
|
return graphics::TFTPalette::White;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline uint16_t getThemeDefaultOffColor()
|
||||||
|
{
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
return graphics::getThemeBodyBg();
|
||||||
|
#else
|
||||||
|
return TFT_BLACK;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
TFTDisplay::TFTDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY geometry, HW_I2C i2cBus)
|
TFTDisplay::TFTDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY geometry, HW_I2C i2cBus)
|
||||||
{
|
{
|
||||||
LOG_DEBUG("TFTDisplay!");
|
LOG_DEBUG("TFTDisplay!");
|
||||||
@@ -1189,14 +1204,15 @@ TFTDisplay::~TFTDisplay()
|
|||||||
free(linePixelBuffer);
|
free(linePixelBuffer);
|
||||||
linePixelBuffer = nullptr;
|
linePixelBuffer = nullptr;
|
||||||
}
|
}
|
||||||
|
if (repaintChunkBuffer != nullptr) {
|
||||||
|
free(repaintChunkBuffer);
|
||||||
|
repaintChunkBuffer = nullptr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the buffer to the display memory
|
// Write the buffer to the display memory
|
||||||
void TFTDisplay::display(bool fromBlank)
|
void TFTDisplay::display(bool fromBlank)
|
||||||
{
|
{
|
||||||
if (fromBlank)
|
|
||||||
tft->fillScreen(TFT_BLACK);
|
|
||||||
|
|
||||||
concurrency::LockGuard g(spiLock);
|
concurrency::LockGuard g(spiLock);
|
||||||
|
|
||||||
uint32_t x, y;
|
uint32_t x, y;
|
||||||
@@ -1205,12 +1221,70 @@ void TFTDisplay::display(bool fromBlank)
|
|||||||
uint32_t x_FirstPixelUpdate;
|
uint32_t x_FirstPixelUpdate;
|
||||||
uint32_t x_LastPixelUpdate;
|
uint32_t x_LastPixelUpdate;
|
||||||
bool isset, dblbuf_isset;
|
bool isset, dblbuf_isset;
|
||||||
uint16_t colorTftMesh, colorTftBlack;
|
uint16_t colorTftWhite, colorTftBlack;
|
||||||
bool somethingChanged = false;
|
bool somethingChanged = false;
|
||||||
|
|
||||||
// Store colors byte-reversed so that TFT_eSPI doesn't have to swap bytes in a separate step
|
// Theme defaults for non-role pixels.
|
||||||
colorTftMesh = __builtin_bswap16(TFT_MESH);
|
const uint16_t defaultOnColor = getThemeDefaultOnColor();
|
||||||
colorTftBlack = __builtin_bswap16(TFT_BLACK);
|
const uint16_t defaultOffColor = getThemeDefaultOffColor();
|
||||||
|
static uint16_t lastDefaultOnColor = 0;
|
||||||
|
static uint16_t lastDefaultOffColor = 0;
|
||||||
|
static bool haveLastDefaults = false;
|
||||||
|
const bool themeDefaultsChanged =
|
||||||
|
!haveLastDefaults || (defaultOnColor != lastDefaultOnColor) || (defaultOffColor != lastDefaultOffColor);
|
||||||
|
const bool forceFullRepaint = fromBlank || themeDefaultsChanged;
|
||||||
|
|
||||||
|
// If theme defaults changed, reset panel background immediately so stale pixels don't linger.
|
||||||
|
if (forceFullRepaint) {
|
||||||
|
tft->fillScreen(defaultOffColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
colorTftWhite = (defaultOnColor >> 8) | ((defaultOnColor & 0xFF) << 8);
|
||||||
|
colorTftBlack = (defaultOffColor >> 8) | ((defaultOffColor & 0xFF) << 8);
|
||||||
|
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
static uint32_t lastColorFrameSignature = 0;
|
||||||
|
const bool hasColorRegions = graphics::getTFTColorRegionCount() > 0;
|
||||||
|
const uint32_t colorFrameSignature = graphics::getTFTColorFrameSignature();
|
||||||
|
const bool forceFullColorRepaint = forceFullRepaint || (colorFrameSignature != lastColorFrameSignature);
|
||||||
|
|
||||||
|
// When region roles/layout changed, color can differ even with identical monochrome glyph bits.
|
||||||
|
// Repaint full frame only for those frames, then return to diff-based updates.
|
||||||
|
if (forceFullColorRepaint) {
|
||||||
|
for (uint32_t yStart = 0; yStart < displayHeight; yStart += kFullRepaintChunkRows) {
|
||||||
|
const uint32_t rowsThisChunk = min<uint32_t>(kFullRepaintChunkRows, displayHeight - yStart);
|
||||||
|
for (uint32_t row = 0; row < rowsThisChunk; row++) {
|
||||||
|
y = yStart + row;
|
||||||
|
y_byteIndex = (y / 8) * displayWidth;
|
||||||
|
y_byteMask = (1 << (y & 7));
|
||||||
|
|
||||||
|
uint16_t *chunkRow = repaintChunkBuffer + (row * displayWidth);
|
||||||
|
for (x = 0; x < displayWidth; x++) {
|
||||||
|
isset = (buffer[x + y_byteIndex] & y_byteMask) != 0;
|
||||||
|
if (hasColorRegions) {
|
||||||
|
chunkRow[x] = graphics::resolveTFTColorPixel(static_cast<int16_t>(x), static_cast<int16_t>(y), isset,
|
||||||
|
colorTftWhite, colorTftBlack);
|
||||||
|
} else {
|
||||||
|
chunkRow[x] = isset ? colorTftWhite : colorTftBlack;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if defined(HACKADAY_COMMUNICATOR)
|
||||||
|
tft->draw16bitBeRGBBitmap(0, yStart, repaintChunkBuffer, displayWidth, rowsThisChunk);
|
||||||
|
#else
|
||||||
|
tft->pushImage(0, yStart, displayWidth, rowsThisChunk, repaintChunkBuffer);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(buffer_back, buffer, displayBufferSize);
|
||||||
|
lastColorFrameSignature = colorFrameSignature;
|
||||||
|
haveLastDefaults = true;
|
||||||
|
lastDefaultOnColor = defaultOnColor;
|
||||||
|
lastDefaultOffColor = defaultOffColor;
|
||||||
|
graphics::clearTFTColorRegions();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
y = 0;
|
y = 0;
|
||||||
while (y < displayHeight) {
|
while (y < displayHeight) {
|
||||||
@@ -1219,7 +1293,7 @@ void TFTDisplay::display(bool fromBlank)
|
|||||||
|
|
||||||
// Step 1: Do a quick scan of 8 rows together. This allows fast-forwarding over unchanged screen areas.
|
// Step 1: Do a quick scan of 8 rows together. This allows fast-forwarding over unchanged screen areas.
|
||||||
if (y_byteMask == 1) {
|
if (y_byteMask == 1) {
|
||||||
if (!fromBlank) {
|
if (!forceFullRepaint) {
|
||||||
for (x = 0; x < displayWidth; x++) {
|
for (x = 0; x < displayWidth; x++) {
|
||||||
if (buffer[x + y_byteIndex] != buffer_back[x + y_byteIndex])
|
if (buffer[x + y_byteIndex] != buffer_back[x + y_byteIndex])
|
||||||
break;
|
break;
|
||||||
@@ -1237,13 +1311,14 @@ void TFTDisplay::display(bool fromBlank)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Scan each of the 8 rows individually. Find the first pixel in each row that needs updating
|
// Step 2: Scan this row for changed span (first and last changed pixel).
|
||||||
for (x_FirstPixelUpdate = 0; x_FirstPixelUpdate < displayWidth; x_FirstPixelUpdate++) {
|
uint32_t x_FirstChanged = 0;
|
||||||
isset = buffer[x_FirstPixelUpdate + y_byteIndex] & y_byteMask;
|
for (x_FirstChanged = 0; x_FirstChanged < displayWidth; x_FirstChanged++) {
|
||||||
|
isset = buffer[x_FirstChanged + y_byteIndex] & y_byteMask;
|
||||||
|
|
||||||
if (!fromBlank) {
|
if (!forceFullRepaint) {
|
||||||
// get src pixel in the page based ordering the OLED lib uses
|
// get src pixel in the page based ordering the OLED lib uses
|
||||||
dblbuf_isset = buffer_back[x_FirstPixelUpdate + y_byteIndex] & y_byteMask;
|
dblbuf_isset = buffer_back[x_FirstChanged + y_byteIndex] & y_byteMask;
|
||||||
if (isset != dblbuf_isset) {
|
if (isset != dblbuf_isset) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1253,43 +1328,51 @@ void TFTDisplay::display(bool fromBlank)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Did we find a pixel that needs updating on this row?
|
// Did we find a pixel that needs updating on this row?
|
||||||
if (x_FirstPixelUpdate < displayWidth) {
|
if (x_FirstChanged < displayWidth) {
|
||||||
// Align the first pixel for update to an even number so the total alignment of
|
uint32_t x_LastChanged = displayWidth - 1;
|
||||||
// the data will be at 32-bit boundary, which is required by GDMA SPI transfers.
|
while (x_LastChanged > x_FirstChanged) {
|
||||||
x_FirstPixelUpdate &= ~1;
|
isset = buffer[x_LastChanged + y_byteIndex] & y_byteMask;
|
||||||
|
if (!forceFullRepaint) {
|
||||||
// Step 3a: copy rest of the pixels in this row into the pixel line buffer,
|
dblbuf_isset = buffer_back[x_LastChanged + y_byteIndex] & y_byteMask;
|
||||||
// while also recording the last pixel in the row that needs updating.
|
|
||||||
// Since the first changed pixel will be looked up, the x_LastPixelUpdate will be set.
|
|
||||||
for (x = x_FirstPixelUpdate; x < displayWidth; x++) {
|
|
||||||
isset = buffer[x + y_byteIndex] & y_byteMask;
|
|
||||||
linePixelBuffer[x] = isset ? colorTftMesh : colorTftBlack;
|
|
||||||
|
|
||||||
if (!fromBlank) {
|
|
||||||
dblbuf_isset = buffer_back[x + y_byteIndex] & y_byteMask;
|
|
||||||
if (isset != dblbuf_isset) {
|
if (isset != dblbuf_isset) {
|
||||||
x_LastPixelUpdate = x;
|
break;
|
||||||
}
|
}
|
||||||
} else if (isset) {
|
} else if (isset) {
|
||||||
x_LastPixelUpdate = x;
|
break;
|
||||||
}
|
}
|
||||||
|
x_LastChanged--;
|
||||||
}
|
}
|
||||||
// Step 3b: Round up the last pixel to odd number to maintain 32-bit alignment for SPIs.
|
|
||||||
// Most displays will have even number of pixels in a row -- this will be in bounds
|
// Align the first pixel for update to an even number so the total alignment of
|
||||||
// of the displayWidth. (Hopefully odd displays will just ignore that extra pixel.)
|
// the data will be at 32-bit boundary, which is required by GDMA SPI transfers.
|
||||||
x_LastPixelUpdate |= 1;
|
x_FirstPixelUpdate = x_FirstChanged & ~1U;
|
||||||
// Ensure the last pixel index does not exceed the display width.
|
x_LastPixelUpdate = x_LastChanged | 1U;
|
||||||
if (x_LastPixelUpdate >= displayWidth) {
|
if (x_LastPixelUpdate >= displayWidth) {
|
||||||
x_LastPixelUpdate = displayWidth - 1;
|
x_LastPixelUpdate = displayWidth - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Step 3: Copy only the changed span into the pixel line buffer.
|
||||||
|
for (x = x_FirstPixelUpdate; x <= x_LastPixelUpdate; x++) {
|
||||||
|
isset = buffer[x + y_byteIndex] & y_byteMask;
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
if (hasColorRegions) {
|
||||||
|
linePixelBuffer[x] = graphics::resolveTFTColorPixel(static_cast<int16_t>(x), static_cast<int16_t>(y), isset,
|
||||||
|
colorTftWhite, colorTftBlack);
|
||||||
|
} else {
|
||||||
|
linePixelBuffer[x] = isset ? colorTftWhite : colorTftBlack;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
linePixelBuffer[x] = isset ? colorTftWhite : colorTftBlack;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
#if defined(HACKADAY_COMMUNICATOR)
|
#if defined(HACKADAY_COMMUNICATOR)
|
||||||
tft->draw16bitBeRGBBitmap(x_FirstPixelUpdate, y, &linePixelBuffer[x_FirstPixelUpdate],
|
tft->draw16bitBeRGBBitmap(x_FirstPixelUpdate, y, &linePixelBuffer[x_FirstPixelUpdate],
|
||||||
(x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1);
|
(x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1);
|
||||||
#else
|
#else
|
||||||
// Step 4: Send the changed pixels on this line to the screen as a single block transfer.
|
// Step 4: Send the changed pixels on this line to the screen as a single block transfer.
|
||||||
// This function accepts pixel data MSB first so it can dump the memory straight out the SPI port.
|
// This function accepts pixel data MSB first so it can dump the memory straight out the SPI port.
|
||||||
tft->pushRect(x_FirstPixelUpdate, y, (x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1,
|
tft->pushImage(x_FirstPixelUpdate, y, (x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1,
|
||||||
&linePixelBuffer[x_FirstPixelUpdate]);
|
&linePixelBuffer[x_FirstPixelUpdate]);
|
||||||
#endif
|
#endif
|
||||||
somethingChanged = true;
|
somethingChanged = true;
|
||||||
}
|
}
|
||||||
@@ -1298,6 +1381,14 @@ void TFTDisplay::display(bool fromBlank)
|
|||||||
// Copy the Buffer to the Back Buffer
|
// Copy the Buffer to the Back Buffer
|
||||||
if (somethingChanged)
|
if (somethingChanged)
|
||||||
memcpy(buffer_back, buffer, displayBufferSize);
|
memcpy(buffer_back, buffer, displayBufferSize);
|
||||||
|
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
lastColorFrameSignature = colorFrameSignature;
|
||||||
|
#endif
|
||||||
|
haveLastDefaults = true;
|
||||||
|
lastDefaultOnColor = defaultOnColor;
|
||||||
|
lastDefaultOffColor = defaultOffColor;
|
||||||
|
graphics::clearTFTColorRegions();
|
||||||
}
|
}
|
||||||
|
|
||||||
void TFTDisplay::sdlLoop()
|
void TFTDisplay::sdlLoop()
|
||||||
@@ -1511,7 +1602,7 @@ bool TFTDisplay::connect()
|
|||||||
#else
|
#else
|
||||||
tft->setRotation(3); // Orient horizontal and wide underneath the silkscreen name label
|
tft->setRotation(3); // Orient horizontal and wide underneath the silkscreen name label
|
||||||
#endif
|
#endif
|
||||||
tft->fillScreen(TFT_BLACK);
|
tft->fillScreen(getThemeDefaultOffColor());
|
||||||
|
|
||||||
if (this->linePixelBuffer == NULL) {
|
if (this->linePixelBuffer == NULL) {
|
||||||
this->linePixelBuffer = (uint16_t *)malloc(sizeof(uint16_t) * displayWidth);
|
this->linePixelBuffer = (uint16_t *)malloc(sizeof(uint16_t) * displayWidth);
|
||||||
@@ -1521,6 +1612,14 @@ bool TFTDisplay::connect()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (this->repaintChunkBuffer == NULL) {
|
||||||
|
this->repaintChunkBuffer = (uint16_t *)malloc(sizeof(uint16_t) * displayWidth * kFullRepaintChunkRows);
|
||||||
|
|
||||||
|
if (!this->repaintChunkBuffer) {
|
||||||
|
LOG_ERROR("Not enough memory to create TFT repaint chunk buffer\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,4 +63,5 @@ class TFTDisplay : public OLEDDisplay
|
|||||||
virtual bool connect() override;
|
virtual bool connect() override;
|
||||||
|
|
||||||
uint16_t *linePixelBuffer = nullptr;
|
uint16_t *linePixelBuffer = nullptr;
|
||||||
};
|
uint16_t *repaintChunkBuffer = nullptr;
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
namespace graphics
|
||||||
|
{
|
||||||
|
namespace TFTPalette
|
||||||
|
{
|
||||||
|
|
||||||
|
constexpr uint16_t rgb565(uint8_t red, uint8_t green, uint8_t blue)
|
||||||
|
{
|
||||||
|
return static_cast<uint16_t>(((red & 0xF8) << 8) | ((green & 0xFC) << 3) | ((blue & 0xF8) >> 3));
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr uint16_t Black = 0x0000;
|
||||||
|
constexpr uint16_t White = 0xFFFF;
|
||||||
|
constexpr uint16_t DarkGray = 0x4208;
|
||||||
|
constexpr uint16_t Gray = 0x8410;
|
||||||
|
constexpr uint16_t LightGray = 0xC618;
|
||||||
|
|
||||||
|
constexpr uint16_t Red = rgb565(255, 0, 0);
|
||||||
|
constexpr uint16_t Green = rgb565(0, 255, 0);
|
||||||
|
constexpr uint16_t Blue = rgb565(0, 130, 252);
|
||||||
|
constexpr uint16_t Yellow = rgb565(255, 255, 0);
|
||||||
|
constexpr uint16_t Orange = rgb565(255, 165, 0);
|
||||||
|
constexpr uint16_t Cyan = rgb565(0, 255, 255);
|
||||||
|
constexpr uint16_t Magenta = rgb565(255, 0, 255);
|
||||||
|
|
||||||
|
constexpr uint16_t Good = Green;
|
||||||
|
constexpr uint16_t Medium = Yellow;
|
||||||
|
constexpr uint16_t Bad = Red;
|
||||||
|
|
||||||
|
// Christmas / seasonal accent colors
|
||||||
|
constexpr uint16_t ChristmasRed = rgb565(178, 34, 34);
|
||||||
|
constexpr uint16_t ChristmasGreen = rgb565(0, 128, 0);
|
||||||
|
constexpr uint16_t Gold = rgb565(255, 215, 0);
|
||||||
|
constexpr uint16_t Pine = rgb565(15, 35, 10);
|
||||||
|
|
||||||
|
// Pink theme colors (light variant)
|
||||||
|
constexpr uint16_t HotPink = rgb565(255, 105, 180);
|
||||||
|
constexpr uint16_t PalePink = rgb565(255, 228, 235);
|
||||||
|
constexpr uint16_t DeepPink = rgb565(200, 50, 120);
|
||||||
|
|
||||||
|
// Blue theme colors (dark variant)
|
||||||
|
constexpr uint16_t SkyBlue = rgb565(100, 180, 255);
|
||||||
|
constexpr uint16_t Navy = rgb565(15, 15, 50);
|
||||||
|
constexpr uint16_t DeepBlue = rgb565(30, 60, 120);
|
||||||
|
|
||||||
|
// Creamsicle theme colors (light variant)
|
||||||
|
constexpr uint16_t CreamOrange = rgb565(255, 140, 50);
|
||||||
|
constexpr uint16_t DeepOrange = rgb565(220, 100, 20);
|
||||||
|
constexpr uint16_t Cream = rgb565(255, 248, 235);
|
||||||
|
|
||||||
|
// Classic monochrome theme accent colors (single-color-on-black themes)
|
||||||
|
constexpr uint16_t MeshtasticGreen = rgb565(0x67, 0xEA, 0x94);
|
||||||
|
constexpr uint16_t ClassicRed = rgb565(255, 64, 64);
|
||||||
|
// Monochrome White reuses TFTPalette::White above.
|
||||||
|
|
||||||
|
// Fast contrast picker for monochrome glyph overlays on arbitrary RGB565 backgrounds.
|
||||||
|
// Uses channel-sum brightness approximation to keep code size small.
|
||||||
|
constexpr uint16_t pickReadableMonoFg(uint16_t backgroundColor)
|
||||||
|
{
|
||||||
|
const uint16_t r = (backgroundColor >> 11) & 0x1F;
|
||||||
|
const uint16_t g = (backgroundColor >> 5) & 0x3F;
|
||||||
|
const uint16_t b = backgroundColor & 0x1F;
|
||||||
|
return ((r + g + b) >= 70) ? DarkGray : White;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace TFTPalette
|
||||||
|
} // namespace graphics
|
||||||
@@ -145,7 +145,7 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
|
|||||||
// === Set Title, Blank for Clock
|
// === Set Title, Blank for Clock
|
||||||
const char *titleStr = "";
|
const char *titleStr = "";
|
||||||
// === Header ===
|
// === Header ===
|
||||||
graphics::drawCommonHeader(display, x, y, titleStr, true, true);
|
graphics::drawCommonHeader(display, x, y, titleStr, true, true, true);
|
||||||
|
|
||||||
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
|
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
|
||||||
char timeString[16];
|
char timeString[16];
|
||||||
@@ -293,11 +293,15 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
|
|||||||
// Draw an analog clock
|
// Draw an analog clock
|
||||||
void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||||
{
|
{
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
// Clear previous frame pixels so moving hands don't leave stale artifacts on TFT light theme.
|
||||||
|
display->clear();
|
||||||
|
#endif
|
||||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
// === Set Title, Blank for Clock
|
// === Set Title, Blank for Clock
|
||||||
const char *titleStr = "";
|
const char *titleStr = "";
|
||||||
// === Header ===
|
// === Header ===
|
||||||
graphics::drawCommonHeader(display, x, y, titleStr, true, true);
|
graphics::drawCommonHeader(display, x, y, titleStr, true, true, true);
|
||||||
|
|
||||||
// clock face center coordinates
|
// clock face center coordinates
|
||||||
int16_t centerX = display->getWidth() / 2;
|
int16_t centerX = display->getWidth() / 2;
|
||||||
@@ -478,4 +482,4 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
} // namespace ClockRenderer
|
} // namespace ClockRenderer
|
||||||
|
|
||||||
} // namespace graphics
|
} // namespace graphics
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -9,113 +9,61 @@ namespace graphics
|
|||||||
{
|
{
|
||||||
namespace CompassRenderer
|
namespace CompassRenderer
|
||||||
{
|
{
|
||||||
|
|
||||||
// Point helper class for compass calculations
|
|
||||||
struct Point {
|
|
||||||
float x, y;
|
|
||||||
Point(float x, float y) : x(x), y(y) {}
|
|
||||||
|
|
||||||
void rotate(float angle)
|
|
||||||
{
|
|
||||||
float cos_a = cosf(angle);
|
|
||||||
float sin_a = sinf(angle);
|
|
||||||
float new_x = x * cos_a - y * sin_a;
|
|
||||||
float new_y = x * sin_a + y * cos_a;
|
|
||||||
x = new_x;
|
|
||||||
y = new_y;
|
|
||||||
}
|
|
||||||
|
|
||||||
void scale(float factor)
|
|
||||||
{
|
|
||||||
x *= factor;
|
|
||||||
y *= factor;
|
|
||||||
}
|
|
||||||
|
|
||||||
void translate(float dx, float dy)
|
|
||||||
{
|
|
||||||
x += dx;
|
|
||||||
y += dy;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading, int16_t radius)
|
void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading, int16_t radius)
|
||||||
{
|
{
|
||||||
// Show the compass heading (not implemented in original)
|
|
||||||
// This could draw a "N" indicator or north arrow
|
|
||||||
// For now, we'll draw a simple north indicator
|
|
||||||
// const float radius = 17.0f;
|
|
||||||
if (currentResolution == ScreenResolution::High) {
|
if (currentResolution == ScreenResolution::High) {
|
||||||
radius += 4;
|
radius += 4;
|
||||||
}
|
}
|
||||||
float northX = 0.0f;
|
|
||||||
float northY = -radius;
|
const float northAngle = (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) ? -myHeading : 0.0f;
|
||||||
if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) {
|
const int16_t nX = compassX + static_cast<int16_t>((radius - 1) * sinf(northAngle));
|
||||||
const float c = cosf(-myHeading);
|
const int16_t nY = compassY - static_cast<int16_t>((radius - 1) * cosf(northAngle));
|
||||||
const float s = sinf(-myHeading);
|
|
||||||
const float rx = northX * c - northY * s;
|
|
||||||
const float ry = northX * s + northY * c;
|
|
||||||
northX = rx;
|
|
||||||
northY = ry;
|
|
||||||
}
|
|
||||||
northX += compassX;
|
|
||||||
northY += compassY;
|
|
||||||
|
|
||||||
display->setFont(FONT_SMALL);
|
display->setFont(FONT_SMALL);
|
||||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||||
|
#if !GRAPHICS_TFT_COLORING_ENABLED
|
||||||
display->setColor(BLACK);
|
display->setColor(BLACK);
|
||||||
const int16_t nLabelWidth = display->getStringWidth("N");
|
const int16_t nLabelWidth = display->getStringWidth("N");
|
||||||
if (currentResolution == ScreenResolution::High) {
|
if (currentResolution == ScreenResolution::High) {
|
||||||
display->fillRect(northX - 8, northY - 1, nLabelWidth + 3, FONT_HEIGHT_SMALL - 6);
|
display->fillRect(nX - 8, nY - 1, nLabelWidth + 3, FONT_HEIGHT_SMALL - 6);
|
||||||
} else {
|
} else {
|
||||||
display->fillRect(northX - 4, northY - 1, nLabelWidth + 2, FONT_HEIGHT_SMALL - 6);
|
display->fillRect(nX - 4, nY - 1, nLabelWidth + 2, FONT_HEIGHT_SMALL - 6);
|
||||||
}
|
}
|
||||||
display->setColor(WHITE);
|
|
||||||
display->drawString(northX, northY - 3, "N");
|
|
||||||
}
|
|
||||||
|
|
||||||
void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian)
|
|
||||||
{
|
|
||||||
Point tip(0.0f, -0.5f), tail(0.0f, 0.35f); // pointing up initially
|
|
||||||
float arrowOffsetX = 0.14f, arrowOffsetY = 0.9f;
|
|
||||||
Point leftArrow(tip.x - arrowOffsetX, tip.y + arrowOffsetY), rightArrow(tip.x + arrowOffsetX, tip.y + arrowOffsetY);
|
|
||||||
|
|
||||||
Point *arrowPoints[] = {&tip, &tail, &leftArrow, &rightArrow};
|
|
||||||
|
|
||||||
for (int i = 0; i < 4; i++) {
|
|
||||||
arrowPoints[i]->rotate(headingRadian);
|
|
||||||
arrowPoints[i]->scale(compassDiam * 0.6);
|
|
||||||
arrowPoints[i]->translate(compassX, compassY);
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef USE_EINK
|
|
||||||
display->drawTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y);
|
|
||||||
#else
|
|
||||||
display->fillTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y);
|
|
||||||
#endif
|
#endif
|
||||||
display->drawTriangle(tip.x, tip.y, leftArrow.x, leftArrow.y, tail.x, tail.y);
|
display->setColor(WHITE);
|
||||||
|
display->drawString(nX, nY - 3, "N");
|
||||||
}
|
}
|
||||||
|
|
||||||
void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, float bearing)
|
void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, float bearing)
|
||||||
{
|
{
|
||||||
float radians = bearing * DEG_TO_RAD;
|
const float radians = bearing * DEG_TO_RAD;
|
||||||
|
const float sinA = sinf(radians);
|
||||||
|
const float cosA = cosf(radians);
|
||||||
|
const float tipHalf = size * 0.5f;
|
||||||
|
const float lx = -(size / 6.0f);
|
||||||
|
const float ly = size / 4.0f;
|
||||||
|
const float rx = (size / 6.0f);
|
||||||
|
const float ry = size / 4.0f;
|
||||||
|
const float tx = 0.0f;
|
||||||
|
const float ty = size / 4.5f;
|
||||||
|
|
||||||
Point tip(0, -size / 2);
|
const int16_t tipX = static_cast<int16_t>(x + (tipHalf * sinA));
|
||||||
Point left(-size / 6, size / 4);
|
const int16_t tipY = static_cast<int16_t>(y - (tipHalf * cosA));
|
||||||
Point right(size / 6, size / 4);
|
const int16_t leftX = static_cast<int16_t>(x + (lx * cosA) - (ly * sinA));
|
||||||
Point tail(0, size / 4.5);
|
const int16_t leftY = static_cast<int16_t>(y + (lx * sinA) + (ly * cosA));
|
||||||
|
const int16_t rightX = static_cast<int16_t>(x + (rx * cosA) - (ry * sinA));
|
||||||
|
const int16_t rightY = static_cast<int16_t>(y + (rx * sinA) + (ry * cosA));
|
||||||
|
const int16_t tailX = static_cast<int16_t>(x + (tx * cosA) - (ty * sinA));
|
||||||
|
const int16_t tailY = static_cast<int16_t>(y + (tx * sinA) + (ty * cosA));
|
||||||
|
|
||||||
tip.rotate(radians);
|
display->fillTriangle(tipX, tipY, leftX, leftY, tailX, tailY);
|
||||||
left.rotate(radians);
|
display->fillTriangle(tipX, tipY, rightX, rightY, tailX, tailY);
|
||||||
right.rotate(radians);
|
}
|
||||||
tail.rotate(radians);
|
|
||||||
|
|
||||||
tip.translate(x, y);
|
void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian)
|
||||||
left.translate(x, y);
|
{
|
||||||
right.translate(x, y);
|
const int16_t size = static_cast<int16_t>(compassDiam * 0.6f);
|
||||||
tail.translate(x, y);
|
drawArrowToNode(display, compassX, compassY, size, headingRadian * RAD_TO_DEG);
|
||||||
|
|
||||||
display->fillTriangle(tip.x, tip.y, left.x, left.y, tail.x, tail.y);
|
|
||||||
display->fillTriangle(tip.x, tip.y, right.x, right.y, tail.x, tail.y);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool getHeadingRadians(double lat, double lon, float &headingRadian)
|
bool getHeadingRadians(double lat, double lon, float &headingRadian)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "graphics/Screen.h"
|
#include "graphics/Screen.h"
|
||||||
|
#include "mesh/generated/meshtastic/mesh.pb.h"
|
||||||
#include <OLEDDisplay.h>
|
#include <OLEDDisplay.h>
|
||||||
#include <OLEDDisplayUi.h>
|
#include <OLEDDisplayUi.h>
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
#include "gps/RTC.h"
|
#include "gps/RTC.h"
|
||||||
#include "graphics/ScreenFonts.h"
|
#include "graphics/ScreenFonts.h"
|
||||||
#include "graphics/SharedUIDisplay.h"
|
#include "graphics/SharedUIDisplay.h"
|
||||||
|
#include "graphics/TFTColorRegions.h"
|
||||||
|
#include "graphics/TFTPalette.h"
|
||||||
#include "graphics/TimeFormatters.h"
|
#include "graphics/TimeFormatters.h"
|
||||||
#include "graphics/images.h"
|
#include "graphics/images.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
@@ -469,9 +471,11 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
|||||||
int chUtil_y = getTextPositions(display)[line] + 3;
|
int chUtil_y = getTextPositions(display)[line] + 3;
|
||||||
|
|
||||||
int chutil_bar_width = (currentResolution == ScreenResolution::High) ? 100 : 50;
|
int chutil_bar_width = (currentResolution == ScreenResolution::High) ? 100 : 50;
|
||||||
|
int chutil_bar_max_fill = chutil_bar_width - 2; // Account for border
|
||||||
int chutil_bar_height = (currentResolution == ScreenResolution::High) ? 12 : 7;
|
int chutil_bar_height = (currentResolution == ScreenResolution::High) ? 12 : 7;
|
||||||
int extraoffset = (currentResolution == ScreenResolution::High) ? 6 : 3;
|
int extraoffset = (currentResolution == ScreenResolution::High) ? 6 : 3;
|
||||||
int chutil_percent = airTime->channelUtilizationPercent();
|
int chutil_percent = airTime->channelUtilizationPercent();
|
||||||
|
const int raw_chutil_percent = chutil_percent;
|
||||||
|
|
||||||
int centerofscreen = SCREEN_WIDTH / 2;
|
int centerofscreen = SCREEN_WIDTH / 2;
|
||||||
int total_line_content_width = (chUtil_x + chutil_bar_width + display->getStringWidth(chUtilPercentage) + extraoffset) / 2;
|
int total_line_content_width = (chUtil_x + chutil_bar_width + display->getStringWidth(chUtilPercentage) + extraoffset) / 2;
|
||||||
@@ -479,7 +483,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
|||||||
|
|
||||||
display->drawString(starting_position, getTextPositions(display)[line], chUtil);
|
display->drawString(starting_position, getTextPositions(display)[line], chUtil);
|
||||||
|
|
||||||
// Force 56% or higher to show a full 100% bar, text would still show related percent.
|
// Force 61% or higher to show a full 100% bar, text would still show related percent.
|
||||||
if (chutil_percent >= 61) {
|
if (chutil_percent >= 61) {
|
||||||
chutil_percent = 100;
|
chutil_percent = 100;
|
||||||
}
|
}
|
||||||
@@ -492,9 +496,9 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
|||||||
float weight3 = 0.20; // Weight for 40–100%
|
float weight3 = 0.20; // Weight for 40–100%
|
||||||
float totalWeight = weight1 + weight2 + weight3;
|
float totalWeight = weight1 + weight2 + weight3;
|
||||||
|
|
||||||
int seg1 = chutil_bar_width * (weight1 / totalWeight);
|
int seg1 = chutil_bar_max_fill * (weight1 / totalWeight);
|
||||||
int seg2 = chutil_bar_width * (weight2 / totalWeight);
|
int seg2 = chutil_bar_max_fill * (weight2 / totalWeight);
|
||||||
int seg3 = chutil_bar_width * (weight3 / totalWeight);
|
int seg3 = chutil_bar_max_fill - seg1 - seg2; // Remainder absorbs rounding errors
|
||||||
|
|
||||||
int fillRight = 0;
|
int fillRight = 0;
|
||||||
|
|
||||||
@@ -511,7 +515,17 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
|||||||
|
|
||||||
// Fill progress
|
// Fill progress
|
||||||
if (fillRight > 0) {
|
if (fillRight > 0) {
|
||||||
display->fillRect(starting_position + chUtil_x, chUtil_y, fillRight, chutil_bar_height);
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
uint16_t UtilizationFillColor = TFTPalette::Good;
|
||||||
|
if (raw_chutil_percent >= 60) {
|
||||||
|
UtilizationFillColor = TFTPalette::Bad;
|
||||||
|
} else if (raw_chutil_percent >= 35) {
|
||||||
|
UtilizationFillColor = TFTPalette::Medium;
|
||||||
|
}
|
||||||
|
setAndRegisterTFTColorRole(TFTColorRole::UtilizationFill, UtilizationFillColor, TFTPalette::Black,
|
||||||
|
starting_position + chUtil_x + 1, chUtil_y + 1, fillRight, chutil_bar_height - 2);
|
||||||
|
#endif
|
||||||
|
display->fillRect(starting_position + chUtil_x + 1, chUtil_y + 1, fillRight, chutil_bar_height - 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[line++],
|
display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[line++],
|
||||||
@@ -584,6 +598,17 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
|
|||||||
display->setColor(WHITE);
|
display->setColor(WHITE);
|
||||||
display->drawRect(barX, barY, adjustedBarWidth, barHeight);
|
display->drawRect(barX, barY, adjustedBarWidth, barHeight);
|
||||||
|
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
uint16_t UtilizationFillColor = TFTPalette::Good;
|
||||||
|
if (percent >= 80) {
|
||||||
|
UtilizationFillColor = TFTPalette::Bad;
|
||||||
|
} else if (percent >= 60) {
|
||||||
|
UtilizationFillColor = TFTPalette::Medium;
|
||||||
|
}
|
||||||
|
setAndRegisterTFTColorRole(TFTColorRole::UtilizationFill, UtilizationFillColor, TFTPalette::Black, barX + 1, barY + 1,
|
||||||
|
fillWidth - 1, barHeight - 2);
|
||||||
|
#endif
|
||||||
|
|
||||||
display->fillRect(barX, barY, fillWidth, barHeight);
|
display->fillRect(barX, barY, fillWidth, barHeight);
|
||||||
display->setColor(WHITE);
|
display->setColor(WHITE);
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include "buzz.h"
|
#include "buzz.h"
|
||||||
#include "graphics/Screen.h"
|
#include "graphics/Screen.h"
|
||||||
#include "graphics/SharedUIDisplay.h"
|
#include "graphics/SharedUIDisplay.h"
|
||||||
|
#include "graphics/TFTColorRegions.h"
|
||||||
#include "graphics/draw/MessageRenderer.h"
|
#include "graphics/draw/MessageRenderer.h"
|
||||||
#include "graphics/draw/UIRenderer.h"
|
#include "graphics/draw/UIRenderer.h"
|
||||||
#include "input/RotaryEncoderInterruptImpl1.h"
|
#include "input/RotaryEncoderInterruptImpl1.h"
|
||||||
@@ -30,8 +31,6 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
extern uint16_t TFT_MESH;
|
|
||||||
|
|
||||||
namespace graphics
|
namespace graphics
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -2028,109 +2027,6 @@ void menuHandler::switchToMUIMenu()
|
|||||||
screen->showOverlayBanner(bannerOptions);
|
screen->showOverlayBanner(bannerOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
void menuHandler::TFTColorPickerMenu(OLEDDisplay *display)
|
|
||||||
{
|
|
||||||
static const ScreenColorOption colorOptions[] = {
|
|
||||||
{"Back", OptionsAction::Back},
|
|
||||||
{"Default", OptionsAction::Select, ScreenColor(0, 0, 0, true)},
|
|
||||||
{"Meshtastic Green", OptionsAction::Select, ScreenColor(0x67, 0xEA, 0x94)},
|
|
||||||
{"Yellow", OptionsAction::Select, ScreenColor(255, 255, 128)},
|
|
||||||
{"Red", OptionsAction::Select, ScreenColor(255, 64, 64)},
|
|
||||||
{"Orange", OptionsAction::Select, ScreenColor(255, 160, 20)},
|
|
||||||
{"Purple", OptionsAction::Select, ScreenColor(204, 153, 255)},
|
|
||||||
{"Blue", OptionsAction::Select, ScreenColor(0, 0, 255)},
|
|
||||||
{"Teal", OptionsAction::Select, ScreenColor(16, 102, 102)},
|
|
||||||
{"Cyan", OptionsAction::Select, ScreenColor(0, 255, 255)},
|
|
||||||
{"Ice", OptionsAction::Select, ScreenColor(173, 216, 230)},
|
|
||||||
{"Pink", OptionsAction::Select, ScreenColor(255, 105, 180)},
|
|
||||||
{"White", OptionsAction::Select, ScreenColor(255, 255, 255)},
|
|
||||||
{"Gray", OptionsAction::Select, ScreenColor(128, 128, 128)},
|
|
||||||
};
|
|
||||||
|
|
||||||
constexpr size_t colorCount = sizeof(colorOptions) / sizeof(colorOptions[0]);
|
|
||||||
static std::array<const char *, colorCount> colorLabels{};
|
|
||||||
|
|
||||||
auto bannerOptions = createStaticBannerOptions(
|
|
||||||
"Select Screen Color", colorOptions, colorLabels, [display](const ScreenColorOption &option, int) -> void {
|
|
||||||
if (option.action == OptionsAction::Back) {
|
|
||||||
menuQueue = SystemBaseMenu;
|
|
||||||
screen->runNow();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!option.hasValue) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || \
|
|
||||||
HAS_TFT || defined(HACKADAY_COMMUNICATOR)
|
|
||||||
const ScreenColor &color = option.value;
|
|
||||||
if (color.useVariant) {
|
|
||||||
LOG_INFO("Setting color to system default or defined variant");
|
|
||||||
} else {
|
|
||||||
LOG_INFO("Setting color to %s", option.label);
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t r = color.r;
|
|
||||||
uint8_t g = color.g;
|
|
||||||
uint8_t b = color.b;
|
|
||||||
|
|
||||||
display->setColor(BLACK);
|
|
||||||
display->fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
|
|
||||||
display->setColor(WHITE);
|
|
||||||
|
|
||||||
if (color.useVariant || (r == 0 && g == 0 && b == 0)) {
|
|
||||||
#ifdef TFT_MESH_OVERRIDE
|
|
||||||
TFT_MESH = TFT_MESH_OVERRIDE;
|
|
||||||
#else
|
|
||||||
TFT_MESH = COLOR565(255, 255, 128);
|
|
||||||
#endif
|
|
||||||
} else {
|
|
||||||
TFT_MESH = COLOR565(r, g, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190)
|
|
||||||
static_cast<ST7789Spi *>(screen->getDisplayDevice())->setRGB(TFT_MESH);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
screen->setFrames(graphics::Screen::FOCUS_SYSTEM);
|
|
||||||
if (color.useVariant || (r == 0 && g == 0 && b == 0)) {
|
|
||||||
uiconfig.screen_rgb_color = 0;
|
|
||||||
} else {
|
|
||||||
uiconfig.screen_rgb_color =
|
|
||||||
(static_cast<uint32_t>(r) << 16) | (static_cast<uint32_t>(g) << 8) | static_cast<uint32_t>(b);
|
|
||||||
}
|
|
||||||
LOG_INFO("Storing Value of %d to uiconfig.screen_rgb_color", uiconfig.screen_rgb_color);
|
|
||||||
saveUIConfig();
|
|
||||||
#endif
|
|
||||||
});
|
|
||||||
|
|
||||||
int initialSelection = 0;
|
|
||||||
if (uiconfig.screen_rgb_color == 0) {
|
|
||||||
initialSelection = 1;
|
|
||||||
} else {
|
|
||||||
uint32_t currentColor = uiconfig.screen_rgb_color;
|
|
||||||
for (size_t i = 0; i < colorCount; ++i) {
|
|
||||||
if (!colorOptions[i].hasValue) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const ScreenColor &color = colorOptions[i].value;
|
|
||||||
if (color.useVariant) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
uint32_t encoded =
|
|
||||||
(static_cast<uint32_t>(color.r) << 16) | (static_cast<uint32_t>(color.g) << 8) | static_cast<uint32_t>(color.b);
|
|
||||||
if (encoded == currentColor) {
|
|
||||||
initialSelection = static_cast<int>(i);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
bannerOptions.InitialSelected = initialSelection;
|
|
||||||
|
|
||||||
screen->showOverlayBanner(bannerOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
void menuHandler::rebootMenu()
|
void menuHandler::rebootMenu()
|
||||||
{
|
{
|
||||||
static const char *optionsArray[] = {"Back", "Confirm"};
|
static const char *optionsArray[] = {"Back", "Confirm"};
|
||||||
@@ -2318,9 +2214,9 @@ void menuHandler::screenOptionsMenu()
|
|||||||
bool hasSupportBrightness = false;
|
bool hasSupportBrightness = false;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits, MessageBubbles };
|
enum optionsNumbers { Back, Brightness, FrameToggles, DisplayUnits, MessageBubbles, Theme };
|
||||||
static const char *optionsArray[6] = {"Back"};
|
static const char *optionsArray[7] = {"Back"};
|
||||||
static int optionsEnumArray[6] = {Back};
|
static int optionsEnumArray[7] = {Back};
|
||||||
int options = 1;
|
int options = 1;
|
||||||
|
|
||||||
// Only show brightness for B&W displays
|
// Only show brightness for B&W displays
|
||||||
@@ -2329,13 +2225,6 @@ void menuHandler::screenOptionsMenu()
|
|||||||
optionsEnumArray[options++] = Brightness;
|
optionsEnumArray[options++] = Brightness;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only show screen color for TFT displays
|
|
||||||
#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || \
|
|
||||||
HAS_TFT || defined(HACKADAY_COMMUNICATOR)
|
|
||||||
optionsArray[options] = "Screen Color";
|
|
||||||
optionsEnumArray[options++] = ScreenColor;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
optionsArray[options] = "Frame Visibility";
|
optionsArray[options] = "Frame Visibility";
|
||||||
optionsEnumArray[options++] = FrameToggles;
|
optionsEnumArray[options++] = FrameToggles;
|
||||||
|
|
||||||
@@ -2345,6 +2234,11 @@ void menuHandler::screenOptionsMenu()
|
|||||||
optionsArray[options] = "Message Bubbles";
|
optionsArray[options] = "Message Bubbles";
|
||||||
optionsEnumArray[options++] = MessageBubbles;
|
optionsEnumArray[options++] = MessageBubbles;
|
||||||
|
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
optionsArray[options] = "Theme";
|
||||||
|
optionsEnumArray[options++] = Theme;
|
||||||
|
#endif
|
||||||
|
|
||||||
BannerOverlayOptions bannerOptions;
|
BannerOverlayOptions bannerOptions;
|
||||||
bannerOptions.message = "Display Options";
|
bannerOptions.message = "Display Options";
|
||||||
bannerOptions.optionsArrayPtr = optionsArray;
|
bannerOptions.optionsArrayPtr = optionsArray;
|
||||||
@@ -2354,9 +2248,6 @@ void menuHandler::screenOptionsMenu()
|
|||||||
if (selected == Brightness) {
|
if (selected == Brightness) {
|
||||||
menuHandler::menuQueue = menuHandler::BrightnessPicker;
|
menuHandler::menuQueue = menuHandler::BrightnessPicker;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
} else if (selected == ScreenColor) {
|
|
||||||
menuHandler::menuQueue = menuHandler::TftColorMenuPicker;
|
|
||||||
screen->runNow();
|
|
||||||
} else if (selected == FrameToggles) {
|
} else if (selected == FrameToggles) {
|
||||||
menuHandler::menuQueue = menuHandler::FrameToggles;
|
menuHandler::menuQueue = menuHandler::FrameToggles;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
@@ -2366,6 +2257,9 @@ void menuHandler::screenOptionsMenu()
|
|||||||
} else if (selected == MessageBubbles) {
|
} else if (selected == MessageBubbles) {
|
||||||
menuHandler::menuQueue = menuHandler::MessageBubblesMenu;
|
menuHandler::menuQueue = menuHandler::MessageBubblesMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
|
} else if (selected == Theme) {
|
||||||
|
menuHandler::menuQueue = menuHandler::ThemeMenu;
|
||||||
|
screen->runNow();
|
||||||
} else {
|
} else {
|
||||||
menuQueue = SystemBaseMenu;
|
menuQueue = SystemBaseMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
@@ -2649,6 +2543,53 @@ void menuHandler::messageBubblesMenu()
|
|||||||
screen->showOverlayBanner(bannerOptions);
|
screen->showOverlayBanner(bannerOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void menuHandler::themeMenu()
|
||||||
|
{
|
||||||
|
// Build menu dynamically from the theme table.
|
||||||
|
// Only visible themes appear!
|
||||||
|
// Slot budget: 1 for "Back" + up to kMaxThemesInMenu visible themes.
|
||||||
|
// Bump kMaxThemesInMenu if you add more themes than will fit here.
|
||||||
|
constexpr size_t kMaxThemesInMenu = 15;
|
||||||
|
const size_t visibleCount = getVisibleThemeCount();
|
||||||
|
static const char *optionsArray[kMaxThemesInMenu + 1] = {"Back"};
|
||||||
|
const size_t shownCount = (visibleCount < kMaxThemesInMenu) ? visibleCount : kMaxThemesInMenu;
|
||||||
|
const int options = static_cast<int>(shownCount) + 1; // +1 for Back
|
||||||
|
|
||||||
|
for (size_t i = 0; i < shownCount; i++) {
|
||||||
|
optionsArray[i + 1] = getVisibleThemeByIndex(i).name;
|
||||||
|
}
|
||||||
|
|
||||||
|
BannerOverlayOptions bannerOptions;
|
||||||
|
bannerOptions.message = "Theme";
|
||||||
|
bannerOptions.optionsArrayPtr = optionsArray;
|
||||||
|
bannerOptions.optionsCount = options;
|
||||||
|
|
||||||
|
// Highlight the currently active theme (visible index + 1 for the Back
|
||||||
|
// offset). If the active theme is hidden, leave selection on "Back".
|
||||||
|
const size_t activeVisible = getActiveVisibleThemeIndex();
|
||||||
|
bannerOptions.InitialSelected = (activeVisible == SIZE_MAX) ? 0 : static_cast<int>(activeVisible) + 1;
|
||||||
|
|
||||||
|
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||||
|
if (selected == 0) {
|
||||||
|
// Back
|
||||||
|
menuHandler::menuQueue = menuHandler::ScreenOptionsMenu;
|
||||||
|
screen->runNow();
|
||||||
|
} else {
|
||||||
|
// Selection is an index into the VISIBLE themes (1-based, slot 0 is Back).
|
||||||
|
const size_t visibleIdx = static_cast<size_t>(selected - 1);
|
||||||
|
if (visibleIdx < getVisibleThemeCount()) {
|
||||||
|
// Persist the theme's uniqueIdentifier so boot-time
|
||||||
|
// resolveThemeIndex() can restore this theme on next startup.
|
||||||
|
uiconfig.screen_rgb_color = COLOR565(255, 255, (getVisibleThemeByIndex(visibleIdx).uniqueIdentifier & 0x1F) << 3);
|
||||||
|
loadThemeDefaults();
|
||||||
|
saveUIConfig();
|
||||||
|
screen->runNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
screen->showOverlayBanner(bannerOptions);
|
||||||
|
}
|
||||||
|
|
||||||
void menuHandler::handleMenuSwitch(OLEDDisplay *display)
|
void menuHandler::handleMenuSwitch(OLEDDisplay *display)
|
||||||
{
|
{
|
||||||
if (menuQueue != MenuNone)
|
if (menuQueue != MenuNone)
|
||||||
@@ -2724,9 +2665,6 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
|
|||||||
case MuiPicker:
|
case MuiPicker:
|
||||||
switchToMUIMenu();
|
switchToMUIMenu();
|
||||||
break;
|
break;
|
||||||
case TftColorMenuPicker:
|
|
||||||
TFTColorPickerMenu(display);
|
|
||||||
break;
|
|
||||||
case BrightnessPicker:
|
case BrightnessPicker:
|
||||||
BrightnessPickerMenu();
|
BrightnessPickerMenu();
|
||||||
break;
|
break;
|
||||||
@@ -2799,6 +2737,9 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
|
|||||||
case MessageBubblesMenu:
|
case MessageBubblesMenu:
|
||||||
messageBubblesMenu();
|
messageBubblesMenu();
|
||||||
break;
|
break;
|
||||||
|
case ThemeMenu:
|
||||||
|
themeMenu();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
menuQueue = MenuNone;
|
menuQueue = MenuNone;
|
||||||
}
|
}
|
||||||
@@ -2810,4 +2751,4 @@ void menuHandler::saveUIConfig()
|
|||||||
|
|
||||||
} // namespace graphics
|
} // namespace graphics
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ class menuHandler
|
|||||||
ResetNodeDbMenu,
|
ResetNodeDbMenu,
|
||||||
BuzzerModeMenuPicker,
|
BuzzerModeMenuPicker,
|
||||||
MuiPicker,
|
MuiPicker,
|
||||||
TftColorMenuPicker,
|
|
||||||
BrightnessPicker,
|
BrightnessPicker,
|
||||||
RebootMenu,
|
RebootMenu,
|
||||||
ShutdownMenu,
|
ShutdownMenu,
|
||||||
@@ -55,7 +54,8 @@ class menuHandler
|
|||||||
NodeNameLengthMenu,
|
NodeNameLengthMenu,
|
||||||
FrameToggles,
|
FrameToggles,
|
||||||
DisplayUnits,
|
DisplayUnits,
|
||||||
MessageBubblesMenu
|
MessageBubblesMenu,
|
||||||
|
ThemeMenu
|
||||||
};
|
};
|
||||||
static screenMenus menuQueue;
|
static screenMenus menuQueue;
|
||||||
static uint32_t pickedNodeNum; // node selected by NodePicker for ManageNodeMenu
|
static uint32_t pickedNodeNum; // node selected by NodePicker for ManageNodeMenu
|
||||||
@@ -89,7 +89,6 @@ class menuHandler
|
|||||||
static void GPSPositionBroadcastMenu();
|
static void GPSPositionBroadcastMenu();
|
||||||
static void BuzzerModeMenu();
|
static void BuzzerModeMenu();
|
||||||
static void switchToMUIMenu();
|
static void switchToMUIMenu();
|
||||||
static void TFTColorPickerMenu(OLEDDisplay *display);
|
|
||||||
static void nodeListMenu();
|
static void nodeListMenu();
|
||||||
static void resetNodeDBMenu();
|
static void resetNodeDBMenu();
|
||||||
static void BrightnessPickerMenu();
|
static void BrightnessPickerMenu();
|
||||||
@@ -110,6 +109,7 @@ class menuHandler
|
|||||||
static void frameTogglesMenu();
|
static void frameTogglesMenu();
|
||||||
static void displayUnitsMenu();
|
static void displayUnitsMenu();
|
||||||
static void messageBubblesMenu();
|
static void messageBubblesMenu();
|
||||||
|
static void themeMenu();
|
||||||
static void textMessageMenu();
|
static void textMessageMenu();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -136,23 +136,10 @@ template <typename T> struct MenuOption {
|
|||||||
MenuOption(const char *labelIn, OptionsAction actionIn) : label(labelIn), action(actionIn), hasValue(false), value() {}
|
MenuOption(const char *labelIn, OptionsAction actionIn) : label(labelIn), action(actionIn), hasValue(false), value() {}
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ScreenColor {
|
|
||||||
uint8_t r;
|
|
||||||
uint8_t g;
|
|
||||||
uint8_t b;
|
|
||||||
bool useVariant;
|
|
||||||
|
|
||||||
explicit ScreenColor(uint8_t rIn = 0, uint8_t gIn = 0, uint8_t bIn = 0, bool variantIn = false)
|
|
||||||
: r(rIn), g(gIn), b(bIn), useVariant(variantIn)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
using RadioPresetOption = MenuOption<meshtastic_Config_LoRaConfig_ModemPreset>;
|
using RadioPresetOption = MenuOption<meshtastic_Config_LoRaConfig_ModemPreset>;
|
||||||
using LoraRegionOption = MenuOption<meshtastic_Config_LoRaConfig_RegionCode>;
|
using LoraRegionOption = MenuOption<meshtastic_Config_LoRaConfig_RegionCode>;
|
||||||
using TimezoneOption = MenuOption<const char *>;
|
using TimezoneOption = MenuOption<const char *>;
|
||||||
using CompassOption = MenuOption<meshtastic_CompassMode>;
|
using CompassOption = MenuOption<meshtastic_CompassMode>;
|
||||||
using ScreenColorOption = MenuOption<ScreenColor>;
|
|
||||||
using GPSToggleOption = MenuOption<meshtastic_Config_PositionConfig_GpsMode>;
|
using GPSToggleOption = MenuOption<meshtastic_Config_PositionConfig_GpsMode>;
|
||||||
using GPSFormatOption = MenuOption<meshtastic_DeviceUIConfig_GpsCoordinateFormat>;
|
using GPSFormatOption = MenuOption<meshtastic_DeviceUIConfig_GpsCoordinateFormat>;
|
||||||
using NodeNameOption = MenuOption<bool>;
|
using NodeNameOption = MenuOption<bool>;
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
#include "graphics/Screen.h"
|
#include "graphics/Screen.h"
|
||||||
#include "graphics/ScreenFonts.h"
|
#include "graphics/ScreenFonts.h"
|
||||||
#include "graphics/SharedUIDisplay.h"
|
#include "graphics/SharedUIDisplay.h"
|
||||||
|
#include "graphics/TFTColorRegions.h"
|
||||||
|
#include "graphics/TFTPalette.h"
|
||||||
#include "graphics/TimeFormatters.h"
|
#include "graphics/TimeFormatters.h"
|
||||||
#include "graphics/emotes.h"
|
#include "graphics/emotes.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
@@ -254,6 +256,76 @@ struct MessageBlock {
|
|||||||
bool mine;
|
bool mine;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
static void setDarkModeBubbleRoleColors(uint32_t themeId, bool mine)
|
||||||
|
{
|
||||||
|
uint16_t bubbleOnColor;
|
||||||
|
uint16_t bubbleOffColor;
|
||||||
|
|
||||||
|
if (themeId == ThemeID::Blue) {
|
||||||
|
bubbleOnColor = mine ? TFTPalette::Navy : TFTPalette::White;
|
||||||
|
bubbleOffColor = mine ? TFTPalette::SkyBlue : TFTPalette::DeepBlue;
|
||||||
|
} else {
|
||||||
|
bubbleOnColor = mine ? TFTPalette::Black : getThemeBodyFg();
|
||||||
|
bubbleOffColor = mine ? TFTPalette::SkyBlue : TFTPalette::DarkGray;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTFTColorRole(TFTColorRole::ActionMenuBody, bubbleOnColor, bubbleOffColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void registerRoundedBubbleFillRegion(int x, int y, int w, int h, int radius)
|
||||||
|
{
|
||||||
|
if (w <= 0 || h <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (radius <= 0 || w < 3 || h < 3) {
|
||||||
|
registerTFTColorRegion(TFTColorRole::ActionMenuBody, x, y, w, h);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep region count low so we don't churn MAX_TFT_COLOR_REGIONS while
|
||||||
|
// scrolling long message lists (which can flatten older bubble corners).
|
||||||
|
int capRows = 0;
|
||||||
|
if (radius >= 4 && h >= 5) {
|
||||||
|
capRows = 2; // 5 regions total (2 top caps + middle + 2 bottom caps)
|
||||||
|
} else if (radius >= 2 && h >= 3) {
|
||||||
|
capRows = 1; // 3 regions total
|
||||||
|
}
|
||||||
|
if (capRows <= 0) {
|
||||||
|
registerTFTColorRegion(TFTColorRole::ActionMenuBody, x, y, w, h);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int row = 0; row < capRows; ++row) {
|
||||||
|
int inset = 0;
|
||||||
|
if (radius >= 4) {
|
||||||
|
inset = (row == 0) ? 2 : 1;
|
||||||
|
} else if (radius >= 2) {
|
||||||
|
inset = 1;
|
||||||
|
}
|
||||||
|
const int stripW = w - (inset * 2);
|
||||||
|
if (stripW <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int topY = y + row;
|
||||||
|
registerTFTColorRegion(TFTColorRole::ActionMenuBody, x + inset, topY, stripW, 1);
|
||||||
|
|
||||||
|
const int bottomY = y + h - 1 - row;
|
||||||
|
if (bottomY != topY) {
|
||||||
|
registerTFTColorRegion(TFTColorRole::ActionMenuBody, x + inset, bottomY, stripW, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const int middleY = y + capRows;
|
||||||
|
const int middleH = h - (capRows * 2);
|
||||||
|
if (middleH > 0) {
|
||||||
|
registerTFTColorRegion(TFTColorRole::ActionMenuBody, x, middleY, w, middleH);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
static int getDrawnLinePixelBottom(int lineTopY, const std::string &line, bool isHeaderLine)
|
static int getDrawnLinePixelBottom(int lineTopY, const std::string &line, bool isHeaderLine)
|
||||||
{
|
{
|
||||||
if (isHeaderLine) {
|
if (isHeaderLine) {
|
||||||
@@ -648,6 +720,11 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
const int contentBottom = scrollBottom; // already excludes nav line
|
const int contentBottom = scrollBottom; // already excludes nav line
|
||||||
const int rightEdge = SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN;
|
const int rightEdge = SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN;
|
||||||
const int bubbleGapY = std::max(1, MESSAGE_BLOCK_GAP / 2);
|
const int bubbleGapY = std::max(1, MESSAGE_BLOCK_GAP / 2);
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
const uint32_t themeId = getActiveTheme().id;
|
||||||
|
// Blue is a dark variant but uses full frame inversion, Keep it on the same filled bubble style as Default Dark.
|
||||||
|
const bool useDarkModeBubbleFill = showBubbles && (!isThemeFullFrameInvert() || themeId == ThemeID::Blue);
|
||||||
|
#endif
|
||||||
|
|
||||||
std::vector<int> lineTop;
|
std::vector<int> lineTop;
|
||||||
lineTop.resize(cachedLines.size());
|
lineTop.resize(cachedLines.size());
|
||||||
@@ -686,6 +763,17 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
int visualBottom = getDrawnLinePixelBottom(lineTop[b.end], cachedLines[b.end], isHeader[b.end]);
|
int visualBottom = getDrawnLinePixelBottom(lineTop[b.end], cachedLines[b.end], isHeader[b.end]);
|
||||||
int bottomY = visualBottom + BUBBLE_PAD_Y;
|
int bottomY = visualBottom + BUBBLE_PAD_Y;
|
||||||
|
|
||||||
|
// On high-res screens, keep a 1px gap under the header
|
||||||
|
if (currentResolution == ScreenResolution::High) {
|
||||||
|
const int minTopY = contentTop + 1;
|
||||||
|
if (topY < minTopY) {
|
||||||
|
// Preserve bubble height when we push it down from the header.
|
||||||
|
const int shift = minTopY - topY;
|
||||||
|
topY = minTopY;
|
||||||
|
bottomY += shift;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (bi + 1 < blocks.size()) {
|
if (bi + 1 < blocks.size()) {
|
||||||
int nextHeaderIndex = (int)blocks[bi + 1].start;
|
int nextHeaderIndex = (int)blocks[bi + 1].start;
|
||||||
int nextTop = lineTop[nextHeaderIndex];
|
int nextTop = lineTop[nextHeaderIndex];
|
||||||
@@ -735,24 +823,56 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
const int by = topY;
|
const int by = topY;
|
||||||
const int bw = bubbleW;
|
const int bw = bubbleW;
|
||||||
const int bh = bubbleH;
|
const int bh = bubbleH;
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
const bool drawBubbleOutline = !useDarkModeBubbleFill;
|
||||||
|
#else
|
||||||
|
const bool drawBubbleOutline = true;
|
||||||
|
#endif
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
if (useDarkModeBubbleFill) {
|
||||||
|
setDarkModeBubbleRoleColors(themeId, b.mine);
|
||||||
|
registerRoundedBubbleFillRegion(bx, by, bw, bh, r);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// Draw the 4 corner arcs using drawCircleQuads
|
if (drawBubbleOutline) {
|
||||||
display->drawCircleQuads(bx + r, by + r, r, 0x2); // Top-left
|
// Draw the 4 corner arcs using drawCircleQuads
|
||||||
display->drawCircleQuads(bx + bw - r - 1, by + r, r, 0x1); // Top-right
|
display->drawCircleQuads(bx + r, by + r, r, 0x2); // Top-left
|
||||||
display->drawCircleQuads(bx + r, by + bh - r - 1, r, 0x4); // Bottom-left
|
display->drawCircleQuads(bx + bw - r - 1, by + r, r, 0x1); // Top-right
|
||||||
display->drawCircleQuads(bx + bw - r - 1, by + bh - r - 1, r, 0x8); // Bottom-right
|
display->drawCircleQuads(bx + r, by + bh - r - 1, r, 0x4); // Bottom-left
|
||||||
|
display->drawCircleQuads(bx + bw - r - 1, by + bh - r - 1, r, 0x8); // Bottom-right
|
||||||
|
|
||||||
// Draw the 4 edges between corners
|
// Draw the 4 edges between corners
|
||||||
display->drawHorizontalLine(bx + r, by, bw - 2 * r); // Top edge
|
display->drawHorizontalLine(bx + r, by, bw - 2 * r); // Top edge
|
||||||
display->drawHorizontalLine(bx + r, by + bh - 1, bw - 2 * r); // Bottom edge
|
display->drawHorizontalLine(bx + r, by + bh - 1, bw - 2 * r); // Bottom edge
|
||||||
display->drawVerticalLine(bx, by + r, bh - 2 * r); // Left edge
|
display->drawVerticalLine(bx, by + r, bh - 2 * r); // Left edge
|
||||||
display->drawVerticalLine(bx + bw - 1, by + r, bh - 2 * r); // Right edge
|
display->drawVerticalLine(bx + bw - 1, by + r, bh - 2 * r); // Right edge
|
||||||
|
}
|
||||||
} else if (bubbleW > 1 && bubbleH > 1) {
|
} else if (bubbleW > 1 && bubbleH > 1) {
|
||||||
// Fallback to simple rectangle for very small bubbles
|
// Fallback to simple rectangle for very small bubbles
|
||||||
display->drawRect(bubbleX, topY, bubbleW, bubbleH);
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
const bool drawBubbleOutline = !useDarkModeBubbleFill;
|
||||||
|
#else
|
||||||
|
const bool drawBubbleOutline = true;
|
||||||
|
#endif
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
if (useDarkModeBubbleFill) {
|
||||||
|
setDarkModeBubbleRoleColors(themeId, b.mine);
|
||||||
|
registerTFTColorRegion(TFTColorRole::ActionMenuBody, bubbleX, topY, bubbleW, bubbleH);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
if (drawBubbleOutline) {
|
||||||
|
display->drawRect(bubbleX, topY, bubbleW, bubbleH);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} // end if (showBubbles)
|
} // end if (showBubbles)
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
if (useDarkModeBubbleFill) {
|
||||||
|
// Restore theme role defaults so other screens keep their intended palette.
|
||||||
|
loadThemeDefaults();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// Render visible lines
|
// Render visible lines
|
||||||
int lineY = yOffset;
|
int lineY = yOffset;
|
||||||
@@ -772,7 +892,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
headerX = x + textIndent;
|
headerX = x + textIndent;
|
||||||
}
|
}
|
||||||
graphics::UIRenderer::drawStringWithEmotes(display, headerX, lineY, cachedLines[i].c_str(), FONT_HEIGHT_SMALL, 1,
|
graphics::UIRenderer::drawStringWithEmotes(display, headerX, lineY, cachedLines[i].c_str(), FONT_HEIGHT_SMALL, 1,
|
||||||
false);
|
true);
|
||||||
|
|
||||||
// Draw underline just under header text
|
// Draw underline just under header text
|
||||||
int underlineY = lineY + FONT_HEIGHT_SMALL;
|
int underlineY = lineY + FONT_HEIGHT_SMALL;
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
#include "gps/RTC.h" // for getTime() function
|
#include "gps/RTC.h" // for getTime() function
|
||||||
#include "graphics/ScreenFonts.h"
|
#include "graphics/ScreenFonts.h"
|
||||||
#include "graphics/SharedUIDisplay.h"
|
#include "graphics/SharedUIDisplay.h"
|
||||||
|
#include "graphics/TFTColorRegions.h"
|
||||||
|
#include "graphics/TFTPalette.h"
|
||||||
#include "graphics/images.h"
|
#include "graphics/images.h"
|
||||||
#include "meshUtils.h"
|
#include "meshUtils.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@@ -213,6 +215,33 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static inline void applyFavoriteNodeNameColor(OLEDDisplay *display, const meshtastic_NodeInfoLite *node, const char *nodeName,
|
||||||
|
int16_t nameX, int16_t y, int nameMaxWidth)
|
||||||
|
{
|
||||||
|
if (!display || !node || !node->is_favorite || !isTFTColoringEnabled() || !nodeName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int textWidth = UIRenderer::measureStringWithEmotes(display, nodeName);
|
||||||
|
const int regionWidth = min(textWidth, max(0, nameMaxWidth));
|
||||||
|
if (regionWidth <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Node list rows can begin a couple of pixels inside header space.
|
||||||
|
// Clamp favorite-name color region below the header to avoid black overlap there.
|
||||||
|
const int16_t minContentY = static_cast<int16_t>(FONT_HEIGHT_SMALL + 1);
|
||||||
|
const int16_t regionY = max(y, minContentY);
|
||||||
|
const int16_t yClip = regionY - y;
|
||||||
|
const int16_t regionHeight = static_cast<int16_t>(FONT_HEIGHT_SMALL - yClip);
|
||||||
|
if (regionHeight <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAndRegisterTFTColorRole(TFTColorRole::FavoriteNode, TFTPalette::Yellow, TFTPalette::Black, nameX, regionY, regionWidth,
|
||||||
|
regionHeight);
|
||||||
|
}
|
||||||
|
|
||||||
// =============================
|
// =============================
|
||||||
// Entry Renderers
|
// Entry Renderers
|
||||||
// =============================
|
// =============================
|
||||||
@@ -227,6 +256,9 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
|
|||||||
char nodeName[96];
|
char nodeName[96];
|
||||||
UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName),
|
UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName),
|
||||||
nameMaxWidth);
|
nameMaxWidth);
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth);
|
||||||
|
#endif
|
||||||
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
|
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
|
||||||
|
|
||||||
char timeStr[10];
|
char timeStr[10];
|
||||||
@@ -286,6 +318,9 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
|
|||||||
char nodeName[96];
|
char nodeName[96];
|
||||||
UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName),
|
UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName),
|
||||||
nameMaxWidth);
|
nameMaxWidth);
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth);
|
||||||
|
#endif
|
||||||
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
|
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
|
||||||
|
|
||||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
@@ -315,6 +350,19 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
|
|||||||
int barStartX = x + barsXOffset;
|
int barStartX = x + barsXOffset;
|
||||||
int barStartY = y + 1 + (FONT_HEIGHT_SMALL / 2) + 2;
|
int barStartY = y + 1 + (FONT_HEIGHT_SMALL / 2) + 2;
|
||||||
|
|
||||||
|
if (bars > 0) {
|
||||||
|
uint16_t signalBarsColor = TFTPalette::Bad;
|
||||||
|
if (bars >= 3) {
|
||||||
|
signalBarsColor = TFTPalette::Good;
|
||||||
|
} else if (bars == 2) {
|
||||||
|
signalBarsColor = TFTPalette::Medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Highest bar reaches 6 px in this renderer.
|
||||||
|
setAndRegisterTFTColorRole(TFTColorRole::SignalBars, signalBarsColor, TFTPalette::Black, barStartX, barStartY - 6,
|
||||||
|
(kBarCount * kBarWidth) + ((kBarCount - 1) * kBarGap), 6);
|
||||||
|
}
|
||||||
|
|
||||||
for (int b = 0; b < kBarCount; b++) {
|
for (int b = 0; b < kBarCount; b++) {
|
||||||
if (b < bars) {
|
if (b < bars) {
|
||||||
int height = (b * 2);
|
int height = (b * 2);
|
||||||
@@ -350,6 +398,9 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
|||||||
char nodeName[96];
|
char nodeName[96];
|
||||||
UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName),
|
UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName),
|
||||||
nameMaxWidth);
|
nameMaxWidth);
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth);
|
||||||
|
#endif
|
||||||
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
|
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
|
||||||
char distStr[10] = "";
|
char distStr[10] = "";
|
||||||
|
|
||||||
@@ -455,6 +506,9 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
|||||||
char nodeName[96];
|
char nodeName[96];
|
||||||
UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName),
|
UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName),
|
||||||
nameMaxWidth);
|
nameMaxWidth);
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth);
|
||||||
|
#endif
|
||||||
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
|
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
|
||||||
|
|
||||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
@@ -710,6 +764,9 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
|||||||
display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1);
|
display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1);
|
||||||
display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1);
|
display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1);
|
||||||
display->setColor(WHITE);
|
display->setColor(WHITE);
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
registerTFTActionMenuRegions(boxLeft, boxTop, boxWidth, boxHeight);
|
||||||
|
#endif
|
||||||
|
|
||||||
// Text
|
// Text
|
||||||
display->drawString(boxLeft + padding, boxTop + padding, buf);
|
display->drawString(boxLeft + padding, boxTop + padding, buf);
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
#include "UIRenderer.h"
|
#include "UIRenderer.h"
|
||||||
#include "graphics/ScreenFonts.h"
|
#include "graphics/ScreenFonts.h"
|
||||||
#include "graphics/SharedUIDisplay.h"
|
#include "graphics/SharedUIDisplay.h"
|
||||||
|
#include "graphics/TFTColorRegions.h"
|
||||||
|
#include "graphics/TFTPalette.h"
|
||||||
#include "graphics/images.h"
|
#include "graphics/images.h"
|
||||||
#include "input/RotaryEncoderInterruptImpl1.h"
|
#include "input/RotaryEncoderInterruptImpl1.h"
|
||||||
#include "input/UpDownInterruptImpl1.h"
|
#include "input/UpDownInterruptImpl1.h"
|
||||||
@@ -608,6 +610,9 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
|||||||
display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1);
|
display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1);
|
||||||
display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1);
|
display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1);
|
||||||
display->setColor(WHITE);
|
display->setColor(WHITE);
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
registerTFTActionMenuRegions(boxLeft, boxTop, boxWidth, boxHeight);
|
||||||
|
#endif
|
||||||
|
|
||||||
// Draw Content
|
// Draw Content
|
||||||
int16_t lineY = boxTop + vPadding;
|
int16_t lineY = boxTop + vPadding;
|
||||||
@@ -630,7 +635,21 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
|||||||
if (strchr(lineBuffer, 'p') || strchr(lineBuffer, 'g') || strchr(lineBuffer, 'y') || strchr(lineBuffer, 'j')) {
|
if (strchr(lineBuffer, 'p') || strchr(lineBuffer, 'g') || strchr(lineBuffer, 'y') || strchr(lineBuffer, 'j')) {
|
||||||
background_yOffset = -1;
|
background_yOffset = -1;
|
||||||
}
|
}
|
||||||
display->fillRect(boxLeft, boxTop + 1, boxWidth, effectiveLineHeight - background_yOffset);
|
const int16_t titleBarY = boxTop + 1;
|
||||||
|
const int16_t titleBarHeight = effectiveLineHeight - background_yOffset;
|
||||||
|
display->fillRect(boxLeft, titleBarY, boxWidth, titleBarHeight);
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
if (alertBannerOptions > 0) {
|
||||||
|
const uint16_t titleTextColor =
|
||||||
|
(getActiveTheme().id == ThemeID::DefaultLight) ? TFTPalette::Black : getThemeHeaderText();
|
||||||
|
// Keep title role away from border/corner pixels so rounded-corner masks are not remapped to the title text
|
||||||
|
// color.
|
||||||
|
if (boxWidth > 2 && titleBarHeight > 0) {
|
||||||
|
setAndRegisterTFTColorRole(TFTColorRole::ActionMenuTitle, getThemeHeaderBg(), titleTextColor, boxLeft + 1,
|
||||||
|
titleBarY, boxWidth - 2, titleBarHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
display->setColor(BLACK);
|
display->setColor(BLACK);
|
||||||
int yOffset = 3;
|
int yOffset = 3;
|
||||||
if (current_notification_type == notificationTypeEnum::node_picker) {
|
if (current_notification_type == notificationTypeEnum::node_picker) {
|
||||||
@@ -650,6 +669,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
|||||||
const int barSpacing = 2;
|
const int barSpacing = 2;
|
||||||
const int barHeightStep = 2;
|
const int barHeightStep = 2;
|
||||||
const int gap = 6;
|
const int gap = 6;
|
||||||
|
const int maxBarHeight = totalBars * barHeightStep;
|
||||||
|
|
||||||
int textWidth = display->getStringWidth(lineBuffer, strlen(lineBuffer), true);
|
int textWidth = display->getStringWidth(lineBuffer, strlen(lineBuffer), true);
|
||||||
int barsWidth = totalBars * barWidth + (totalBars - 1) * barSpacing + gap;
|
int barsWidth = totalBars * barWidth + (totalBars - 1) * barSpacing + gap;
|
||||||
@@ -664,6 +684,20 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
|||||||
|
|
||||||
int baseX = groupStartX + textWidth + gap;
|
int baseX = groupStartX + textWidth + gap;
|
||||||
int baseY = lineY + effectiveLineHeight - 1;
|
int baseY = lineY + effectiveLineHeight - 1;
|
||||||
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
|
if (graphics::bannerSignalBars > 0) {
|
||||||
|
uint16_t signalBarsColor = TFTPalette::Medium;
|
||||||
|
if (graphics::bannerSignalBars <= 1) {
|
||||||
|
signalBarsColor = TFTPalette::Bad;
|
||||||
|
} else if (graphics::bannerSignalBars >= 4) {
|
||||||
|
signalBarsColor = TFTPalette::Good;
|
||||||
|
}
|
||||||
|
const int activeBars = min(graphics::bannerSignalBars, totalBars);
|
||||||
|
const int regionWidth = activeBars * barWidth + (activeBars - 1) * barSpacing;
|
||||||
|
setAndRegisterTFTColorRole(TFTColorRole::SignalBars, signalBarsColor, TFTPalette::Black, baseX,
|
||||||
|
baseY - maxBarHeight, regionWidth, maxBarHeight);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
for (int b = 0; b < totalBars; b++) {
|
for (int b = 0; b < totalBars; b++) {
|
||||||
int barHeight = (b + 1) * barHeightStep;
|
int barHeight = (b + 1) * barHeightStep;
|
||||||
int x = baseX + b * (barWidth + barSpacing);
|
int x = baseX + b * (barWidth + barSpacing);
|
||||||
|
|||||||
+638
-248
File diff suppressed because it is too large
Load Diff
@@ -54,6 +54,8 @@ class UIRenderer
|
|||||||
|
|
||||||
static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
|
|
||||||
|
static void drawBootIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
|
|
||||||
// Icon and screen drawing functions
|
// Icon and screen drawing functions
|
||||||
static void drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
static void drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
|
|
||||||
|
|||||||
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
+1
-1
@@ -79,7 +79,7 @@ RTC_DATA_ATTR int bootCount = 0;
|
|||||||
*/
|
*/
|
||||||
void setCPUFast(bool on)
|
void setCPUFast(bool on)
|
||||||
{
|
{
|
||||||
#if defined(ARCH_ESP32) && HAS_WIFI && !HAS_TFT
|
#if defined(ARCH_ESP32) && HAS_WIFI && !HAS_TFT && !defined(T_LORA_PAGER) && !defined(T_DECK)
|
||||||
|
|
||||||
if (isWifiAvailable()) {
|
if (isWifiAvailable()) {
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ build_flags =
|
|||||||
-D TFT_OFFSET_Y=0
|
-D TFT_OFFSET_Y=0
|
||||||
-D TFT_OFFSET_ROTATION=0
|
-D TFT_OFFSET_ROTATION=0
|
||||||
-D SCREEN_ROTATE
|
-D SCREEN_ROTATE
|
||||||
-D SCREEN_TRANSITION_FRAMERATE=5
|
-D SCREEN_TRANSITION_FRAMERATE=30
|
||||||
-D BRIGHTNESS_DEFAULT=130 ; Medium Low Brightness
|
-D BRIGHTNESS_DEFAULT=130 ; Medium Low Brightness
|
||||||
-D HAS_TOUCHSCREEN=1
|
-D HAS_TOUCHSCREEN=1
|
||||||
-D TOUCH_I2C_PORT=0
|
-D TOUCH_I2C_PORT=0
|
||||||
@@ -133,4 +133,4 @@ lib_deps = ${heltec_v4_base.lib_deps}
|
|||||||
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
|
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
|
||||||
lovyan03/LovyanGFX@1.2.19
|
lovyan03/LovyanGFX@1.2.19
|
||||||
# renovate: datasource=git-refs depName=Quency-D_chsc6x packageName=https://github.com/Quency-D/chsc6x gitBranch=master
|
# renovate: datasource=git-refs depName=Quency-D_chsc6x packageName=https://github.com/Quency-D/chsc6x gitBranch=master
|
||||||
https://github.com/Quency-D/chsc6x/archive/5cbead829d6b432a8d621ed1aafd4eb474fd4f27.zip
|
https://github.com/Quency-D/chsc6x/archive/5cbead829d6b432a8d621ed1aafd4eb474fd4f27.zip
|
||||||
|
|||||||
@@ -20,5 +20,5 @@ build_flags =
|
|||||||
lib_deps =
|
lib_deps =
|
||||||
${esp32s3_base.lib_deps}
|
${esp32s3_base.lib_deps}
|
||||||
# renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main
|
# renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main
|
||||||
https://github.com/meshtastic/st7789/archive/92bae2e4a307afb430c3b0bc3d661c55ee1565f0.zip
|
https://github.com/meshtastic/st7789/archive/5180423ae2dbf5885168a8bfb308c7fb7eff6930.zip
|
||||||
upload_speed = 921600
|
upload_speed = 921600
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ build_flags =
|
|||||||
lib_deps =
|
lib_deps =
|
||||||
${esp32s3_base.lib_deps}
|
${esp32s3_base.lib_deps}
|
||||||
# renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main
|
# renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main
|
||||||
https://github.com/meshtastic/st7789/archive/92bae2e4a307afb430c3b0bc3d661c55ee1565f0.zip
|
https://github.com/meshtastic/st7789/archive/5180423ae2dbf5885168a8bfb308c7fb7eff6930.zip
|
||||||
# renovate: datasource=github-tags depName=pschatzmann_arduino-audio-driver packageName=pschatzmann/arduino-audio-driver
|
# renovate: datasource=github-tags depName=pschatzmann_arduino-audio-driver packageName=pschatzmann/arduino-audio-driver
|
||||||
https://github.com/pschatzmann/arduino-audio-driver/archive/v0.2.1.zip
|
https://github.com/pschatzmann/arduino-audio-driver/archive/v0.2.1.zip
|
||||||
# renovate: datasource=git-refs depName=ESP8266Audio packageName=https://github.com/meshtastic/ESP8266Audio gitBranch=meshtastic-2.0.0-dacfix
|
# renovate: datasource=git-refs depName=ESP8266Audio packageName=https://github.com/meshtastic/ESP8266Audio gitBranch=meshtastic-2.0.0-dacfix
|
||||||
|
|||||||
@@ -47,10 +47,9 @@
|
|||||||
#define TFT_OFFSET_Y 0
|
#define TFT_OFFSET_Y 0
|
||||||
#define TFT_OFFSET_ROTATION 0
|
#define TFT_OFFSET_ROTATION 0
|
||||||
#define SCREEN_ROTATE
|
#define SCREEN_ROTATE
|
||||||
#define SCREEN_TRANSITION_FRAMERATE 5
|
#define SCREEN_TRANSITION_FRAMERATE 30
|
||||||
|
|
||||||
// Picomputer gets a white on black display
|
// Picomputer gets a white on black display
|
||||||
#define TFT_MESH_OVERRIDE COLOR565(255, 255, 255)
|
|
||||||
|
|
||||||
#define INPUTBROKER_MATRIX_TYPE 1
|
#define INPUTBROKER_MATRIX_TYPE 1
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
#define TFT_OFFSET_ROTATION 0
|
#define TFT_OFFSET_ROTATION 0
|
||||||
#define TFT_BL 45
|
#define TFT_BL 45
|
||||||
#define SCREEN_ROTATE
|
#define SCREEN_ROTATE
|
||||||
#define SCREEN_TRANSITION_FRAMERATE 5 // fps
|
#define SCREEN_TRANSITION_FRAMERATE 30 // fps
|
||||||
#define USE_TFTDISPLAY 1
|
#define USE_TFTDISPLAY 1
|
||||||
|
|
||||||
#define HAS_TOUCHSCREEN 1
|
#define HAS_TOUCHSCREEN 1
|
||||||
|
|||||||
Regular → Executable
Regular → Executable
Regular → Executable
@@ -20,7 +20,7 @@
|
|||||||
#define TFT_OFFSET_Y 0
|
#define TFT_OFFSET_Y 0
|
||||||
#define TFT_OFFSET_ROTATION 0
|
#define TFT_OFFSET_ROTATION 0
|
||||||
#define SCREEN_ROTATE
|
#define SCREEN_ROTATE
|
||||||
#define SCREEN_TRANSITION_FRAMERATE 5
|
#define SCREEN_TRANSITION_FRAMERATE 30
|
||||||
#define BRIGHTNESS_DEFAULT 130 // Medium Low Brightness
|
#define BRIGHTNESS_DEFAULT 130 // Medium Low Brightness
|
||||||
#define USE_TFTDISPLAY 1
|
#define USE_TFTDISPLAY 1
|
||||||
#define HAS_PHYSICAL_KEYBOARD 1
|
#define HAS_PHYSICAL_KEYBOARD 1
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
#define TFT_OFFSET_Y 0
|
#define TFT_OFFSET_Y 0
|
||||||
#define TFT_OFFSET_ROTATION 3
|
#define TFT_OFFSET_ROTATION 3
|
||||||
#define SCREEN_ROTATE
|
#define SCREEN_ROTATE
|
||||||
#define SCREEN_TRANSITION_FRAMERATE 5
|
#define SCREEN_TRANSITION_FRAMERATE 30
|
||||||
#define BRIGHTNESS_DEFAULT 130 // Medium Low Brightness
|
#define BRIGHTNESS_DEFAULT 130 // Medium Low Brightness
|
||||||
#define USE_TFTDISPLAY 1
|
#define USE_TFTDISPLAY 1
|
||||||
#define HAS_PHYSICAL_KEYBOARD 1
|
#define HAS_PHYSICAL_KEYBOARD 1
|
||||||
|
|||||||
@@ -73,7 +73,6 @@
|
|||||||
#define SX126X_DIO3_TCXO_VOLTAGE 1.8
|
#define SX126X_DIO3_TCXO_VOLTAGE 1.8
|
||||||
|
|
||||||
// Picomputer gets a white on black display
|
// Picomputer gets a white on black display
|
||||||
#define TFT_MESH_OVERRIDE COLOR565(255, 255, 255)
|
|
||||||
|
|
||||||
// keyboard changes
|
// keyboard changes
|
||||||
|
|
||||||
@@ -89,4 +88,4 @@
|
|||||||
{ \
|
{ \
|
||||||
26, 37, 17, 16, 15, 7 \
|
26, 37, 17, 16, 15, 7 \
|
||||||
}
|
}
|
||||||
// #end keyboard
|
// #end keyboard
|
||||||
|
|||||||
@@ -97,7 +97,6 @@
|
|||||||
#define SX126X_DIO3_TCXO_VOLTAGE 1.8
|
#define SX126X_DIO3_TCXO_VOLTAGE 1.8
|
||||||
|
|
||||||
// Picomputer gets a white on black display
|
// Picomputer gets a white on black display
|
||||||
#define TFT_MESH_OVERRIDE COLOR565(255, 255, 255)
|
|
||||||
|
|
||||||
// keyboard changes
|
// keyboard changes
|
||||||
|
|
||||||
@@ -113,4 +112,4 @@
|
|||||||
{ \
|
{ \
|
||||||
26, 37, 17, 16, 15, 7 \
|
26, 37, 17, 16, 15, 7 \
|
||||||
}
|
}
|
||||||
// #end keyboard
|
// #end keyboard
|
||||||
|
|||||||
@@ -74,7 +74,6 @@
|
|||||||
#define SX126X_DIO3_TCXO_VOLTAGE 1.8
|
#define SX126X_DIO3_TCXO_VOLTAGE 1.8
|
||||||
|
|
||||||
// Picomputer gets a white on black display
|
// Picomputer gets a white on black display
|
||||||
#define TFT_MESH_OVERRIDE COLOR565(255, 255, 255)
|
|
||||||
|
|
||||||
// keyboard changes
|
// keyboard changes
|
||||||
|
|
||||||
@@ -90,4 +89,4 @@
|
|||||||
{ \
|
{ \
|
||||||
26, 37, 17, 16, 15, 7 \
|
26, 37, 17, 16, 15, 7 \
|
||||||
}
|
}
|
||||||
// #end keyboard
|
// #end keyboard
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
#define TFT_OFFSET_ROTATION 6 // unPhone's screen wired unusually, 0 typical
|
#define TFT_OFFSET_ROTATION 6 // unPhone's screen wired unusually, 0 typical
|
||||||
#define TFT_INVERT false
|
#define TFT_INVERT false
|
||||||
#define SCREEN_ROTATE true
|
#define SCREEN_ROTATE true
|
||||||
#define SCREEN_TRANSITION_FRAMERATE 5
|
#define SCREEN_TRANSITION_FRAMERATE 30
|
||||||
#define USE_TFTDISPLAY 1
|
#define USE_TFTDISPLAY 1
|
||||||
|
|
||||||
#define HAS_TOUCHSCREEN 1
|
#define HAS_TOUCHSCREEN 1
|
||||||
@@ -74,4 +74,4 @@
|
|||||||
|
|
||||||
// #define BATTERY_PIN 13 // battery V measurement pin; vbat divider is here
|
// #define BATTERY_PIN 13 // battery V measurement pin; vbat divider is here
|
||||||
// #define ADC_CHANNEL ADC2_GPIO13_CHANNEL
|
// #define ADC_CHANNEL ADC2_GPIO13_CHANNEL
|
||||||
// #define BAT_MEASURE_ADC_UNIT 2
|
// #define BAT_MEASURE_ADC_UNIT 2
|
||||||
|
|||||||
@@ -23,4 +23,4 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_
|
|||||||
lib_deps =
|
lib_deps =
|
||||||
${nrf52840_base.lib_deps}
|
${nrf52840_base.lib_deps}
|
||||||
# renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main
|
# renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main
|
||||||
https://github.com/meshtastic/st7789/archive/92bae2e4a307afb430c3b0bc3d661c55ee1565f0.zip
|
https://github.com/meshtastic/st7789/archive/5180423ae2dbf5885168a8bfb308c7fb7eff6930.zip
|
||||||
|
|||||||
@@ -57,9 +57,6 @@ extern "C" {
|
|||||||
#define TFT_OFFSET_X 0
|
#define TFT_OFFSET_X 0
|
||||||
#define TFT_OFFSET_Y 0
|
#define TFT_OFFSET_Y 0
|
||||||
|
|
||||||
// T114 gets a muted yellow on black display
|
|
||||||
#define TFT_MESH_OVERRIDE COLOR565(255, 255, 128)
|
|
||||||
|
|
||||||
// #define TFT_OFFSET_ROTATION 0
|
// #define TFT_OFFSET_ROTATION 0
|
||||||
// #define SCREEN_ROTATE
|
// #define SCREEN_ROTATE
|
||||||
// #define SCREEN_TRANSITION_FRAMERATE 5
|
// #define SCREEN_TRANSITION_FRAMERATE 5
|
||||||
|
|||||||
@@ -132,4 +132,4 @@ build_flags = ${heltec_mesh_solar_base.build_flags}
|
|||||||
lib_deps =
|
lib_deps =
|
||||||
${heltec_mesh_solar_base.lib_deps}
|
${heltec_mesh_solar_base.lib_deps}
|
||||||
# renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main
|
# renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main
|
||||||
https://github.com/meshtastic/st7789/archive/92bae2e4a307afb430c3b0bc3d661c55ee1565f0.zip
|
https://github.com/meshtastic/st7789/archive/5180423ae2dbf5885168a8bfb308c7fb7eff6930.zip
|
||||||
|
|||||||
Reference in New Issue
Block a user