feat: Enhance ThemeEngine and apply new theming to SettingsActivity

- Improve ThemeManager with Children property support and safe integer parsing.
- Refactor SettingsActivity to use new theme elements.
- Update BasicElements and LayoutElements for better rendering.
This commit is contained in:
Brackyt 2026-01-27 23:49:16 +01:00
parent 7bfb3af0da
commit e6b3ecc519
12 changed files with 1123 additions and 485 deletions

View File

@ -24,16 +24,17 @@ class Container : public UIElement {
public:
explicit Container(const std::string& id) : UIElement(id), bgColorExpr(Expression::parse("0xFF")) {}
virtual ~Container() {
for (auto child : children) delete child;
}
virtual ~Container() = default;
Container* asContainer() override { return this; }
ElementType getType() const override { return ElementType::Container; }
const char* getTypeName() const override { return "Container"; }
void addChild(UIElement* child) { children.push_back(child); }
void clearChildren() { children.clear(); }
const std::vector<UIElement*>& getChildren() const { return children; }
void setBackgroundColorExpr(const std::string& expr) {
@ -87,55 +88,7 @@ class Container : public UIElement {
}
}
void draw(const GfxRenderer& renderer, const ThemeContext& context) override {
if (!isVisible(context)) return;
if (hasBg) {
std::string colStr = context.evaluatestring(bgColorExpr);
uint8_t color = Color::parse(colStr).value;
// Use dithered fill for grayscale values, solid fill for black/white
// Use rounded rect if borderRadius > 0
if (color == 0x00) {
if (borderRadius > 0) {
renderer.fillRoundedRect(absX, absY, absW, absH, borderRadius, true);
} else {
renderer.fillRect(absX, absY, absW, absH, true);
}
} else if (color >= 0xF0) {
if (borderRadius > 0) {
renderer.fillRoundedRect(absX, absY, absW, absH, borderRadius, false);
} else {
renderer.fillRect(absX, absY, absW, absH, false);
}
} else {
if (borderRadius > 0) {
renderer.fillRoundedRectDithered(absX, absY, absW, absH, borderRadius, color);
} else {
renderer.fillRectDithered(absX, absY, absW, absH, color);
}
}
}
// Handle dynamic border expression
bool drawBorder = border;
if (hasBorderExpr()) {
drawBorder = context.evaluateBool(borderExpr.rawExpr);
}
if (drawBorder) {
if (borderRadius > 0) {
renderer.drawRoundedRect(absX, absY, absW, absH, borderRadius, true);
} else {
renderer.drawRect(absX, absY, absW, absH, true);
}
}
for (auto child : children) {
child->draw(renderer, context);
}
markClean();
}
void draw(const GfxRenderer& renderer, const ThemeContext& context) override;
};
// --- Rectangle ---
@ -143,10 +96,12 @@ class Rectangle : public UIElement {
bool fill = false;
Expression fillExpr; // Dynamic fill based on expression
Expression colorExpr;
int borderRadius = 0;
public:
explicit Rectangle(const std::string& id) : UIElement(id), colorExpr(Expression::parse("0x00")) {}
ElementType getType() const override { return ElementType::Rectangle; }
const char* getTypeName() const override { return "Rectangle"; }
void setFill(bool f) {
fill = f;
@ -163,26 +118,12 @@ class Rectangle : public UIElement {
markDirty();
}
void draw(const GfxRenderer& renderer, const ThemeContext& context) override {
if (!isVisible(context)) return;
std::string colStr = context.evaluatestring(colorExpr);
uint8_t color = Color::parse(colStr).value;
bool black = (color == 0x00);
bool shouldFill = fill;
if (!fillExpr.empty()) {
shouldFill = context.evaluateBool(fillExpr.rawExpr);
}
if (shouldFill) {
renderer.fillRect(absX, absY, absW, absH, black);
} else {
renderer.drawRect(absX, absY, absW, absH, black);
}
markClean();
void setBorderRadius(int r) {
borderRadius = r;
markDirty();
}
void draw(const GfxRenderer& renderer, const ThemeContext& context) override;
};
// --- Label ---
@ -201,6 +142,7 @@ class Label : public UIElement {
public:
explicit Label(const std::string& id) : UIElement(id), colorExpr(Expression::parse("0x00")) {}
ElementType getType() const override { return ElementType::Label; }
const char* getTypeName() const override { return "Label"; }
void setText(const std::string& expr) {
textExpr = Expression::parse(expr);
@ -231,132 +173,7 @@ class Label : public UIElement {
markDirty();
}
void draw(const GfxRenderer& renderer, const ThemeContext& context) override {
if (!isVisible(context)) return;
std::string finalText = context.evaluatestring(textExpr);
if (finalText.empty()) {
markClean();
return;
}
std::string colStr = context.evaluatestring(colorExpr);
uint8_t color = Color::parse(colStr).value;
bool black = (color == 0x00);
int textWidth = renderer.getTextWidth(fontId, finalText.c_str());
int lineHeight = renderer.getLineHeight(fontId);
// Split text into lines based on width
std::vector<std::string> lines;
if (absW > 0 && textWidth > absW && maxLines > 1) {
// Logic to wrap text
std::string remaining = finalText;
while (!remaining.empty() && (int)lines.size() < maxLines) {
// If it fits, add entire line
if (renderer.getTextWidth(fontId, remaining.c_str()) <= absW) {
lines.push_back(remaining);
break;
}
// Binary search for cut point
int len = remaining.length();
int cut = len;
// Find split point
// Optimistic start: approximate chars that fit
int avgCharWidth = renderer.getTextWidth(fontId, "a");
if (avgCharWidth < 1) avgCharWidth = 8;
int approxChars = absW / avgCharWidth;
if (approxChars < 1) approxChars = 1;
if (approxChars >= len) approxChars = len - 1;
// Refine from approxChars
int w = renderer.getTextWidth(fontId, remaining.substr(0, approxChars).c_str());
if (w < absW) {
// Grow
for (int i = approxChars; i <= len; i++) {
if (renderer.getTextWidth(fontId, remaining.substr(0, i).c_str()) > absW) {
cut = i - 1;
break;
}
cut = i;
}
} else {
// Shrink
for (int i = approxChars; i > 0; i--) {
if (renderer.getTextWidth(fontId, remaining.substr(0, i).c_str()) <= absW) {
cut = i;
break;
}
}
}
// Find last space before cut
if (cut < (int)remaining.length()) {
int space = -1;
for (int i = cut; i > 0; i--) {
if (remaining[i] == ' ') {
space = i;
break;
}
}
if (space != -1) cut = space;
}
std::string line = remaining.substr(0, cut);
// If we're at the last allowed line but still have more text
if ((int)lines.size() == maxLines - 1 && cut < (int)remaining.length()) {
if (ellipsis) {
line = renderer.truncatedText(fontId, remaining.c_str(), absW);
}
lines.push_back(line);
break;
}
lines.push_back(line);
// Advance
if (cut < (int)remaining.length()) {
// Skip the space if check
if (remaining[cut] == ' ') cut++;
remaining = remaining.substr(cut);
} else {
remaining = "";
}
}
} else {
// Single line handling (truncate if needed)
if (ellipsis && textWidth > absW && absW > 0) {
finalText = renderer.truncatedText(fontId, finalText.c_str(), absW);
}
lines.push_back(finalText);
}
// Draw lines
int totalTextHeight = lines.size() * lineHeight;
int startY = absY;
// Vertical centering
if (absH > 0 && totalTextHeight < absH) {
startY = absY + (absH - totalTextHeight) / 2;
}
for (size_t i = 0; i < lines.size(); i++) {
int lineWidth = renderer.getTextWidth(fontId, lines[i].c_str());
int drawX = absX;
if (alignment == Alignment::Center && absW > 0) {
drawX = absX + (absW - lineWidth) / 2;
} else if (alignment == Alignment::Right && absW > 0) {
drawX = absX + absW - lineWidth;
}
renderer.drawText(fontId, drawX, startY + i * lineHeight, lines[i].c_str(), black);
}
markClean();
}
void draw(const GfxRenderer& renderer, const ThemeContext& context) override;
};
// --- BitmapElement ---
@ -371,6 +188,7 @@ class BitmapElement : public UIElement {
cacheable = true; // Bitmaps benefit from caching
}
ElementType getType() const override { return ElementType::Bitmap; }
const char* getTypeName() const override { return "Bitmap"; }
void setSrc(const std::string& src) {
srcExpr = Expression::parse(src);
@ -416,6 +234,7 @@ class ProgressBar : public UIElement {
{}
ElementType getType() const override { return ElementType::ProgressBar; }
const char* getTypeName() const override { return "ProgressBar"; }
void setValue(const std::string& expr) {
valueExpr = Expression::parse(expr);
@ -484,6 +303,7 @@ class Divider : public UIElement {
explicit Divider(const std::string& id) : UIElement(id), colorExpr(Expression::parse("0x00")) {}
ElementType getType() const override { return ElementType::Divider; }
const char* getTypeName() const override { return "Divider"; }
void setColorExpr(const std::string& expr) {
colorExpr = Expression::parse(expr);
@ -531,6 +351,7 @@ class BatteryIcon : public UIElement {
}
ElementType getType() const override { return ElementType::BatteryIcon; }
const char* getTypeName() const override { return "BatteryIcon"; }
void setValue(const std::string& expr) {
valueExpr = Expression::parse(expr);

View File

@ -19,6 +19,7 @@ class HStack : public Container {
public:
HStack(const std::string& id) : Container(id) {}
ElementType getType() const override { return ElementType::HStack; }
const char* getTypeName() const override { return "HStack"; }
void setSpacing(int s) {
spacing = s;
@ -71,6 +72,7 @@ class VStack : public Container {
public:
VStack(const std::string& id) : Container(id) {}
ElementType getType() const override { return ElementType::VStack; }
const char* getTypeName() const override { return "VStack"; }
void setSpacing(int s) {
spacing = s;
@ -122,6 +124,7 @@ class Grid : public Container {
public:
Grid(const std::string& id) : Container(id) {}
ElementType getType() const override { return ElementType::Grid; }
const char* getTypeName() const override { return "Grid"; }
void setColumns(int c) {
columns = c > 0 ? c : 1;
@ -182,7 +185,7 @@ class Badge : public UIElement {
int fontId = 0;
int paddingH = 8; // Horizontal padding
int paddingV = 4; // Vertical padding
int cornerRadius = 0;
int borderRadius = 0;
public:
Badge(const std::string& id) : UIElement(id) {
@ -191,6 +194,7 @@ class Badge : public UIElement {
}
ElementType getType() const override { return ElementType::Badge; }
const char* getTypeName() const override { return "Badge"; }
void setText(const std::string& expr) {
textExpr = Expression::parse(expr);
@ -217,6 +221,11 @@ class Badge : public UIElement {
markDirty();
}
void setBorderRadius(int r) {
borderRadius = r;
markDirty();
}
void draw(const GfxRenderer& renderer, const ThemeContext& context) override {
if (!isVisible(context)) return;
@ -226,29 +235,59 @@ class Badge : public UIElement {
return;
}
// Calculate badge size based on text
// Calculate badge size based on text content - always auto-sizes
int textW = renderer.getTextWidth(fontId, text.c_str());
int textH = renderer.getLineHeight(fontId);
int badgeW = textW + 2 * paddingH;
int badgeH = textH + 2 * paddingV;
// Use absX, absY as position, but we may auto-size
if (absW == 0) absW = badgeW;
if (absH == 0) absH = badgeH;
// Badge always auto-sizes to content
int drawW = badgeW;
int drawH = badgeH;
// Position the badge within its container
// If absW/absH are set, use them as bounding box for alignment
int drawX = absX;
int drawY = absY;
// Right-align badge within bounding box if width is specified
if (absW > 0 && absW > drawW) {
drawX = absX + absW - drawW;
}
// Vertically center badge within bounding box if height is specified
if (absH > 0 && absH > drawH) {
drawY = absY + (absH - drawH) / 2;
}
// Draw background
std::string bgStr = context.evaluatestring(bgColorExpr);
uint8_t bgColor = Color::parse(bgStr).value;
renderer.fillRect(absX, absY, absW, absH, bgColor == 0x00);
if (borderRadius > 0) {
if (bgColor == 0x00) {
renderer.fillRoundedRect(drawX, drawY, drawW, drawH, borderRadius, true);
} else if (bgColor >= 0xF0) {
renderer.fillRoundedRect(drawX, drawY, drawW, drawH, borderRadius, false);
} else {
renderer.fillRoundedRectDithered(drawX, drawY, drawW, drawH, borderRadius, bgColor);
}
} else {
renderer.fillRect(drawX, drawY, drawW, drawH, bgColor == 0x00);
}
// Draw border for contrast
renderer.drawRect(absX, absY, absW, absH, bgColor != 0x00);
// Draw border for contrast (only if not black background)
if (bgColor != 0x00) {
if (borderRadius > 0) {
renderer.drawRoundedRect(drawX, drawY, drawW, drawH, borderRadius, true);
} else {
renderer.drawRect(drawX, drawY, drawW, drawH, true);
}
}
// Draw text centered
// Draw text centered within the badge
std::string fgStr = context.evaluatestring(fgColorExpr);
uint8_t fgColor = Color::parse(fgStr).value;
int textX = absX + (absW - textW) / 2;
int textY = absY + (absH - textH) / 2;
int textX = drawX + paddingH;
int textY = drawY + paddingV;
renderer.drawText(fontId, textX, textY, text.c_str(), fgColor == 0x00);
markClean();
@ -256,22 +295,28 @@ class Badge : public UIElement {
};
// --- Toggle: On/Off Switch ---
// Fully themable toggle with track and knob
// Supports rounded or square appearance based on BorderRadius
class Toggle : public UIElement {
Expression valueExpr; // Boolean expression
Expression onColorExpr;
Expression offColorExpr;
Expression valueExpr; // Boolean expression for on/off state
Expression onColorExpr; // Track color when ON
Expression offColorExpr; // Track color when OFF
Expression knobColorExpr; // Knob color (optional, defaults to opposite of track)
int trackWidth = 44;
int trackHeight = 24;
int knobSize = 20;
int borderRadius = 0; // 0 = square, >0 = rounded (use trackHeight/2 for pill shape)
int knobRadius = 0; // Knob corner radius
public:
Toggle(const std::string& id) : UIElement(id) {
valueExpr = Expression::parse("false");
onColorExpr = Expression::parse("0x00"); // Black when on
offColorExpr = Expression::parse("0xFF"); // White when off
offColorExpr = Expression::parse("0xCC"); // Light gray when off
}
ElementType getType() const override { return ElementType::Toggle; }
const char* getTypeName() const override { return "Toggle"; }
void setValue(const std::string& expr) {
valueExpr = Expression::parse(expr);
@ -285,6 +330,10 @@ class Toggle : public UIElement {
offColorExpr = Expression::parse(expr);
markDirty();
}
void setKnobColor(const std::string& expr) {
knobColorExpr = Expression::parse(expr);
markDirty();
}
void setTrackWidth(int w) {
trackWidth = w;
markDirty();
@ -297,30 +346,109 @@ class Toggle : public UIElement {
knobSize = s;
markDirty();
}
void setBorderRadius(int r) {
borderRadius = r;
markDirty();
}
void setKnobRadius(int r) {
knobRadius = r;
markDirty();
}
void draw(const GfxRenderer& renderer, const ThemeContext& context) override {
if (!isVisible(context)) return;
bool isOn = context.evaluateBool(valueExpr.rawExpr);
// Evaluate the value - handle simple variable references directly
bool isOn = false;
std::string rawExpr = valueExpr.rawExpr;
// Get colors
// If it's a simple {variable} reference, resolve it directly
if (rawExpr.size() > 2 && rawExpr.front() == '{' && rawExpr.back() == '}') {
std::string varName = rawExpr.substr(1, rawExpr.size() - 2);
// Trim whitespace
size_t start = varName.find_first_not_of(" \t");
size_t end = varName.find_last_not_of(" \t");
if (start != std::string::npos) {
varName = varName.substr(start, end - start + 1);
}
isOn = context.getAnyAsBool(varName, false);
} else {
isOn = context.evaluateBool(rawExpr);
}
// Get track color based on state
std::string colorStr = isOn ? context.evaluatestring(onColorExpr) : context.evaluatestring(offColorExpr);
uint8_t trackColor = Color::parse(colorStr).value;
// Draw track
// Calculate track position (centered vertically in bounding box)
int trackX = absX;
int trackY = absY + (absH - trackHeight) / 2;
renderer.fillRect(trackX, trackY, trackWidth, trackHeight, trackColor == 0x00);
renderer.drawRect(trackX, trackY, trackWidth, trackHeight, true);
// Draw knob
// Calculate effective border radius (capped at half height for pill shape)
int effectiveRadius = borderRadius;
if (effectiveRadius > trackHeight / 2) {
effectiveRadius = trackHeight / 2;
}
// Draw track
if (effectiveRadius > 0) {
// Rounded track
if (trackColor == 0x00) {
renderer.fillRoundedRect(trackX, trackY, trackWidth, trackHeight, effectiveRadius, true);
} else if (trackColor >= 0xF0) {
renderer.fillRoundedRect(trackX, trackY, trackWidth, trackHeight, effectiveRadius, false);
renderer.drawRoundedRect(trackX, trackY, trackWidth, trackHeight, effectiveRadius, true);
} else {
renderer.fillRoundedRectDithered(trackX, trackY, trackWidth, trackHeight, effectiveRadius, trackColor);
renderer.drawRoundedRect(trackX, trackY, trackWidth, trackHeight, effectiveRadius, true);
}
} else {
// Square track
if (trackColor == 0x00) {
renderer.fillRect(trackX, trackY, trackWidth, trackHeight, true);
} else if (trackColor >= 0xF0) {
renderer.fillRect(trackX, trackY, trackWidth, trackHeight, false);
renderer.drawRect(trackX, trackY, trackWidth, trackHeight, true);
} else {
renderer.fillRectDithered(trackX, trackY, trackWidth, trackHeight, trackColor);
renderer.drawRect(trackX, trackY, trackWidth, trackHeight, true);
}
}
// Calculate knob position
int knobMargin = (trackHeight - knobSize) / 2;
int knobX = isOn ? (trackX + trackWidth - knobSize - knobMargin) : (trackX + knobMargin);
int knobY = trackY + knobMargin;
// Knob is opposite color of track
renderer.fillRect(knobX, knobY, knobSize, knobSize, trackColor != 0x00);
renderer.drawRect(knobX, knobY, knobSize, knobSize, true);
// Determine knob color
bool knobBlack;
if (!knobColorExpr.empty()) {
std::string knobStr = context.evaluatestring(knobColorExpr);
uint8_t knobColor = Color::parse(knobStr).value;
knobBlack = (knobColor == 0x00);
} else {
// Default: knob is opposite color of track
knobBlack = (trackColor >= 0x80);
}
// Calculate effective knob radius
int effectiveKnobRadius = knobRadius;
if (effectiveKnobRadius > knobSize / 2) {
effectiveKnobRadius = knobSize / 2;
}
// Draw knob
if (effectiveKnobRadius > 0) {
renderer.fillRoundedRect(knobX, knobY, knobSize, knobSize, effectiveKnobRadius, knobBlack);
if (!knobBlack) {
renderer.drawRoundedRect(knobX, knobY, knobSize, knobSize, effectiveKnobRadius, true);
}
} else {
renderer.fillRect(knobX, knobY, knobSize, knobSize, knobBlack);
if (!knobBlack) {
renderer.drawRect(knobX, knobY, knobSize, knobSize, true);
}
}
markClean();
}
@ -337,6 +465,7 @@ class TabBar : public Container {
public:
TabBar(const std::string& id) : Container(id) {}
ElementType getType() const override { return ElementType::TabBar; }
const char* getTypeName() const override { return "TabBar"; }
void setSelected(const std::string& expr) {
selectedExpr = Expression::parse(expr);
@ -414,9 +543,6 @@ class TabBar : public Container {
}
}
// Draw bottom border
renderer.drawLine(absX, absY + absH - 1, absX + absW - 1, absY + absH - 1, true);
markClean();
}
};
@ -437,6 +563,7 @@ class Icon : public UIElement {
}
ElementType getType() const override { return ElementType::Icon; }
const char* getTypeName() const override { return "Icon"; }
void setSrc(const std::string& expr) {
srcExpr = Expression::parse(expr);
@ -469,6 +596,7 @@ class ScrollIndicator : public UIElement {
}
ElementType getType() const override { return ElementType::ScrollIndicator; }
const char* getTypeName() const override { return "ScrollIndicator"; }
void setPosition(const std::string& expr) {
positionExpr = Expression::parse(expr);

View File

@ -34,6 +34,7 @@ class List : public Container {
List(const std::string& id) : Container(id) {}
ElementType getType() const override { return ElementType::List; }
const char* getTypeName() const override { return "List"; }
void setSource(const std::string& s) {
source = s;

View File

@ -1,5 +1,7 @@
#pragma once
#include <cctype>
#include <cstdlib>
#include <functional>
#include <map>
#include <string>
@ -82,6 +84,36 @@ class ThemeContext {
return start < s.length();
}
// Helper to check if string is a hex number (0x..)
static bool isHexNumber(const std::string& s) {
if (s.size() < 3) return false;
if (!(s[0] == '0' && (s[1] == 'x' || s[1] == 'X'))) return false;
for (size_t i = 2; i < s.length(); i++) {
char c = s[i];
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) return false;
}
return true;
}
static int parseInt(const std::string& s) {
if (isHexNumber(s)) {
return static_cast<int>(std::strtol(s.c_str(), nullptr, 16));
}
if (isNumber(s)) {
return static_cast<int>(std::strtol(s.c_str(), nullptr, 10));
}
return 0;
}
static bool coerceBool(const std::string& s) {
std::string v = trim(s);
if (v.empty()) return false;
if (v == "true" || v == "1") return true;
if (v == "false" || v == "0") return false;
if (isHexNumber(v) || isNumber(v)) return parseInt(v) != 0;
return true;
}
public:
explicit ThemeContext(const ThemeContext* parent = nullptr) : parent(parent) {}
@ -89,6 +121,20 @@ class ThemeContext {
void setInt(const std::string& key, int value) { ints[key] = value; }
void setBool(const std::string& key, bool value) { bools[key] = value; }
// Helper to populate list data efficiently
void setListItem(const std::string& listName, int index, const std::string& property, const std::string& value) {
strings[listName + "." + std::to_string(index) + "." + property] = value;
}
void setListItem(const std::string& listName, int index, const std::string& property, int value) {
ints[listName + "." + std::to_string(index) + "." + property] = value;
}
void setListItem(const std::string& listName, int index, const std::string& property, bool value) {
bools[listName + "." + std::to_string(index) + "." + property] = value;
}
void setListItem(const std::string& listName, int index, const std::string& property, const char* value) {
strings[listName + "." + std::to_string(index) + "." + property] = value;
}
std::string getString(const std::string& key, const std::string& defaultValue = "") const {
auto it = strings.find(key);
if (it != strings.end()) return it->second;
@ -136,6 +182,34 @@ class ThemeContext {
return "";
}
bool getAnyAsBool(const std::string& key, bool defaultValue = false) const {
auto bit = bools.find(key);
if (bit != bools.end()) return bit->second;
auto iit = ints.find(key);
if (iit != ints.end()) return iit->second != 0;
auto sit = strings.find(key);
if (sit != strings.end()) return coerceBool(sit->second);
if (parent) return parent->getAnyAsBool(key, defaultValue);
return defaultValue;
}
int getAnyAsInt(const std::string& key, int defaultValue = 0) const {
auto iit = ints.find(key);
if (iit != ints.end()) return iit->second;
auto bit = bools.find(key);
if (bit != bools.end()) return bit->second ? 1 : 0;
auto sit = strings.find(key);
if (sit != strings.end()) return parseInt(sit->second);
if (parent) return parent->getAnyAsInt(key, defaultValue);
return defaultValue;
}
// Evaluate a complex boolean expression
// Supports: !, &&, ||, ==, !=, <, >, <=, >=, parentheses
bool evaluateBool(const std::string& expression) const {
@ -148,96 +222,233 @@ class ThemeContext {
// Handle {var} wrapper
if (expr.size() > 2 && expr.front() == '{' && expr.back() == '}') {
expr = expr.substr(1, expr.size() - 2);
expr = trim(expr.substr(1, expr.size() - 2));
}
// Handle negation
if (!expr.empty() && expr[0] == '!') {
return !evaluateBool(expr.substr(1));
}
struct Token {
enum Type { Identifier, Number, String, Op, LParen, RParen, End };
Type type;
std::string text;
};
// Handle parentheses
if (!expr.empty() && expr[0] == '(') {
int depth = 1;
size_t closePos = 1;
while (closePos < expr.length() && depth > 0) {
if (expr[closePos] == '(') depth++;
if (expr[closePos] == ')') depth--;
closePos++;
struct Tokenizer {
const std::string& s;
size_t pos = 0;
Token peeked{Token::End, ""};
bool hasPeek = false;
explicit Tokenizer(const std::string& input) : s(input) {}
static std::string trimCopy(const std::string& in) {
size_t start = in.find_first_not_of(" \t\n\r");
if (start == std::string::npos) return "";
size_t end = in.find_last_not_of(" \t\n\r");
return in.substr(start, end - start + 1);
}
if (closePos <= expr.length()) {
std::string inner = expr.substr(1, closePos - 2);
std::string rest = trim(expr.substr(closePos));
bool innerResult = evaluateBool(inner);
// Check for && or ||
if (rest.length() >= 2 && rest.substr(0, 2) == "&&") {
return innerResult && evaluateBool(rest.substr(2));
void skipWs() {
while (pos < s.size() && (s[pos] == ' ' || s[pos] == '\t' || s[pos] == '\n' || s[pos] == '\r')) {
pos++;
}
if (rest.length() >= 2 && rest.substr(0, 2) == "||") {
return innerResult || evaluateBool(rest.substr(2));
}
return innerResult;
}
}
// Handle && and || (lowest precedence)
size_t andPos = expr.find("&&");
size_t orPos = expr.find("||");
Token readToken() {
skipWs();
if (pos >= s.size()) return {Token::End, ""};
char c = s[pos];
// Process || first (lower precedence than &&)
if (orPos != std::string::npos && (andPos == std::string::npos || orPos < andPos)) {
return evaluateBool(expr.substr(0, orPos)) || evaluateBool(expr.substr(orPos + 2));
}
if (andPos != std::string::npos) {
return evaluateBool(expr.substr(0, andPos)) && evaluateBool(expr.substr(andPos + 2));
}
if (c == '(') {
pos++;
return {Token::LParen, "("};
}
if (c == ')') {
pos++;
return {Token::RParen, ")"};
}
// Handle comparisons
size_t eqPos = expr.find("==");
if (eqPos != std::string::npos) {
std::string left = trim(expr.substr(0, eqPos));
std::string right = trim(expr.substr(eqPos + 2));
return compareValues(left, right) == 0;
}
if (c == '{') {
size_t end = s.find('}', pos + 1);
std::string inner;
if (end == std::string::npos) {
inner = s.substr(pos + 1);
pos = s.size();
} else {
inner = s.substr(pos + 1, end - pos - 1);
pos = end + 1;
}
return {Token::Identifier, trimCopy(inner)};
}
size_t nePos = expr.find("!=");
if (nePos != std::string::npos) {
std::string left = trim(expr.substr(0, nePos));
std::string right = trim(expr.substr(nePos + 2));
return compareValues(left, right) != 0;
}
if (c == '"' || c == '\'') {
char quote = c;
pos++;
std::string out;
while (pos < s.size()) {
char ch = s[pos++];
if (ch == '\\' && pos < s.size()) {
out.push_back(s[pos++]);
continue;
}
if (ch == quote) break;
out.push_back(ch);
}
return {Token::String, out};
}
size_t gePos = expr.find(">=");
if (gePos != std::string::npos) {
std::string left = trim(expr.substr(0, gePos));
std::string right = trim(expr.substr(gePos + 2));
return compareValues(left, right) >= 0;
}
// Operators
if (pos + 1 < s.size()) {
std::string two = s.substr(pos, 2);
if (two == "&&" || two == "||" || two == "==" || two == "!=" || two == "<=" || two == ">=") {
pos += 2;
return {Token::Op, two};
}
}
if (c == '!' || c == '<' || c == '>') {
pos++;
return {Token::Op, std::string(1, c)};
}
size_t lePos = expr.find("<=");
if (lePos != std::string::npos) {
std::string left = trim(expr.substr(0, lePos));
std::string right = trim(expr.substr(lePos + 2));
return compareValues(left, right) <= 0;
}
// Number (decimal or hex)
if (isdigit(c) || (c == '-' && pos + 1 < s.size() && isdigit(s[pos + 1]))) {
size_t start = pos;
pos++;
if (pos + 1 < s.size() && s[start] == '0' && (s[pos] == 'x' || s[pos] == 'X')) {
pos++; // consume x
while (pos < s.size() && isxdigit(s[pos])) pos++;
} else {
while (pos < s.size() && isdigit(s[pos])) pos++;
}
return {Token::Number, s.substr(start, pos - start)};
}
size_t gtPos = expr.find('>');
if (gtPos != std::string::npos) {
std::string left = trim(expr.substr(0, gtPos));
std::string right = trim(expr.substr(gtPos + 1));
return compareValues(left, right) > 0;
}
// Identifier
if (isalpha(c) || c == '_' || c == '.') {
size_t start = pos;
pos++;
while (pos < s.size()) {
char ch = s[pos];
if (isalnum(ch) || ch == '_' || ch == '.') {
pos++;
continue;
}
break;
}
return {Token::Identifier, s.substr(start, pos - start)};
}
size_t ltPos = expr.find('<');
if (ltPos != std::string::npos) {
std::string left = trim(expr.substr(0, ltPos));
std::string right = trim(expr.substr(ltPos + 1));
return compareValues(left, right) < 0;
}
// Unknown char, skip
pos++;
return readToken();
}
// Simple variable lookup
return getBool(expr, false);
Token next() {
if (hasPeek) {
hasPeek = false;
return peeked;
}
return readToken();
}
Token peek() {
if (!hasPeek) {
peeked = readToken();
hasPeek = true;
}
return peeked;
}
};
Tokenizer tz(expr);
std::function<bool()> parseOr;
std::function<bool()> parseAnd;
std::function<bool()> parseNot;
std::function<bool()> parseComparison;
std::function<std::string()> parseValue;
parseValue = [&]() -> std::string {
Token t = tz.next();
if (t.type == Token::LParen) {
bool inner = parseOr();
Token close = tz.next();
if (close.type != Token::RParen) {
// best-effort: no-op
}
return inner ? "true" : "false";
}
if (t.type == Token::String) {
return "'" + t.text + "'";
}
if (t.type == Token::Number) {
return t.text;
}
if (t.type == Token::Identifier) {
return t.text;
}
return "";
};
auto isComparisonOp = [](const Token& t) {
if (t.type != Token::Op) return false;
return t.text == "==" || t.text == "!=" || t.text == "<" || t.text == ">" || t.text == "<=" || t.text == ">=";
};
parseComparison = [&]() -> bool {
std::string left = parseValue();
Token op = tz.peek();
if (isComparisonOp(op)) {
tz.next();
std::string right = parseValue();
int cmp = compareValues(left, right);
if (op.text == "==") return cmp == 0;
if (op.text == "!=") return cmp != 0;
if (op.text == "<") return cmp < 0;
if (op.text == ">") return cmp > 0;
if (op.text == "<=") return cmp <= 0;
if (op.text == ">=") return cmp >= 0;
return false;
}
return coerceBool(resolveValue(left));
};
parseNot = [&]() -> bool {
Token t = tz.peek();
if (t.type == Token::Op && t.text == "!") {
tz.next();
return !parseNot();
}
return parseComparison();
};
parseAnd = [&]() -> bool {
bool value = parseNot();
while (true) {
Token t = tz.peek();
if (t.type == Token::Op && t.text == "&&") {
tz.next();
value = value && parseNot();
continue;
}
break;
}
return value;
};
parseOr = [&]() -> bool {
bool value = parseAnd();
while (true) {
Token t = tz.peek();
if (t.type == Token::Op && t.text == "||") {
tz.next();
value = value || parseAnd();
continue;
}
break;
}
return value;
};
return parseOr();
}
// Compare two values (handles variables, numbers, strings)
@ -246,9 +457,9 @@ class ThemeContext {
std::string rightVal = resolveValue(right);
// Try numeric comparison
if (isNumber(leftVal) && isNumber(rightVal)) {
int l = std::stoi(leftVal);
int r = std::stoi(rightVal);
if ((isNumber(leftVal) || isHexNumber(leftVal)) && (isNumber(rightVal) || isHexNumber(rightVal))) {
int l = parseInt(leftVal);
int r = parseInt(rightVal);
return (l < r) ? -1 : (l > r) ? 1 : 0;
}
@ -272,7 +483,7 @@ class ThemeContext {
if (isNumber(v)) return v;
// Check for hex color literals (0x00, 0xFF, etc.)
if (v.size() > 2 && v[0] == '0' && (v[1] == 'x' || v[1] == 'X')) {
if (isHexNumber(v)) {
return v;
}
@ -282,13 +493,18 @@ class ThemeContext {
}
// Check for boolean literals
if (v == "true" || v == "false") {
if (v == "true" || v == "false" || v == "1" || v == "0") {
return v;
}
// Try to look up as variable
if (hasKey(v)) {
return getAnyAsString(v);
std::string varName = v;
if (varName.size() >= 2 && varName.front() == '{' && varName.back() == '}') {
varName = trim(varName.substr(1, varName.size() - 2));
}
if (hasKey(varName)) {
return getAnyAsString(varName);
}
// Return as literal if not found as variable

View File

@ -17,10 +17,17 @@ struct Dimension {
static Dimension parse(const std::string& str) {
if (str.empty()) return Dimension(0, DimensionUnit::PIXELS);
auto safeParseInt = [](const std::string& s) {
char* end = nullptr;
long v = std::strtol(s.c_str(), &end, 10);
if (!end || end == s.c_str()) return 0;
return static_cast<int>(v);
};
if (str.back() == '%') {
return Dimension(std::stoi(str.substr(0, str.length() - 1)), DimensionUnit::PERCENT);
return Dimension(safeParseInt(str.substr(0, str.length() - 1)), DimensionUnit::PERCENT);
}
return Dimension(std::stoi(str), DimensionUnit::PIXELS);
return Dimension(safeParseInt(str), DimensionUnit::PIXELS);
}
int resolve(int parentSize) const {
@ -45,7 +52,8 @@ struct Color {
if (str.size() > 2 && str.substr(0, 2) == "0x") {
return Color((uint8_t)std::strtol(str.c_str(), nullptr, 16));
}
return Color((uint8_t)std::stoi(str));
// Safe fallback using strtol (returns 0 on error, no exception)
return Color((uint8_t)std::strtol(str.c_str(), nullptr, 10));
}
};

View File

@ -150,6 +150,7 @@ class UIElement {
};
virtual ElementType getType() const { return ElementType::Base; }
virtual const char* getTypeName() const { return "UIElement"; }
int getLayoutHeight() const { return absH; }
int getLayoutWidth() const { return absW; }

View File

@ -9,6 +9,232 @@
namespace ThemeEngine {
// --- Container ---
void Container::draw(const GfxRenderer& renderer, const ThemeContext& context) {
if (!isVisible(context)) return;
if (hasBg) {
std::string colStr = context.evaluatestring(bgColorExpr);
uint8_t color = Color::parse(colStr).value;
// Use dithered fill for grayscale values, solid fill for black/white
// Use rounded rect if borderRadius > 0
if (color == 0x00) {
if (borderRadius > 0) {
renderer.fillRoundedRect(absX, absY, absW, absH, borderRadius, true);
} else {
renderer.fillRect(absX, absY, absW, absH, true);
}
} else if (color >= 0xF0) {
if (borderRadius > 0) {
renderer.fillRoundedRect(absX, absY, absW, absH, borderRadius, false);
} else {
renderer.fillRect(absX, absY, absW, absH, false);
}
} else {
if (borderRadius > 0) {
renderer.fillRoundedRectDithered(absX, absY, absW, absH, borderRadius, color);
} else {
renderer.fillRectDithered(absX, absY, absW, absH, color);
}
}
}
// Handle dynamic border expression
bool drawBorder = border;
if (hasBorderExpr()) {
drawBorder = context.evaluateBool(borderExpr.rawExpr);
}
if (drawBorder) {
if (borderRadius > 0) {
renderer.drawRoundedRect(absX, absY, absW, absH, borderRadius, true);
} else {
renderer.drawRect(absX, absY, absW, absH, true);
}
}
for (auto child : children) {
child->draw(renderer, context);
}
markClean();
}
// --- Rectangle ---
void Rectangle::draw(const GfxRenderer& renderer, const ThemeContext& context) {
if (!isVisible(context)) return;
std::string colStr = context.evaluatestring(colorExpr);
uint8_t color = Color::parse(colStr).value;
bool shouldFill = fill;
if (!fillExpr.empty()) {
shouldFill = context.evaluateBool(fillExpr.rawExpr);
}
if (shouldFill) {
// Use dithered fill for grayscale values, solid fill for black/white
// Use rounded rect if borderRadius > 0
if (color == 0x00) {
if (borderRadius > 0) {
renderer.fillRoundedRect(absX, absY, absW, absH, borderRadius, true);
} else {
renderer.fillRect(absX, absY, absW, absH, true);
}
} else if (color >= 0xF0) {
if (borderRadius > 0) {
renderer.fillRoundedRect(absX, absY, absW, absH, borderRadius, false);
} else {
renderer.fillRect(absX, absY, absW, absH, false);
}
} else {
if (borderRadius > 0) {
renderer.fillRoundedRectDithered(absX, absY, absW, absH, borderRadius, color);
} else {
renderer.fillRectDithered(absX, absY, absW, absH, color);
}
}
} else {
// Draw border
bool black = (color == 0x00);
if (borderRadius > 0) {
renderer.drawRoundedRect(absX, absY, absW, absH, borderRadius, black);
} else {
renderer.drawRect(absX, absY, absW, absH, black);
}
}
markClean();
}
// --- Label ---
void Label::draw(const GfxRenderer& renderer, const ThemeContext& context) {
if (!isVisible(context)) return;
std::string finalStr = context.evaluatestring(textExpr);
if (finalStr.empty()) {
markClean();
return;
}
std::string colStr = context.evaluatestring(colorExpr);
uint8_t color = Color::parse(colStr).value;
bool black = (color == 0x00);
int textWidth = renderer.getTextWidth(fontId, finalStr.c_str());
int lineHeight = renderer.getLineHeight(fontId);
std::vector<std::string> lines;
if (absW > 0 && textWidth > absW && maxLines > 1) {
// Logic to wrap text
std::string remaining = finalStr;
while (!remaining.empty() && (int)lines.size() < maxLines) {
// If it fits, add entire line
if (renderer.getTextWidth(fontId, remaining.c_str()) <= absW) {
lines.push_back(remaining);
break;
}
// Binary search for cut point
int len = remaining.length();
int cut = len;
// Find split point
// Optimistic start: approximate chars that fit
int avgCharWidth = renderer.getTextWidth(fontId, "a");
if (avgCharWidth < 1) avgCharWidth = 8;
int approxChars = absW / avgCharWidth;
if (approxChars < 1) approxChars = 1;
if (approxChars >= len) approxChars = len - 1;
// Refine from approxChars
int w = renderer.getTextWidth(fontId, remaining.substr(0, approxChars).c_str());
if (w < absW) {
// Grow
for (int i = approxChars; i <= len; i++) {
if (renderer.getTextWidth(fontId, remaining.substr(0, i).c_str()) > absW) {
cut = i - 1;
break;
}
cut = i;
}
} else {
// Shrink
for (int i = approxChars; i > 0; i--) {
if (renderer.getTextWidth(fontId, remaining.substr(0, i).c_str()) <= absW) {
cut = i;
break;
}
}
}
// Find last space before cut
if (cut < (int)remaining.length()) {
int space = -1;
for (int i = cut; i > 0; i--) {
if (remaining[i] == ' ') {
space = i;
break;
}
}
if (space != -1) cut = space;
}
std::string line = remaining.substr(0, cut);
// If we're at the last allowed line but still have more text
if ((int)lines.size() == maxLines - 1 && cut < (int)remaining.length()) {
if (ellipsis) {
line = renderer.truncatedText(fontId, remaining.c_str(), absW);
}
lines.push_back(line);
break;
}
lines.push_back(line);
// Advance
if (cut < (int)remaining.length()) {
// Skip the space if check
if (remaining[cut] == ' ') cut++;
remaining = remaining.substr(cut);
} else {
remaining = "";
}
}
} else {
// Single line handling (truncate if needed)
if (ellipsis && textWidth > absW && absW > 0) {
finalStr = renderer.truncatedText(fontId, finalStr.c_str(), absW);
}
lines.push_back(finalStr);
}
// Draw lines
int totalTextHeight = lines.size() * lineHeight;
int startY = absY;
// Vertical centering
if (absH > 0 && totalTextHeight < absH) {
startY = absY + (absH - totalTextHeight) / 2;
}
for (size_t i = 0; i < lines.size(); i++) {
int lineWidth = renderer.getTextWidth(fontId, lines[i].c_str());
int drawX = absX;
if (alignment == Alignment::Center && absW > 0) {
drawX = absX + (absW - lineWidth) / 2;
} else if (alignment == Alignment::Right && absW > 0) {
drawX = absX + absW - lineWidth;
}
renderer.drawText(fontId, drawX, startY + i * lineHeight, lines[i].c_str(), black);
}
markClean();
}
// --- BitmapElement ---
void BitmapElement::draw(const GfxRenderer& renderer, const ThemeContext& context) {
if (!isVisible(context)) {
@ -52,7 +278,6 @@ void BitmapElement::draw(const GfxRenderer& renderer, const ThemeContext& contex
if (SdMan.openFileForRead("HOME", path, file)) {
Bitmap bmp(file, true); // (file, dithering=true)
if (bmp.parseHeaders() == BmpReaderError::Ok) {
// Center logic
int drawX = absX;
int drawY = absY;
if (bmp.getWidth() < absW) drawX += (absW - bmp.getWidth()) / 2;
@ -69,7 +294,7 @@ void BitmapElement::draw(const GfxRenderer& renderer, const ThemeContext& contex
}
}
// 3. Fallback to RAM Cache (Standard method)
// 3. Fallback to RAM Cache (Standard method)
if (!drawSuccess) {
const std::vector<uint8_t>* data = ThemeManager::get().getCachedAsset(path);
if (data && !data->empty()) {
@ -168,15 +393,25 @@ void List::draw(const GfxRenderer& renderer, const ThemeContext& context) {
ThemeContext itemContext(&context);
std::string prefix = source + "." + std::to_string(i) + ".";
// Standard list item variables
// Standard list item variables - include all properties for full flexibility
std::string nameVal = context.getString(prefix + "Name");
itemContext.setString("Item.Name", nameVal);
itemContext.setString("Item.Title", context.getString(prefix + "Title"));
itemContext.setString("Item.Value", context.getString(prefix + "Value"));
itemContext.setString("Item.Value", context.getAnyAsString(prefix + "Value"));
itemContext.setString("Item.Type", context.getString(prefix + "Type"));
itemContext.setString("Item.ValueLabel", context.getString(prefix + "ValueLabel"));
itemContext.setString("Item.BgColor", context.getString(prefix + "BgColor"));
itemContext.setBool("Item.Selected", context.getBool(prefix + "Selected"));
itemContext.setBool("Item.Value", context.getBool(prefix + "Value"));
itemContext.setString("Item.Icon", context.getString(prefix + "Icon"));
itemContext.setString("Item.Image", context.getString(prefix + "Image"));
itemContext.setString("Item.Progress", context.getString(prefix + "Progress"));
itemContext.setInt("Item.Index", i);
itemContext.setInt("Item.Count", count);
// ValueIndex may not exist for all item types, so check first
if (context.hasKey(prefix + "ValueIndex")) {
itemContext.setInt("Item.ValueIndex", context.getInt(prefix + "ValueIndex"));
}
// Viewport check
if (direction == Direction::Horizontal) {
@ -239,14 +474,25 @@ void List::draw(const GfxRenderer& renderer, const ThemeContext& context) {
ThemeContext itemContext(&context);
std::string prefix = source + "." + std::to_string(i) + ".";
// Standard list item variables - include all properties for full flexibility
std::string nameVal = context.getString(prefix + "Name");
itemContext.setString("Item.Name", nameVal);
itemContext.setString("Item.Title", context.getString(prefix + "Title"));
itemContext.setString("Item.Value", context.getString(prefix + "Value"));
itemContext.setString("Item.Value", context.getAnyAsString(prefix + "Value"));
itemContext.setString("Item.Type", context.getString(prefix + "Type"));
itemContext.setString("Item.ValueLabel", context.getString(prefix + "ValueLabel"));
itemContext.setString("Item.BgColor", context.getString(prefix + "BgColor"));
itemContext.setBool("Item.Selected", context.getBool(prefix + "Selected"));
itemContext.setBool("Item.Value", context.getBool(prefix + "Value"));
itemContext.setString("Item.Icon", context.getString(prefix + "Icon"));
itemContext.setString("Item.Image", context.getString(prefix + "Image"));
itemContext.setString("Item.Progress", context.getString(prefix + "Progress"));
itemContext.setInt("Item.Index", i);
itemContext.setInt("Item.Count", count);
// ValueIndex may not exist for all item types, so check first
if (context.hasKey(prefix + "ValueIndex")) {
itemContext.setInt("Item.ValueIndex", context.getInt(prefix + "ValueIndex"));
}
// Layout and draw the template for this item
itemTemplate->layout(itemContext, absX, currentY, absW, itemH);

View File

@ -21,11 +21,8 @@ void IniParser::trim(std::string& s) {
std::map<std::string, std::map<std::string, std::string>> IniParser::parse(Stream& stream) {
std::map<std::string, std::map<std::string, std::string>> sections;
// stream check not strictly possible like file, can rely on available()
std::string currentSection = "";
String line; // Use Arduino String for easy file reading, then convert to
// std::string
std::string currentSection;
String line;
while (stream.available()) {
line = stream.readStringUntil('\n');
@ -33,7 +30,7 @@ std::map<std::string, std::map<std::string, std::string>> IniParser::parse(Strea
trim(sLine);
if (sLine.empty() || sLine[0] == ';' || sLine[0] == '#') {
continue; // Skip comments and empty lines
continue;
}
if (sLine.front() == '[' && sLine.back() == ']') {
@ -65,7 +62,7 @@ std::map<std::string, std::map<std::string, std::string>> IniParser::parseString
std::map<std::string, std::map<std::string, std::string>> sections;
std::stringstream ss(content);
std::string line;
std::string currentSection = "";
std::string currentSection;
while (std::getline(ss, line)) {
trim(line);

View File

@ -22,13 +22,23 @@ void Icon::draw(const GfxRenderer& renderer, const ThemeContext& context) {
std::string colStr = context.evaluatestring(colorExpr);
uint8_t color = Color::parse(colStr).value;
bool black = (color == 0x00);
// Draw as black if color is dark (< 0x80), as white if light
// This allows grayscale colors to render visibly
bool black = (color < 0x80);
// Use absW/absH if set, otherwise use iconSize
int w = absW > 0 ? absW : iconSize;
int h = absH > 0 ? absH : iconSize;
int cx = absX + w / 2;
int cy = absY + h / 2;
// iconSize determines the actual drawn icon size
// absW/absH determine the bounding box for centering
int drawSize = iconSize;
int boundW = absW > 0 ? absW : iconSize;
int boundH = absH > 0 ? absH : iconSize;
// Center the icon within its bounding box
int iconX = absX + (boundW - drawSize) / 2;
int iconY = absY + (boundH - drawSize) / 2;
int w = drawSize;
int h = drawSize;
int cx = iconX + w / 2;
int cy = iconY + h / 2;
// 1. Try to load as a theme asset (exact match or .bmp extension)
std::string path = iconName;
@ -45,13 +55,14 @@ void Icon::draw(const GfxRenderer& renderer, const ThemeContext& context) {
if (data && !data->empty()) {
Bitmap bmp(data->data(), data->size());
if (bmp.parseHeaders() == BmpReaderError::Ok) {
renderer.drawTransparentBitmap(bmp, absX, absY, w, h);
renderer.drawTransparentBitmap(bmp, iconX, iconY, w, h);
markClean();
return;
}
}
// 2. Built-in icons (simple geometric shapes) as fallback
// All icons use iconX, iconY, w, h, cx, cy for proper centering
if (iconName == "heart" || iconName == "favorite") {
// Simple heart shape approximation
int s = w / 4;
@ -66,8 +77,8 @@ void Icon::draw(const GfxRenderer& renderer, const ThemeContext& context) {
// Book icon
int bw = w * 2 / 3;
int bh = h * 3 / 4;
int bx = absX + (w - bw) / 2;
int by = absY + (h - bh) / 2;
int bx = iconX + (w - bw) / 2;
int by = iconY + (h - bh) / 2;
renderer.drawRect(bx, by, bw, bh, black);
renderer.drawLine(bx + bw / 3, by, bx + bw / 3, by + bh - 1, black);
// Pages
@ -77,8 +88,8 @@ void Icon::draw(const GfxRenderer& renderer, const ThemeContext& context) {
// Folder icon
int fw = w * 3 / 4;
int fh = h * 2 / 3;
int fx = absX + (w - fw) / 2;
int fy = absY + (h - fh) / 2;
int fx = iconX + (w - fw) / 2;
int fy = iconY + (h - fh) / 2;
// Tab
renderer.fillRect(fx, fy, fw / 3, fh / 6, black);
// Body
@ -93,15 +104,15 @@ void Icon::draw(const GfxRenderer& renderer, const ThemeContext& context) {
renderer.drawRect(cx - ir, cy - ir, ir * 2, ir * 2, black);
// Teeth
int t = r / 3;
renderer.fillRect(cx - t / 2, absY, t, r - ir, black);
renderer.fillRect(cx - t / 2, iconY, t, r - ir, black);
renderer.fillRect(cx - t / 2, cy + r, t, r - ir, black);
renderer.fillRect(absX, cy - t / 2, r - ir, t, black);
renderer.fillRect(iconX, cy - t / 2, r - ir, t, black);
renderer.fillRect(cx + r, cy - t / 2, r - ir, t, black);
} else if (iconName == "transfer" || iconName == "arrow" || iconName == "send") {
// Arrow pointing right
int aw = w / 2;
int ah = h / 3;
int ax = absX + w / 4;
int ax = iconX + w / 4;
int ay = cy - ah / 2;
// Shaft
renderer.fillRect(ax, ay, aw, ah, black);
@ -114,8 +125,8 @@ void Icon::draw(const GfxRenderer& renderer, const ThemeContext& context) {
// Device/tablet icon
int dw = w * 2 / 3;
int dh = h * 3 / 4;
int dx = absX + (w - dw) / 2;
int dy = absY + (h - dh) / 2;
int dx = iconX + (w - dw) / 2;
int dy = iconY + (h - dh) / 2;
renderer.drawRect(dx, dy, dw, dh, black);
// Screen
renderer.drawRect(dx + 2, dy + 2, dw - 4, dh - 8, black);
@ -125,18 +136,18 @@ void Icon::draw(const GfxRenderer& renderer, const ThemeContext& context) {
// Battery icon
int bw = w * 3 / 4;
int bh = h / 2;
int bx = absX + (w - bw) / 2;
int by = absY + (h - bh) / 2;
int bx = iconX + (w - bw) / 2;
int by = iconY + (h - bh) / 2;
renderer.drawRect(bx, by, bw - 3, bh, black);
renderer.fillRect(bx + bw - 3, by + bh / 4, 3, bh / 2, black);
} else if (iconName == "check" || iconName == "checkmark") {
// Checkmark
int x1 = absX + w / 4;
// Checkmark - use iconX/iconY for proper centering
int x1 = iconX + w / 4;
int y1 = cy;
int x2 = cx;
int y2 = absY + h * 3 / 4;
int x3 = absX + w * 3 / 4;
int y3 = absY + h / 4;
int y2 = iconY + h * 3 / 4;
int x3 = iconX + w * 3 / 4;
int y3 = iconY + h / 4;
renderer.drawLine(x1, y1, x2, y2, black);
renderer.drawLine(x2, y2, x3, y3, black);
// Thicken
@ -163,11 +174,18 @@ void Icon::draw(const GfxRenderer& renderer, const ThemeContext& context) {
renderer.drawLine(cx, cy + s - i, cx - s + i, cy, black);
renderer.drawLine(cx, cy + s - i, cx + s - i, cy, black);
}
} else if (iconName == "right") {
// Right arrow
int s = w / 3;
for (int i = 0; i < s; i++) {
renderer.drawLine(cx + s - i, cy, cx, cy - s + i, black);
renderer.drawLine(cx + s - i, cy, cx, cy + s - i, black);
}
} else {
// Unknown icon - draw placeholder
renderer.drawRect(absX, absY, w, h, black);
renderer.drawLine(absX, absY, absX + w - 1, absY + h - 1, black);
renderer.drawLine(absX + w - 1, absY, absX, absY + h - 1, black);
renderer.drawRect(iconX, iconY, w, h, black);
renderer.drawLine(iconX, iconY, iconX + w - 1, iconY + h - 1, black);
renderer.drawLine(iconX + w - 1, iconY, iconX, iconY + h - 1, black);
}
markClean();

View File

@ -3,6 +3,7 @@
#include <SDCardManager.h>
#include <algorithm>
#include <cstdlib>
#include <map>
#include <vector>
@ -12,9 +13,7 @@
namespace ThemeEngine {
void ThemeManager::begin() {
// Default fonts or setup
}
void ThemeManager::begin() {}
void ThemeManager::registerFont(const std::string& name, int id) { fontMap[name] = id; }
@ -56,6 +55,11 @@ UIElement* ThemeManager::createElement(const std::string& id, const std::string&
return nullptr;
}
// Parse integer safely - returns 0 on error
static int parseIntSafe(const std::string& val) {
return static_cast<int>(std::strtol(val.c_str(), nullptr, 10));
}
void ThemeManager::applyProperties(UIElement* elem, const std::map<std::string, std::string>& props) {
const auto elemType = elem->getType();
@ -63,7 +67,7 @@ void ThemeManager::applyProperties(UIElement* elem, const std::map<std::string,
const std::string& key = kv.first;
const std::string& val = kv.second;
// ========== Common properties ==========
// Common properties
if (key == "X")
elem->setX(Dimension::parse(val));
else if (key == "Y")
@ -77,7 +81,7 @@ void ThemeManager::applyProperties(UIElement* elem, const std::map<std::string,
else if (key == "Cacheable")
elem->setCacheable(val == "true" || val == "1");
// ========== Rectangle properties ==========
// Rectangle properties
else if (key == "Fill") {
if (elemType == UIElement::ElementType::Rectangle) {
auto rect = static_cast<Rectangle*>(elem);
@ -105,7 +109,7 @@ void ThemeManager::applyProperties(UIElement* elem, const std::map<std::string,
}
}
// ========== Container properties ==========
// Container properties
else if (key == "Border") {
if (auto c = elem->asContainer()) {
if (val.find('{') != std::string::npos) {
@ -117,24 +121,30 @@ void ThemeManager::applyProperties(UIElement* elem, const std::map<std::string,
} else if (key == "Padding") {
if (elemType == UIElement::ElementType::Container || elemType == UIElement::ElementType::HStack ||
elemType == UIElement::ElementType::VStack || elemType == UIElement::ElementType::Grid) {
static_cast<Container*>(elem)->setPadding(std::stoi(val));
static_cast<Container*>(elem)->setPadding(parseIntSafe(val));
} else if (elemType == UIElement::ElementType::TabBar) {
static_cast<TabBar*>(elem)->setPadding(std::stoi(val));
static_cast<TabBar*>(elem)->setPadding(parseIntSafe(val));
}
} else if (key == "BorderRadius") {
if (elemType == UIElement::ElementType::Container || elemType == UIElement::ElementType::HStack ||
elemType == UIElement::ElementType::VStack || elemType == UIElement::ElementType::Grid) {
static_cast<Container*>(elem)->setBorderRadius(std::stoi(val));
static_cast<Container*>(elem)->setBorderRadius(parseIntSafe(val));
} else if (elemType == UIElement::ElementType::Bitmap) {
static_cast<BitmapElement*>(elem)->setBorderRadius(std::stoi(val));
static_cast<BitmapElement*>(elem)->setBorderRadius(parseIntSafe(val));
} else if (elemType == UIElement::ElementType::Rectangle) {
static_cast<Rectangle*>(elem)->setBorderRadius(parseIntSafe(val));
} else if (elemType == UIElement::ElementType::Badge) {
static_cast<Badge*>(elem)->setBorderRadius(parseIntSafe(val));
} else if (elemType == UIElement::ElementType::Toggle) {
static_cast<Toggle*>(elem)->setBorderRadius(parseIntSafe(val));
}
} else if (key == "Spacing") {
if (elemType == UIElement::ElementType::HStack) {
static_cast<HStack*>(elem)->setSpacing(std::stoi(val));
static_cast<HStack*>(elem)->setSpacing(parseIntSafe(val));
} else if (elemType == UIElement::ElementType::VStack) {
static_cast<VStack*>(elem)->setSpacing(std::stoi(val));
static_cast<VStack*>(elem)->setSpacing(parseIntSafe(val));
} else if (elemType == UIElement::ElementType::List) {
static_cast<List*>(elem)->setSpacing(std::stoi(val));
static_cast<List*>(elem)->setSpacing(parseIntSafe(val));
}
} else if (key == "CenterVertical") {
if (elemType == UIElement::ElementType::HStack) {
@ -146,7 +156,7 @@ void ThemeManager::applyProperties(UIElement* elem, const std::map<std::string,
}
}
// ========== Label properties ==========
// Label properties
else if (key == "Text") {
if (elemType == UIElement::ElementType::Label) {
static_cast<Label*>(elem)->setText(val);
@ -176,7 +186,7 @@ void ThemeManager::applyProperties(UIElement* elem, const std::map<std::string,
}
} else if (key == "MaxLines") {
if (elemType == UIElement::ElementType::Label) {
static_cast<Label*>(elem)->setMaxLines(std::stoi(val));
static_cast<Label*>(elem)->setMaxLines(parseIntSafe(val));
}
} else if (key == "Ellipsis") {
if (elemType == UIElement::ElementType::Label) {
@ -184,7 +194,7 @@ void ThemeManager::applyProperties(UIElement* elem, const std::map<std::string,
}
}
// ========== Bitmap/Icon properties ==========
// Bitmap/Icon properties
else if (key == "Src") {
if (elemType == UIElement::ElementType::Bitmap) {
auto b = static_cast<BitmapElement*>(elem);
@ -206,11 +216,11 @@ void ThemeManager::applyProperties(UIElement* elem, const std::map<std::string,
}
} else if (key == "IconSize") {
if (elemType == UIElement::ElementType::Icon) {
static_cast<Icon*>(elem)->setIconSize(std::stoi(val));
static_cast<Icon*>(elem)->setIconSize(parseIntSafe(val));
}
}
// ========== List properties ==========
// List properties
else if (key == "Source") {
if (elemType == UIElement::ElementType::List) {
static_cast<List*>(elem)->setSource(val);
@ -221,11 +231,11 @@ void ThemeManager::applyProperties(UIElement* elem, const std::map<std::string,
}
} else if (key == "ItemHeight") {
if (elemType == UIElement::ElementType::List) {
static_cast<List*>(elem)->setItemHeight(std::stoi(val));
static_cast<List*>(elem)->setItemHeight(parseIntSafe(val));
}
} else if (key == "ItemWidth") {
if (elemType == UIElement::ElementType::List) {
static_cast<List*>(elem)->setItemWidth(std::stoi(val));
static_cast<List*>(elem)->setItemWidth(parseIntSafe(val));
}
} else if (key == "Direction") {
if (elemType == UIElement::ElementType::List) {
@ -233,21 +243,21 @@ void ThemeManager::applyProperties(UIElement* elem, const std::map<std::string,
}
} else if (key == "Columns") {
if (elemType == UIElement::ElementType::List) {
static_cast<List*>(elem)->setColumns(std::stoi(val));
static_cast<List*>(elem)->setColumns(parseIntSafe(val));
} else if (elemType == UIElement::ElementType::Grid) {
static_cast<Grid*>(elem)->setColumns(std::stoi(val));
static_cast<Grid*>(elem)->setColumns(parseIntSafe(val));
}
} else if (key == "RowSpacing") {
if (elemType == UIElement::ElementType::Grid) {
static_cast<Grid*>(elem)->setRowSpacing(std::stoi(val));
static_cast<Grid*>(elem)->setRowSpacing(parseIntSafe(val));
}
} else if (key == "ColSpacing") {
if (elemType == UIElement::ElementType::Grid) {
static_cast<Grid*>(elem)->setColSpacing(std::stoi(val));
static_cast<Grid*>(elem)->setColSpacing(parseIntSafe(val));
}
}
// ========== ProgressBar properties ==========
// ProgressBar properties
else if (key == "Value") {
if (elemType == UIElement::ElementType::ProgressBar) {
static_cast<ProgressBar*>(elem)->setValue(val);
@ -281,18 +291,18 @@ void ThemeManager::applyProperties(UIElement* elem, const std::map<std::string,
}
}
// ========== Divider properties ==========
// Divider properties
else if (key == "Horizontal") {
if (elemType == UIElement::ElementType::Divider) {
static_cast<Divider*>(elem)->setHorizontal(val == "true" || val == "1");
}
} else if (key == "Thickness") {
if (elemType == UIElement::ElementType::Divider) {
static_cast<Divider*>(elem)->setThickness(std::stoi(val));
static_cast<Divider*>(elem)->setThickness(parseIntSafe(val));
}
}
// ========== Toggle properties ==========
// Toggle properties
else if (key == "OnColor") {
if (elemType == UIElement::ElementType::Toggle) {
static_cast<Toggle*>(elem)->setOnColor(val);
@ -301,34 +311,42 @@ void ThemeManager::applyProperties(UIElement* elem, const std::map<std::string,
if (elemType == UIElement::ElementType::Toggle) {
static_cast<Toggle*>(elem)->setOffColor(val);
}
} else if (key == "KnobColor") {
if (elemType == UIElement::ElementType::Toggle) {
static_cast<Toggle*>(elem)->setKnobColor(val);
}
} else if (key == "TrackWidth") {
if (elemType == UIElement::ElementType::Toggle) {
static_cast<Toggle*>(elem)->setTrackWidth(std::stoi(val));
static_cast<Toggle*>(elem)->setTrackWidth(parseIntSafe(val));
} else if (elemType == UIElement::ElementType::ScrollIndicator) {
static_cast<ScrollIndicator*>(elem)->setTrackWidth(std::stoi(val));
static_cast<ScrollIndicator*>(elem)->setTrackWidth(parseIntSafe(val));
}
} else if (key == "TrackHeight") {
if (elemType == UIElement::ElementType::Toggle) {
static_cast<Toggle*>(elem)->setTrackHeight(std::stoi(val));
static_cast<Toggle*>(elem)->setTrackHeight(parseIntSafe(val));
}
} else if (key == "KnobSize") {
if (elemType == UIElement::ElementType::Toggle) {
static_cast<Toggle*>(elem)->setKnobSize(std::stoi(val));
static_cast<Toggle*>(elem)->setKnobSize(parseIntSafe(val));
}
} else if (key == "KnobRadius") {
if (elemType == UIElement::ElementType::Toggle) {
static_cast<Toggle*>(elem)->setKnobRadius(parseIntSafe(val));
}
}
// ========== TabBar properties ==========
// TabBar properties
else if (key == "Selected") {
if (elemType == UIElement::ElementType::TabBar) {
static_cast<TabBar*>(elem)->setSelected(val);
}
} else if (key == "TabSpacing") {
if (elemType == UIElement::ElementType::TabBar) {
static_cast<TabBar*>(elem)->setTabSpacing(std::stoi(val));
static_cast<TabBar*>(elem)->setTabSpacing(parseIntSafe(val));
}
} else if (key == "IndicatorHeight") {
if (elemType == UIElement::ElementType::TabBar) {
static_cast<TabBar*>(elem)->setIndicatorHeight(std::stoi(val));
static_cast<TabBar*>(elem)->setIndicatorHeight(parseIntSafe(val));
}
} else if (key == "ShowIndicator") {
if (elemType == UIElement::ElementType::TabBar) {
@ -336,7 +354,7 @@ void ThemeManager::applyProperties(UIElement* elem, const std::map<std::string,
}
}
// ========== ScrollIndicator properties ==========
// ScrollIndicator properties
else if (key == "Position") {
if (elemType == UIElement::ElementType::ScrollIndicator) {
static_cast<ScrollIndicator*>(elem)->setPosition(val);
@ -351,14 +369,14 @@ void ThemeManager::applyProperties(UIElement* elem, const std::map<std::string,
}
}
// ========== Badge properties ==========
// Badge properties
else if (key == "PaddingH") {
if (elemType == UIElement::ElementType::Badge) {
static_cast<Badge*>(elem)->setPaddingH(std::stoi(val));
static_cast<Badge*>(elem)->setPaddingH(parseIntSafe(val));
}
} else if (key == "PaddingV") {
if (elemType == UIElement::ElementType::Badge) {
static_cast<Badge*>(elem)->setPaddingV(std::stoi(val));
static_cast<Badge*>(elem)->setPaddingV(parseIntSafe(val));
}
}
}
@ -385,7 +403,6 @@ const std::vector<uint8_t>* ThemeManager::getCachedAsset(const std::string& path
const ProcessedAsset* ThemeManager::getProcessedAsset(const std::string& path, GfxRenderer::Orientation orientation,
int targetW, int targetH) {
// Include dimensions in cache key for scaled images
std::string cacheKey = path;
if (targetW > 0 && targetH > 0) {
cacheKey += ":" + std::to_string(targetW) + "x" + std::to_string(targetH);
@ -414,8 +431,8 @@ void ThemeManager::clearAssetCaches() {
}
void ThemeManager::unloadTheme() {
for (auto it = elements.begin(); it != elements.end(); ++it) {
delete it->second;
for (auto& kv : elements) {
delete kv.second;
}
elements.clear();
clearAssetCaches();
@ -456,30 +473,23 @@ void ThemeManager::loadTheme(const std::string& themeName) {
if (SdMan.openFileForRead("Theme", path, file)) {
sections = IniParser::parse(file);
file.close();
Serial.println("[ThemeManager] Loaded Default theme from SD card");
}
} else {
Serial.println("[ThemeManager] Using embedded Default theme");
sections = IniParser::parseString(getDefaultThemeIni());
}
currentThemeName = "Default";
} else {
std::string path = "/themes/" + themeName + "/theme.ini";
Serial.printf("[ThemeManager] Checking path: %s\n", path.c_str());
if (!SdMan.exists(path.c_str())) {
Serial.printf("[ThemeManager] Theme %s not found, using Default\n", themeName.c_str());
sections = IniParser::parseString(getDefaultThemeIni());
currentThemeName = "Default";
} else {
FsFile file;
if (SdMan.openFileForRead("Theme", path, file)) {
Serial.printf("[ThemeManager] Parsing theme file...\n");
sections = IniParser::parse(file);
file.close();
Serial.printf("[ThemeManager] Parsed %d sections from %s\n", (int)sections.size(), themeName.c_str());
} else {
Serial.printf("[ThemeManager] Failed to open %s, using Default\n", path.c_str());
sections = IniParser::parseString(getDefaultThemeIni());
currentThemeName = "Default";
}
@ -487,26 +497,27 @@ void ThemeManager::loadTheme(const std::string& themeName) {
}
// Read theme configuration from [Global] section
navBookCount = 1; // Default
navBookCount = 1;
if (sections.count("Global")) {
const auto& global = sections.at("Global");
if (global.count("NavBookCount")) {
navBookCount = std::stoi(global.at("NavBookCount"));
navBookCount = parseIntSafe(global.at("NavBookCount"));
if (navBookCount < 1) navBookCount = 1;
if (navBookCount > 10) navBookCount = 10; // Reasonable max
if (navBookCount > 10) navBookCount = 10;
}
}
// Pass 1: Creation
// Pass 1: Create elements
for (const auto& sec : sections) {
std::string id = sec.first;
const std::string& id = sec.first;
const std::map<std::string, std::string>& props = sec.second;
if (id == "Global") continue;
auto it = props.find("Type");
if (it == props.end()) continue;
std::string type = it->second;
const std::string& type = it->second;
if (type.empty()) continue;
UIElement* elem = createElement(id, type);
@ -515,10 +526,10 @@ void ThemeManager::loadTheme(const std::string& themeName) {
}
}
// Pass 2: Properties & Parent Wiring
// Pass 2: Apply properties and wire parent relationships
std::vector<List*> lists;
for (const auto& sec : sections) {
std::string id = sec.first;
const std::string& id = sec.first;
if (id == "Global") continue;
if (elements.find(id) == elements.end()) continue;
@ -529,38 +540,63 @@ void ThemeManager::loadTheme(const std::string& themeName) {
lists.push_back(static_cast<List*>(elem));
}
// Wire parent relationship (fallback if Children not specified)
if (sec.second.count("Parent")) {
std::string parentId = sec.second.at("Parent");
const std::string& parentId = sec.second.at("Parent");
if (elements.count(parentId)) {
UIElement* parent = elements[parentId];
if (auto c = parent->asContainer()) {
c->addChild(elem);
const auto& children = c->getChildren();
if (std::find(children.begin(), children.end(), elem) == children.end()) {
c->addChild(elem);
}
}
} else {
Serial.printf("[ThemeManager] WARN: Parent %s not found for %s\n", parentId.c_str(), id.c_str());
}
}
// Children property - explicit ordering
if (sec.second.count("Children")) {
if (auto c = elem->asContainer()) {
c->clearChildren();
std::string s = sec.second.at("Children");
size_t pos = 0;
auto processChild = [&](const std::string& childName) {
std::string childId = childName;
size_t start = childId.find_first_not_of(" ");
size_t end = childId.find_last_not_of(" ");
if (start == std::string::npos) return;
childId = childId.substr(start, end - start + 1);
if (elements.count(childId)) {
c->addChild(elements[childId]);
}
};
while ((pos = s.find(',')) != std::string::npos) {
processChild(s.substr(0, pos));
s.erase(0, pos + 1);
}
processChild(s);
}
}
}
// Pass 3: Resolve List Templates
for (auto l : lists) {
// Pass 3: Resolve list templates
for (auto* l : lists) {
l->resolveTemplate(elements);
}
Serial.printf("[ThemeManager] Theme loaded with %d elements\n", (int)elements.size());
}
void ThemeManager::renderScreen(const std::string& screenName, const GfxRenderer& renderer,
const ThemeContext& context) {
if (elements.count(screenName) == 0) {
Serial.printf("[ThemeManager] Screen '%s' not found\n", screenName.c_str());
return;
}
UIElement* root = elements[screenName];
root->layout(context, 0, 0, renderer.getScreenWidth(), renderer.getScreenHeight());
root->draw(renderer, context);
}

View File

@ -1,11 +1,16 @@
#include "SettingsActivity.h"
#include <GfxRenderer.h>
#include <HardwareSerial.h>
#include "Battery.h"
#include "CalibreSettingsActivity.h"
#include "CategorySettingsActivity.h"
#include "ClearCacheActivity.h"
#include "CrossPointSettings.h"
#include "KOReaderSettingsActivity.h"
#include "MappedInputManager.h"
#include "OtaUpdateActivity.h"
#include "ThemeSelectionActivity.h"
#include "fontIds.h"
const char* SettingsActivity::categoryNames[categoryCount] = {"Display", "Reader", "Controls", "System"};
@ -13,7 +18,6 @@ const char* SettingsActivity::categoryNames[categoryCount] = {"Display", "Reader
namespace {
constexpr int displaySettingsCount = 6;
const SettingInfo displaySettings[displaySettingsCount] = {
// Should match with SLEEP_SCREEN_MODE
SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover", "None"}),
SettingInfo::Enum("Sleep Screen Cover Mode", &CrossPointSettings::sleepScreenCoverMode, {"Fit", "Crop"}),
SettingInfo::Enum("Status Bar", &CrossPointSettings::statusBar, {"None", "No Progress", "Full"}),
@ -40,17 +44,67 @@ constexpr int controlsSettingsCount = 4;
const SettingInfo controlsSettings[controlsSettingsCount] = {
SettingInfo::Enum("Front Button Layout", &CrossPointSettings::frontButtonLayout,
{"Bck, Cnfrm, Lft, Rght", "Lft, Rght, Bck, Cnfrm", "Lft, Bck, Cnfrm, Rght"}),
SettingInfo::Enum("Side Button Layout (reader)", &CrossPointSettings::sideButtonLayout,
{"Prev, Next", "Next, Prev"}),
SettingInfo::Enum("Side Button Layout (reader)", &CrossPointSettings::sideButtonLayout, {"Prev, Next", "Next, Prev"}),
SettingInfo::Toggle("Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip),
SettingInfo::Enum("Short Power Button Click", &CrossPointSettings::shortPwrBtn, {"Ignore", "Sleep", "Page Turn"})};
constexpr int systemSettingsCount = 5;
const SettingInfo systemSettings[systemSettingsCount] = {
SettingInfo::Enum("Time to Sleep", &CrossPointSettings::sleepTimeout,
{"1 min", "5 min", "10 min", "15 min", "30 min"}),
SettingInfo::Action("KOReader Sync"), SettingInfo::Action("Calibre Settings"), SettingInfo::Action("Clear Cache"),
SettingInfo::Enum("Time to Sleep", &CrossPointSettings::sleepTimeout, {"1 min", "5 min", "10 min", "15 min", "30 min"}),
SettingInfo::Action("KOReader Sync"),
SettingInfo::Action("Calibre Settings"),
SettingInfo::Action("Clear Cache"),
SettingInfo::Action("Check for updates")};
// All categories with their settings
struct CategoryData {
const char* name;
const SettingInfo* settings;
int count;
};
const CategoryData allCategories[4] = {
{"Display", displaySettings, displaySettingsCount},
{"Reader", readerSettings, readerSettingsCount},
{"Controls", controlsSettings, controlsSettingsCount},
{"System", systemSettings, systemSettingsCount}};
void updateContextForSetting(ThemeEngine::ThemeContext& ctx, const std::string& prefix, int i, const SettingInfo& info,
bool isSelected, bool fullUpdate) {
if (fullUpdate) {
ctx.setListItem(prefix, i, "Name", info.name);
ctx.setListItem(prefix, i, "Type",
info.type == SettingType::TOGGLE ? "Toggle"
: info.type == SettingType::ENUM ? "Enum"
: info.type == SettingType::ACTION ? "Action"
: info.type == SettingType::VALUE ? "Value"
: "Unknown");
}
ctx.setListItem(prefix, i, "Selected", isSelected);
// Values definitely need update
if (info.type == SettingType::TOGGLE && info.valuePtr) {
bool val = SETTINGS.*(info.valuePtr);
ctx.setListItem(prefix, i, "Value", val);
ctx.setListItem(prefix, i, "ValueLabel", val ? "On" : "Off");
} else if (info.type == SettingType::ENUM && info.valuePtr) {
uint8_t val = SETTINGS.*(info.valuePtr);
if (val < info.enumValues.size()) {
ctx.setListItem(prefix, i, "Value", info.enumValues[val]);
ctx.setListItem(prefix, i, "ValueLabel", info.enumValues[val]);
ctx.setListItem(prefix, i, "ValueIndex", static_cast<int>(val));
}
} else if (info.type == SettingType::VALUE && info.valuePtr) {
int val = SETTINGS.*(info.valuePtr);
ctx.setListItem(prefix, i, "Value", val);
ctx.setListItem(prefix, i, "ValueLabel", std::to_string(val));
} else if (info.type == SettingType::ACTION) {
if (fullUpdate) {
ctx.setListItem(prefix, i, "Value", "");
ctx.setListItem(prefix, i, "ValueLabel", "");
}
}
}
} // namespace
void SettingsActivity::taskTrampoline(void* param) {
@ -61,26 +115,21 @@ void SettingsActivity::taskTrampoline(void* param) {
void SettingsActivity::onEnter() {
Activity::onEnter();
renderingMutex = xSemaphoreCreateMutex();
// Reset selection to first category
selectedCategoryIndex = 0;
selectedSettingIndex = 0;
// For themed mode, provide all data upfront
if (ThemeEngine::ThemeManager::get().getElement("Settings")) {
updateThemeContext(true); // Full update
}
// Trigger first update
updateRequired = true;
xTaskCreate(&SettingsActivity::taskTrampoline, "SettingsActivityTask",
4096, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
xTaskCreate(&SettingsActivity::taskTrampoline, "SettingsActivityTask", 4096, this, 1, &displayTaskHandle);
}
void SettingsActivity::onExit() {
ActivityWithSubactivity::onExit();
// Wait until not rendering to delete task to avoid killing mid-instruction to
// EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
@ -91,14 +140,26 @@ void SettingsActivity::onExit() {
}
void SettingsActivity::loop() {
if (subActivityExitPending) {
subActivityExitPending = false;
exitActivity();
updateThemeContext(true);
updateRequired = true;
}
if (subActivity) {
subActivity->loop();
return;
}
// Handle category selection
if (ThemeEngine::ThemeManager::get().getElement("Settings")) {
handleThemeInput();
return;
}
// Legacy mode
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
enterCategory(selectedCategoryIndex);
enterCategoryLegacy(selectedCategoryIndex);
return;
}
@ -108,52 +169,25 @@ void SettingsActivity::loop() {
return;
}
// Handle navigation
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
// Move selection up (with wrap-around)
selectedCategoryIndex = (selectedCategoryIndex > 0) ? (selectedCategoryIndex - 1) : (categoryCount - 1);
updateRequired = true;
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
// Move selection down (with wrap around)
selectedCategoryIndex = (selectedCategoryIndex < categoryCount - 1) ? (selectedCategoryIndex + 1) : 0;
updateRequired = true;
}
}
void SettingsActivity::enterCategory(int categoryIndex) {
if (categoryIndex < 0 || categoryIndex >= categoryCount) {
return;
}
void SettingsActivity::enterCategoryLegacy(int categoryIndex) {
if (categoryIndex < 0 || categoryIndex >= categoryCount) return;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity();
const SettingInfo* settingsList = nullptr;
int settingsCount = 0;
switch (categoryIndex) {
case 0: // Display
settingsList = displaySettings;
settingsCount = displaySettingsCount;
break;
case 1: // Reader
settingsList = readerSettings;
settingsCount = readerSettingsCount;
break;
case 2: // Controls
settingsList = controlsSettings;
settingsCount = controlsSettingsCount;
break;
case 3: // System
settingsList = systemSettings;
settingsCount = systemSettingsCount;
break;
}
enterNewActivity(new CategorySettingsActivity(renderer, mappedInput, categoryNames[categoryIndex], settingsList,
settingsCount, [this] {
enterNewActivity(new CategorySettingsActivity(renderer, mappedInput, allCategories[categoryIndex].name,
allCategories[categoryIndex].settings, allCategories[categoryIndex].count,
[this] {
exitActivity();
updateRequired = true;
}));
@ -164,9 +198,11 @@ void SettingsActivity::displayTaskLoop() {
while (true) {
if (updateRequired && !subActivity) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
render();
xSemaphoreGive(renderingMutex);
if (xSemaphoreTake(renderingMutex, portMAX_DELAY) == pdTRUE) {
render();
xSemaphoreGive(renderingMutex);
}
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
@ -175,31 +211,153 @@ void SettingsActivity::displayTaskLoop() {
void SettingsActivity::render() const {
renderer.clearScreen();
if (ThemeEngine::ThemeManager::get().getElement("Settings")) {
ThemeEngine::ThemeManager::get().renderScreen("Settings", renderer, themeContext);
renderer.displayBuffer();
return;
}
// Legacy rendering
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
// Draw header
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Settings", true, EpdFontFamily::BOLD);
// Draw selection
renderer.fillRect(0, 60 + selectedCategoryIndex * 30 - 2, pageWidth - 1, 30);
// Draw all categories
for (int i = 0; i < categoryCount; i++) {
const int categoryY = 60 + i * 30; // 30 pixels between categories
// Draw category name
renderer.drawText(UI_10_FONT_ID, 20, categoryY, categoryNames[i], i != selectedCategoryIndex);
renderer.drawText(UI_10_FONT_ID, 20, 60 + i * 30, categoryNames[i], i != selectedCategoryIndex);
}
// Draw version text above button hints
renderer.drawText(SMALL_FONT_ID, pageWidth - 20 - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION),
pageHeight - 60, CROSSPOINT_VERSION);
// Draw help text
const auto labels = mappedInput.mapLabels("« Back", "Select", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
// Always use standard refresh for settings screen
renderer.displayBuffer();
}
void SettingsActivity::updateThemeContext(bool fullUpdate) {
themeContext.setInt("System.Battery", battery.readPercentage());
// Categories
if (fullUpdate) {
themeContext.setInt("Categories.Count", categoryCount);
}
themeContext.setInt("Categories.Selected", selectedCategoryIndex);
for (int i = 0; i < categoryCount; i++) {
if (fullUpdate) {
themeContext.setListItem("Categories", i, "Name", allCategories[i].name);
themeContext.setListItem("Categories", i, "SettingsCount", allCategories[i].count);
}
themeContext.setListItem("Categories", i, "Selected", i == selectedCategoryIndex);
}
// Provide ALL settings for ALL categories
// Format: Category0.Settings.0.Name, Category0.Settings.1.Name, etc.
for (int cat = 0; cat < categoryCount; cat++) {
std::string catPrefix = "Category" + std::to_string(cat) + ".Settings";
for (int i = 0; i < allCategories[cat].count; i++) {
bool isSelected = (cat == selectedCategoryIndex && i == selectedSettingIndex);
updateContextForSetting(themeContext, catPrefix, i, allCategories[cat].settings[i], isSelected, fullUpdate);
}
}
// Also provide current category's settings as "Settings" for simpler themes
if (fullUpdate) {
themeContext.setInt("Settings.Count", allCategories[selectedCategoryIndex].count);
}
for (int i = 0; i < allCategories[selectedCategoryIndex].count; i++) {
updateContextForSetting(themeContext, "Settings", i, allCategories[selectedCategoryIndex].settings[i],
i == selectedSettingIndex, fullUpdate);
}
}
void SettingsActivity::handleThemeInput() {
const int currentCategorySettingsCount = allCategories[selectedCategoryIndex].count;
// Up/Down navigates settings within current category
if (mappedInput.wasPressed(MappedInputManager::Button::Up)) {
selectedSettingIndex = (selectedSettingIndex > 0) ? (selectedSettingIndex - 1) : (currentCategorySettingsCount - 1);
updateThemeContext(false); // Partial update
updateRequired = true;
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Down)) {
selectedSettingIndex = (selectedSettingIndex < currentCategorySettingsCount - 1) ? (selectedSettingIndex + 1) : 0;
updateThemeContext(false); // Partial update
updateRequired = true;
return;
}
// Left/Right/PageBack/PageForward switches categories
if (mappedInput.wasPressed(MappedInputManager::Button::Left) ||
mappedInput.wasPressed(MappedInputManager::Button::PageBack)) {
selectedCategoryIndex = (selectedCategoryIndex > 0) ? (selectedCategoryIndex - 1) : (categoryCount - 1);
selectedSettingIndex = 0; // Reset to first setting in new category
updateThemeContext(true); // Full update (category changed)
updateRequired = true;
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Right) ||
mappedInput.wasPressed(MappedInputManager::Button::PageForward)) {
selectedCategoryIndex = (selectedCategoryIndex < categoryCount - 1) ? (selectedCategoryIndex + 1) : 0;
selectedSettingIndex = 0;
updateThemeContext(true); // Full update
updateRequired = true;
return;
}
// Confirm toggles/activates current setting
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
toggleCurrentSetting();
updateThemeContext(false); // Values changed, partial update is enough (names don't change)
updateRequired = true;
return;
}
// Back exits
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
SETTINGS.saveToFile();
onGoHome();
}
}
void SettingsActivity::toggleCurrentSetting() {
const auto& setting = allCategories[selectedCategoryIndex].settings[selectedSettingIndex];
if (setting.type == SettingType::TOGGLE && setting.valuePtr) {
bool currentVal = SETTINGS.*(setting.valuePtr);
SETTINGS.*(setting.valuePtr) = !currentVal;
} else if (setting.type == SettingType::ENUM && setting.valuePtr) {
uint8_t val = SETTINGS.*(setting.valuePtr);
SETTINGS.*(setting.valuePtr) = (val + 1) % static_cast<uint8_t>(setting.enumValues.size());
} else if (setting.type == SettingType::VALUE && setting.valuePtr) {
int8_t val = SETTINGS.*(setting.valuePtr);
if (val + setting.valueRange.step > setting.valueRange.max) {
SETTINGS.*(setting.valuePtr) = setting.valueRange.min;
} else {
SETTINGS.*(setting.valuePtr) = val + setting.valueRange.step;
}
} else if (setting.type == SettingType::ACTION) {
if (strcmp(setting.name, "KOReader Sync") == 0) {
enterNewActivity(new KOReaderSettingsActivity(renderer, mappedInput, [this] { subActivityExitPending = true; }));
} else if (strcmp(setting.name, "Calibre Settings") == 0) {
enterNewActivity(new CalibreSettingsActivity(renderer, mappedInput, [this] { subActivityExitPending = true; }));
} else if (strcmp(setting.name, "Clear Cache") == 0) {
enterNewActivity(new ClearCacheActivity(renderer, mappedInput, [this] { subActivityExitPending = true; }));
} else if (strcmp(setting.name, "Check for updates") == 0) {
enterNewActivity(new OtaUpdateActivity(renderer, mappedInput, [this] { subActivityExitPending = true; }));
} else if (strcmp(setting.name, "Theme") == 0) {
enterNewActivity(new ThemeSelectionActivity(renderer, mappedInput, [this] { subActivityExitPending = true; }));
}
vTaskDelay(50 / portTICK_PERIOD_MS);
}
SETTINGS.saveToFile();
}

View File

@ -4,9 +4,9 @@
#include <freertos/task.h>
#include <functional>
#include <string>
#include <vector>
#include "ThemeContext.h"
#include "ThemeManager.h"
#include "activities/ActivityWithSubactivity.h"
class CrossPointSettings;
@ -16,7 +16,9 @@ class SettingsActivity final : public ActivityWithSubactivity {
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
bool updateRequired = false;
int selectedCategoryIndex = 0; // Currently selected category
bool subActivityExitPending = false;
int selectedCategoryIndex = 0;
int selectedSettingIndex = 0;
const std::function<void()> onGoHome;
static constexpr int categoryCount = 4;
@ -25,7 +27,13 @@ class SettingsActivity final : public ActivityWithSubactivity {
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void render() const;
void enterCategory(int categoryIndex);
void enterCategoryLegacy(int categoryIndex);
// Theme support
ThemeEngine::ThemeContext themeContext;
void updateThemeContext(bool fullUpdate = false);
void handleThemeInput();
void toggleCurrentSetting();
public:
explicit SettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,