mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-04 14:47:37 +03:00
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:
parent
7bfb3af0da
commit
e6b3ecc519
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -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; }
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user