#pragma once #include #include #include #include "ThemeContext.h" #include "ThemeTypes.h" #include "UIElement.h" namespace ThemeEngine { // --- Container --- class Container : public UIElement { protected: std::vector children; Expression bgColorExpr; bool hasBg = false; bool border = false; Expression borderExpr; // Dynamic border based on expression int padding = 0; // Inner padding for children int borderRadius = 0; // Corner radius (for future rounded rect support) public: explicit Container(const std::string& id) : UIElement(id), bgColorExpr(Expression::parse("0xFF")) {} virtual ~Container() { for (auto child : children) delete child; } Container* asContainer() override { return this; } ElementType getType() const override { return ElementType::Container; } void addChild(UIElement* child) { children.push_back(child); } const std::vector& getChildren() const { return children; } void setBackgroundColorExpr(const std::string& expr) { bgColorExpr = Expression::parse(expr); hasBg = true; markDirty(); } void setBorder(bool enable) { border = enable; markDirty(); } void setBorderExpr(const std::string& expr) { borderExpr = Expression::parse(expr); markDirty(); } bool hasBorderExpr() const { return !borderExpr.empty(); } void setPadding(int p) { padding = p; markDirty(); } int getPadding() const { return padding; } void setBorderRadius(int r) { borderRadius = r; markDirty(); } int getBorderRadius() const { return borderRadius; } void layout(const ThemeContext& context, int parentX, int parentY, int parentW, int parentH) override { UIElement::layout(context, parentX, parentY, parentW, parentH); // Children are laid out with padding offset int childX = absX + padding; int childY = absY + padding; int childW = absW - 2 * padding; int childH = absH - 2 * padding; for (auto child : children) { child->layout(context, childX, childY, childW, childH); } } void markDirty() override { UIElement::markDirty(); for (auto child : children) { child->markDirty(); } } 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(); } }; // --- Rectangle --- class Rectangle : public UIElement { bool fill = false; Expression fillExpr; // Dynamic fill based on expression Expression colorExpr; public: explicit Rectangle(const std::string& id) : UIElement(id), colorExpr(Expression::parse("0x00")) {} ElementType getType() const override { return ElementType::Rectangle; } void setFill(bool f) { fill = f; markDirty(); } void setFillExpr(const std::string& expr) { fillExpr = Expression::parse(expr); markDirty(); } void setColorExpr(const std::string& c) { colorExpr = Expression::parse(c); 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(); } }; // --- Label --- class Label : public UIElement { public: enum class Alignment { Left, Center, Right }; private: Expression textExpr; int fontId = 0; Alignment alignment = Alignment::Left; Expression colorExpr; int maxLines = 1; // For multi-line support bool ellipsis = true; // Truncate with ... if too long public: explicit Label(const std::string& id) : UIElement(id), colorExpr(Expression::parse("0x00")) {} ElementType getType() const override { return ElementType::Label; } void setText(const std::string& expr) { textExpr = Expression::parse(expr); markDirty(); } void setFont(int fid) { fontId = fid; markDirty(); } void setAlignment(Alignment a) { alignment = a; markDirty(); } void setCentered(bool c) { alignment = c ? Alignment::Center : Alignment::Left; markDirty(); } void setColorExpr(const std::string& c) { colorExpr = Expression::parse(c); markDirty(); } void setMaxLines(int lines) { maxLines = lines; markDirty(); } void setEllipsis(bool e) { ellipsis = e; 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 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(); } }; // --- BitmapElement --- class BitmapElement : public UIElement { Expression srcExpr; bool scaleToFit = true; bool preserveAspect = true; int borderRadius = 0; public: explicit BitmapElement(const std::string& id) : UIElement(id) { cacheable = true; // Bitmaps benefit from caching } ElementType getType() const override { return ElementType::Bitmap; } void setSrc(const std::string& src) { srcExpr = Expression::parse(src); invalidateCache(); } void setScaleToFit(bool scale) { scaleToFit = scale; invalidateCache(); } void setPreserveAspect(bool preserve) { preserveAspect = preserve; invalidateCache(); } void setBorderRadius(int r) { borderRadius = r; // Radius doesn't affect cache key unless we baked it in (we don't currently), // but we should redraw. markDirty(); } void draw(const GfxRenderer& renderer, const ThemeContext& context) override; }; // --- ProgressBar --- class ProgressBar : public UIElement { Expression valueExpr; // Current value (0-100 or 0-max) Expression maxExpr; // Max value (default 100) Expression fgColorExpr; // Foreground color Expression bgColorExpr; // Background color bool showBorder = true; int borderWidth = 1; public: explicit ProgressBar(const std::string& id) : UIElement(id), valueExpr(Expression::parse("0")), maxExpr(Expression::parse("100")), fgColorExpr(Expression::parse("0x00")), // Black fill bgColorExpr(Expression::parse("0xFF")) // White background {} ElementType getType() const override { return ElementType::ProgressBar; } void setValue(const std::string& expr) { valueExpr = Expression::parse(expr); markDirty(); } void setMax(const std::string& expr) { maxExpr = Expression::parse(expr); markDirty(); } void setFgColor(const std::string& expr) { fgColorExpr = Expression::parse(expr); markDirty(); } void setBgColor(const std::string& expr) { bgColorExpr = Expression::parse(expr); markDirty(); } void setShowBorder(bool show) { showBorder = show; markDirty(); } void draw(const GfxRenderer& renderer, const ThemeContext& context) override { if (!isVisible(context)) return; std::string valStr = context.evaluatestring(valueExpr); std::string maxStr = context.evaluatestring(maxExpr); int value = valStr.empty() ? 0 : std::stoi(valStr); int maxVal = maxStr.empty() ? 100 : std::stoi(maxStr); if (maxVal <= 0) maxVal = 100; float ratio = static_cast(value) / static_cast(maxVal); if (ratio < 0) ratio = 0; if (ratio > 1) ratio = 1; // Draw background std::string bgStr = context.evaluatestring(bgColorExpr); uint8_t bgColor = Color::parse(bgStr).value; renderer.fillRect(absX, absY, absW, absH, bgColor == 0x00); // Draw filled portion int fillWidth = static_cast(absW * ratio); if (fillWidth > 0) { std::string fgStr = context.evaluatestring(fgColorExpr); uint8_t fgColor = Color::parse(fgStr).value; renderer.fillRect(absX, absY, fillWidth, absH, fgColor == 0x00); } // Draw border if (showBorder) { renderer.drawRect(absX, absY, absW, absH, true); } markClean(); } }; // --- Divider (horizontal or vertical line) --- class Divider : public UIElement { Expression colorExpr; bool horizontal = true; int thickness = 1; public: explicit Divider(const std::string& id) : UIElement(id), colorExpr(Expression::parse("0x00")) {} ElementType getType() const override { return ElementType::Divider; } void setColorExpr(const std::string& expr) { colorExpr = Expression::parse(expr); markDirty(); } void setHorizontal(bool h) { horizontal = h; markDirty(); } void setThickness(int t) { thickness = t; 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); if (horizontal) { for (int i = 0; i < thickness && i < absH; i++) { renderer.drawLine(absX, absY + i, absX + absW - 1, absY + i, black); } } else { for (int i = 0; i < thickness && i < absW; i++) { renderer.drawLine(absX + i, absY, absX + i, absY + absH - 1, black); } } markClean(); } }; // --- BatteryIcon --- class BatteryIcon : public UIElement { Expression valueExpr; Expression colorExpr; public: explicit BatteryIcon(const std::string& id) : UIElement(id), valueExpr(Expression::parse("0")), colorExpr(Expression::parse("0x00")) { // Black by default } ElementType getType() const override { return ElementType::BatteryIcon; } void setValue(const std::string& expr) { valueExpr = Expression::parse(expr); markDirty(); } void setColor(const std::string& expr) { colorExpr = Expression::parse(expr); markDirty(); } void draw(const GfxRenderer& renderer, const ThemeContext& context) override { if (!isVisible(context)) return; std::string valStr = context.evaluatestring(valueExpr); int percentage = valStr.empty() ? 0 : std::stoi(valStr); std::string colStr = context.evaluatestring(colorExpr); uint8_t color = Color::parse(colStr).value; bool black = (color == 0x00); constexpr int batteryWidth = 15; constexpr int batteryHeight = 12; int x = absX; int y = absY; if (absW > batteryWidth) x += (absW - batteryWidth) / 2; if (absH > batteryHeight) y += (absH - batteryHeight) / 2; renderer.drawLine(x + 1, y, x + batteryWidth - 3, y, black); renderer.drawLine(x + 1, y + batteryHeight - 1, x + batteryWidth - 3, y + batteryHeight - 1, black); renderer.drawLine(x, y + 1, x, y + batteryHeight - 2, black); renderer.drawLine(x + batteryWidth - 2, y + 1, x + batteryWidth - 2, y + batteryHeight - 2, black); renderer.drawPixel(x + batteryWidth - 1, y + 3, black); renderer.drawPixel(x + batteryWidth - 1, y + batteryHeight - 4, black); renderer.drawLine(x + batteryWidth - 0, y + 4, x + batteryWidth - 0, y + batteryHeight - 5, black); if (percentage > 0) { int filledWidth = percentage * (batteryWidth - 5) / 100 + 1; if (filledWidth > batteryWidth - 5) { filledWidth = batteryWidth - 5; } renderer.fillRect(x + 2, y + 2, filledWidth, batteryHeight - 4, black); } markClean(); } }; } // namespace ThemeEngine