refactor: Deduplicate Roundraff battery drawing (#1847)
## Summary Deduplicated battery drawing code for Roundraff theme with other themes. Now `BaseTheme` provides non-virtual `drawBatteryLeft` and `drawBatteryRight` methods, which use a shared static `drawBatteryOutline` implementation, and defer to a virtual `fillBatteryIcon` implementation. In Classic and Roundraff `fillBatteryIcon` uses a solid fill, while Lyra and Lyra Extended use a segmented fill. The charging indicator inside the battery was missing for Roundraff, but is now consistent with other themes because of the shared drawing implementation. --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**PARTIALLY**_
This commit is contained in:
@@ -20,37 +20,6 @@ constexpr int homeMenuMargin = 20;
|
||||
constexpr int homeMarginTop = 30;
|
||||
constexpr int subtitleY = 738;
|
||||
|
||||
// Helper: draw battery icon at given position
|
||||
void drawBatteryIcon(const GfxRenderer& renderer, int x, int y, int battWidth, int rectHeight, uint16_t percentage) {
|
||||
// Draw battery outline (shared code)
|
||||
BaseTheme::drawBatteryOutline(renderer, x, y, battWidth, rectHeight);
|
||||
|
||||
const bool charging = gpio.isUsbConnected();
|
||||
|
||||
// The +1 is to round up, so that we always fill at least one pixel
|
||||
const int maxFillWidth = battWidth - 5;
|
||||
const int fillHeight = rectHeight - 4;
|
||||
if (maxFillWidth <= 0 || fillHeight <= 0) {
|
||||
return;
|
||||
}
|
||||
int filledWidth = percentage * maxFillWidth / 100 + 1;
|
||||
if (filledWidth > maxFillWidth) {
|
||||
filledWidth = maxFillWidth;
|
||||
}
|
||||
|
||||
// When charging, ensure minimum fill so lightning bolt is fully visible
|
||||
constexpr int minFillForBolt = 8;
|
||||
if (charging && filledWidth < minFillForBolt) {
|
||||
filledWidth = std::min(minFillForBolt, maxFillWidth);
|
||||
}
|
||||
|
||||
renderer.fillRect(x + 2, y + 2, filledWidth, fillHeight);
|
||||
|
||||
// Draw lightning bolt when charging (white/inverted on black fill for visibility)
|
||||
if (charging) {
|
||||
BaseTheme::drawBatteryLightningBolt(renderer, x + 4, y + 2);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void BaseTheme::drawBatteryOutline(const GfxRenderer& renderer, int x, int y, int battWidth, int rectHeight) {
|
||||
@@ -79,6 +48,33 @@ void BaseTheme::drawBatteryLightningBolt(const GfxRenderer& renderer, int boltX,
|
||||
renderer.drawLine(boltX + 1, boltY + 7, boltX + 2, boltY + 7, false);
|
||||
}
|
||||
|
||||
void BaseTheme::fillBatteryIcon(const GfxRenderer& renderer, Rect rect, uint16_t percentage) const {
|
||||
const bool charging = gpio.isUsbConnected();
|
||||
|
||||
const int maxFillWidth = rect.width - 5;
|
||||
const int fillHeight = rect.height - 4;
|
||||
if (maxFillWidth <= 0 || fillHeight <= 0) {
|
||||
return;
|
||||
}
|
||||
// +1 to round up so we always fill at least one pixel
|
||||
int filledWidth = percentage * maxFillWidth / 100 + 1;
|
||||
if (filledWidth > maxFillWidth) {
|
||||
filledWidth = maxFillWidth;
|
||||
}
|
||||
|
||||
// When charging, ensure minimum fill so lightning bolt is fully visible
|
||||
constexpr int minFillForBolt = 8;
|
||||
if (charging && filledWidth < minFillForBolt) {
|
||||
filledWidth = std::min(minFillForBolt, maxFillWidth);
|
||||
}
|
||||
|
||||
renderer.fillRect(rect.x + 2, rect.y + 2, filledWidth, fillHeight);
|
||||
|
||||
if (charging) {
|
||||
drawBatteryLightningBolt(renderer, rect.x + 4, rect.y + 2);
|
||||
}
|
||||
}
|
||||
|
||||
void BaseTheme::drawBatteryLeft(const GfxRenderer& renderer, Rect rect, const bool showPercentage) const {
|
||||
// Left aligned: icon on left, percentage on right (reader mode)
|
||||
const uint16_t percentage = powerManager.getBatteryPercentage();
|
||||
@@ -86,11 +82,12 @@ void BaseTheme::drawBatteryLeft(const GfxRenderer& renderer, Rect rect, const bo
|
||||
|
||||
if (showPercentage) {
|
||||
const auto percentageText = std::to_string(percentage) + "%";
|
||||
renderer.drawText(SMALL_FONT_ID, rect.x + BaseTheme::batteryPercentSpacing + BaseMetrics::values.batteryWidth,
|
||||
rect.y, percentageText.c_str());
|
||||
renderer.drawText(SMALL_FONT_ID, rect.x + batteryPercentSpacing + rect.width, rect.y, percentageText.c_str());
|
||||
}
|
||||
|
||||
drawBatteryIcon(renderer, rect.x, y, BaseMetrics::values.batteryWidth, rect.height, percentage);
|
||||
const Rect iconRect{rect.x, y, rect.width, rect.height};
|
||||
drawBatteryOutline(renderer, rect.x, y, rect.width, rect.height);
|
||||
fillBatteryIcon(renderer, iconRect, percentage);
|
||||
}
|
||||
|
||||
void BaseTheme::drawBatteryRight(const GfxRenderer& renderer, Rect rect, const bool showPercentage) const {
|
||||
@@ -102,16 +99,12 @@ void BaseTheme::drawBatteryRight(const GfxRenderer& renderer, Rect rect, const b
|
||||
if (showPercentage) {
|
||||
const auto percentageText = std::to_string(percentage) + "%";
|
||||
const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, percentageText.c_str());
|
||||
// Clear the area where we're going to draw the text to prevent ghosting
|
||||
const auto textHeight = renderer.getTextHeight(SMALL_FONT_ID);
|
||||
renderer.fillRect(rect.x - textWidth - BaseTheme::batteryPercentSpacing, rect.y, textWidth, textHeight, false);
|
||||
// Draw text to the left of the icon
|
||||
renderer.drawText(SMALL_FONT_ID, rect.x - textWidth - BaseTheme::batteryPercentSpacing, rect.y,
|
||||
percentageText.c_str());
|
||||
renderer.drawText(SMALL_FONT_ID, rect.x - textWidth - batteryPercentSpacing, rect.y, percentageText.c_str());
|
||||
}
|
||||
|
||||
// Icon is already at correct position from rect.x
|
||||
drawBatteryIcon(renderer, rect.x, y, BaseMetrics::values.batteryWidth, rect.height, percentage);
|
||||
const Rect iconRect{rect.x, y, rect.width, rect.height};
|
||||
drawBatteryOutline(renderer, rect.x, y, rect.width, rect.height);
|
||||
fillBatteryIcon(renderer, iconRect, percentage);
|
||||
}
|
||||
|
||||
void BaseTheme::drawProgressBar(const GfxRenderer& renderer, Rect rect, const size_t current,
|
||||
|
||||
@@ -125,11 +125,12 @@ class BaseTheme {
|
||||
virtual ~BaseTheme() = default;
|
||||
|
||||
// Component drawing methods
|
||||
virtual void drawProgressBar(const GfxRenderer& renderer, Rect rect, size_t current, size_t total) const;
|
||||
virtual void drawBatteryLeft(const GfxRenderer& renderer, Rect rect,
|
||||
void drawProgressBar(const GfxRenderer& renderer, Rect rect, size_t current, size_t total) const;
|
||||
void drawBatteryLeft(const GfxRenderer& renderer, Rect rect,
|
||||
bool showPercentage = true) const; // Left aligned (reader mode)
|
||||
virtual void drawBatteryRight(const GfxRenderer& renderer, Rect rect,
|
||||
void drawBatteryRight(const GfxRenderer& renderer, Rect rect,
|
||||
bool showPercentage = true) const; // Right aligned (UI headers)
|
||||
virtual void fillBatteryIcon(const GfxRenderer& renderer, Rect rect, uint16_t percentage) const;
|
||||
virtual void drawButtonHints(GfxRenderer& renderer, const char* btn1, const char* btn2, const char* btn3,
|
||||
const char* btn4) const;
|
||||
virtual void drawSideButtonHints(const GfxRenderer& renderer, const char* topBtn, const char* bottomBtn) const;
|
||||
@@ -153,10 +154,9 @@ class BaseTheme {
|
||||
const std::function<UIIcon(int index)>& rowIcon) const;
|
||||
virtual Rect drawPopup(const GfxRenderer& renderer, const char* message) const;
|
||||
virtual void fillPopupProgress(const GfxRenderer& renderer, const Rect& layout, const int progress) const;
|
||||
virtual void drawStatusBar(GfxRenderer& renderer, const float bookProgress, const int currentPage,
|
||||
const int pageCount, std::string title, const int paddingBottom = 0,
|
||||
const int textYOffset = 0) const;
|
||||
virtual void drawHelpText(const GfxRenderer& renderer, Rect rect, const char* label) const;
|
||||
void drawStatusBar(GfxRenderer& renderer, const float bookProgress, const int currentPage, const int pageCount,
|
||||
std::string title, const int paddingBottom = 0, const int textYOffset = 0) const;
|
||||
void drawHelpText(const GfxRenderer& renderer, Rect rect, const char* label) const;
|
||||
virtual void drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth, bool cursorMode = false,
|
||||
int contentStartX = 0, int contentWidth = 0) const;
|
||||
virtual void drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected,
|
||||
|
||||
@@ -41,30 +41,6 @@ constexpr int listIconSize = 24;
|
||||
constexpr int mainMenuColumns = 2;
|
||||
int coverWidth = 0;
|
||||
|
||||
void drawLyraBatteryIcon(const GfxRenderer& renderer, int x, int y, int battWidth, int rectHeight,
|
||||
uint16_t percentage) {
|
||||
BaseTheme::drawBatteryOutline(renderer, x, y, battWidth, rectHeight);
|
||||
|
||||
const bool charging = gpio.isUsbConnected();
|
||||
|
||||
if (charging) {
|
||||
// Draw solid fill when charging so lightning bolt is visible
|
||||
renderer.fillRect(x + 2, y + 2, battWidth - 5, rectHeight - 4);
|
||||
BaseTheme::drawBatteryLightningBolt(renderer, x + 4, y + 2);
|
||||
} else {
|
||||
// Draw bars when not charging
|
||||
if (percentage > 10) {
|
||||
renderer.fillRect(x + 2, y + 2, 3, rectHeight - 4);
|
||||
}
|
||||
if (percentage > 40) {
|
||||
renderer.fillRect(x + 6, y + 2, 3, rectHeight - 4);
|
||||
}
|
||||
if (percentage > 70) {
|
||||
renderer.fillRect(x + 10, y + 2, 3, rectHeight - 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const uint8_t* iconForName(UIIcon icon, int size) {
|
||||
if (size == 24) {
|
||||
switch (icon) {
|
||||
@@ -107,35 +83,24 @@ const uint8_t* iconForName(UIIcon icon, int size) {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void LyraTheme::drawBatteryLeft(const GfxRenderer& renderer, Rect rect, const bool showPercentage) const {
|
||||
// Left aligned: icon on left, percentage on right (reader mode)
|
||||
const uint16_t percentage = powerManager.getBatteryPercentage();
|
||||
void LyraTheme::fillBatteryIcon(const GfxRenderer& renderer, Rect rect, uint16_t percentage) const {
|
||||
const bool charging = gpio.isUsbConnected();
|
||||
|
||||
if (showPercentage) {
|
||||
const auto percentageText = std::to_string(percentage) + "%";
|
||||
renderer.drawText(SMALL_FONT_ID, rect.x + BaseTheme::batteryPercentSpacing + LyraMetrics::values.batteryWidth,
|
||||
rect.y, percentageText.c_str());
|
||||
if (charging) {
|
||||
// Solid fill when charging so lightning bolt is visible
|
||||
renderer.fillRect(rect.x + 2, rect.y + 2, rect.width - 5, rect.height - 4);
|
||||
drawBatteryLightningBolt(renderer, rect.x + 4, rect.y + 2);
|
||||
} else {
|
||||
if (percentage > 10) {
|
||||
renderer.fillRect(rect.x + 2, rect.y + 2, 3, rect.height - 4);
|
||||
}
|
||||
if (percentage > 40) {
|
||||
renderer.fillRect(rect.x + 6, rect.y + 2, 3, rect.height - 4);
|
||||
}
|
||||
if (percentage > 70) {
|
||||
renderer.fillRect(rect.x + 10, rect.y + 2, 3, rect.height - 4);
|
||||
}
|
||||
|
||||
drawLyraBatteryIcon(renderer, rect.x, rect.y + 6, LyraMetrics::values.batteryWidth, rect.height, percentage);
|
||||
}
|
||||
|
||||
void LyraTheme::drawBatteryRight(const GfxRenderer& renderer, Rect rect, const bool showPercentage) const {
|
||||
// Right aligned: percentage on left, icon on right (UI headers)
|
||||
const uint16_t percentage = powerManager.getBatteryPercentage();
|
||||
|
||||
if (showPercentage) {
|
||||
const auto percentageText = std::to_string(percentage) + "%";
|
||||
const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, percentageText.c_str());
|
||||
// Clear the area where we're going to draw the text to prevent ghosting
|
||||
const auto textHeight = renderer.getTextHeight(SMALL_FONT_ID);
|
||||
renderer.fillRect(rect.x - textWidth - BaseTheme::batteryPercentSpacing, rect.y, textWidth, textHeight, false);
|
||||
// Draw text to the left of the icon
|
||||
renderer.drawText(SMALL_FONT_ID, rect.x - textWidth - BaseTheme::batteryPercentSpacing, rect.y,
|
||||
percentageText.c_str());
|
||||
}
|
||||
|
||||
drawLyraBatteryIcon(renderer, rect.x, rect.y + 6, LyraMetrics::values.batteryWidth, rect.height, percentage);
|
||||
}
|
||||
|
||||
void LyraTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* title, const char* subtitle) const {
|
||||
|
||||
@@ -49,9 +49,7 @@ constexpr ThemeMetrics values = {.batteryWidth = 16,
|
||||
class LyraTheme : public BaseTheme {
|
||||
public:
|
||||
// Component drawing methods
|
||||
// void drawProgressBar(const GfxRenderer& renderer, Rect rect, size_t current, size_t total) override;
|
||||
void drawBatteryLeft(const GfxRenderer& renderer, Rect rect, bool showPercentage = true) const override;
|
||||
void drawBatteryRight(const GfxRenderer& renderer, Rect rect, bool showPercentage = true) const override;
|
||||
void fillBatteryIcon(const GfxRenderer& renderer, Rect rect, uint16_t percentage) const override;
|
||||
void drawHeader(const GfxRenderer& renderer, Rect rect, const char* title, const char* subtitle) const override;
|
||||
void drawSubHeader(const GfxRenderer& renderer, Rect rect, const char* label,
|
||||
const char* rightLabel = nullptr) const override;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include "RoundedRaffTheme.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <HalPowerManager.h>
|
||||
#include <HalStorage.h>
|
||||
#include <I18n.h>
|
||||
|
||||
@@ -22,7 +21,6 @@ constexpr int kBottomRadius = 15;
|
||||
constexpr int kRowRadius = 20;
|
||||
constexpr int kInteractiveInsetX = 20;
|
||||
constexpr int kSelectableRowGap = 6;
|
||||
constexpr int batteryPercentSpacing = 4;
|
||||
constexpr int kTitleFontId = UI_12_FONT_ID; // Requested main title size: 12px
|
||||
constexpr int kSubtitleFontId = SMALL_FONT_ID; // Requested subtitle size: 8px
|
||||
constexpr int kGuideFontId = SMALL_FONT_ID; // Closest available to requested 6px
|
||||
@@ -46,42 +44,6 @@ void drawScrollBar(const GfxRenderer& renderer, Rect rect, int itemCount, int pa
|
||||
renderer.fillRect(barX, thumbY, barW, thumbH);
|
||||
}
|
||||
|
||||
void drawBatteryIcon(const GfxRenderer& renderer, int x, int y, int battWidth, int rectHeight, uint16_t percentage) {
|
||||
// Top line
|
||||
renderer.drawLine(x + 1, y, x + battWidth - 3, y);
|
||||
// Bottom line
|
||||
renderer.drawLine(x + 1, y + rectHeight - 1, x + battWidth - 3, y + rectHeight - 1);
|
||||
// Left line
|
||||
renderer.drawLine(x, y + 1, x, y + rectHeight - 2);
|
||||
// Battery end
|
||||
renderer.drawLine(x + battWidth - 2, y + 1, x + battWidth - 2, y + rectHeight - 2);
|
||||
renderer.drawPixel(x + battWidth - 1, y + 3);
|
||||
renderer.drawPixel(x + battWidth - 1, y + rectHeight - 4);
|
||||
renderer.drawLine(x + battWidth - 0, y + 4, x + battWidth - 0, y + rectHeight - 5);
|
||||
|
||||
// The +1 is to round up, so that we always fill at least one pixel.
|
||||
int filledWidth = percentage * (battWidth - 5) / 100 + 1;
|
||||
if (filledWidth > battWidth - 5) {
|
||||
filledWidth = battWidth - 5; // Ensure we don't overflow.
|
||||
}
|
||||
|
||||
renderer.fillRect(x + 2, y + 2, filledWidth, rectHeight - 4);
|
||||
}
|
||||
|
||||
void drawBatteryRightStable(const GfxRenderer& renderer, Rect iconRect, uint16_t percentage, bool showPercentage) {
|
||||
// Match BaseTheme::drawBatteryRight layout, but use a stable percentage value for this render.
|
||||
const int iconY = iconRect.y + 6;
|
||||
|
||||
if (showPercentage) {
|
||||
const auto percentageText = std::to_string(percentage) + "%";
|
||||
const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, percentageText.c_str());
|
||||
renderer.drawText(SMALL_FONT_ID, iconRect.x - textWidth - batteryPercentSpacing, iconRect.y,
|
||||
percentageText.c_str());
|
||||
}
|
||||
|
||||
drawBatteryIcon(renderer, iconRect.x, iconY, RoundedRaffMetrics::values.batteryWidth, iconRect.height, percentage);
|
||||
}
|
||||
|
||||
std::string sanitizeButtonLabel(std::string label) {
|
||||
// Remove common directional prefixes/symbols (e.g. "<< Home", unsupported icon glyphs).
|
||||
while (!label.empty() && !std::isalnum(static_cast<unsigned char>(label[0]))) {
|
||||
@@ -110,28 +72,27 @@ void RoundedRaffTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const
|
||||
|
||||
const bool showBatteryPercentage =
|
||||
SETTINGS.hideBatteryPercentage != CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_ALWAYS;
|
||||
const uint16_t percentage = powerManager.getBatteryPercentage();
|
||||
const int batteryIconX = rect.x + rect.width - sidePadding - RoundedRaffMetrics::values.batteryWidth;
|
||||
|
||||
// Reserve space for the widest possible percentage text to avoid title/battery overlap
|
||||
int batteryGroupLeftX = batteryIconX;
|
||||
if (showBatteryPercentage) {
|
||||
const auto percentageText = std::to_string(percentage) + "%";
|
||||
batteryGroupLeftX -= renderer.getTextWidth(SMALL_FONT_ID, percentageText.c_str()) + batteryPercentSpacing;
|
||||
|
||||
// Clear a fixed-width area for the battery percentage to avoid ghosting when digit count changes (e.g. 100% ->
|
||||
// 99%).
|
||||
// Clear a fixed-width area for the battery percentage to avoid ghosting when digit count changes (e.g. 100% -> 99%)
|
||||
const int maxTextWidth = renderer.getTextWidth(SMALL_FONT_ID, "100%");
|
||||
batteryGroupLeftX -= maxTextWidth + batteryPercentSpacing;
|
||||
|
||||
const int clearW = maxTextWidth + batteryPercentSpacing + RoundedRaffMetrics::values.batteryWidth;
|
||||
const int clearH = std::max(renderer.getTextHeight(SMALL_FONT_ID), RoundedRaffMetrics::values.batteryHeight + 8);
|
||||
renderer.fillRect(batteryIconX - maxTextWidth - batteryPercentSpacing, rect.y + 14, clearW, clearH, false);
|
||||
}
|
||||
|
||||
const int maxTextWidth = std::max(0, batteryGroupLeftX - 20 - titleX);
|
||||
auto headerTitle = renderer.truncatedText(kTitleFontId, title, maxTextWidth, EpdFontFamily::BOLD);
|
||||
const int maxTitleWidth = std::max(0, batteryGroupLeftX - 20 - titleX);
|
||||
auto headerTitle = renderer.truncatedText(kTitleFontId, title, maxTitleWidth, EpdFontFamily::BOLD);
|
||||
renderer.drawText(kTitleFontId, titleX, titleY, headerTitle.c_str(), true, EpdFontFamily::BOLD);
|
||||
drawBatteryRightStable(renderer,
|
||||
drawBatteryRight(renderer,
|
||||
Rect{batteryIconX, rect.y + 14, RoundedRaffMetrics::values.batteryWidth,
|
||||
RoundedRaffMetrics::values.batteryHeight},
|
||||
percentage, showBatteryPercentage);
|
||||
showBatteryPercentage);
|
||||
}
|
||||
|
||||
void RoundedRaffTheme::drawTabBar(const GfxRenderer& renderer, Rect rect, const std::vector<TabInfo>& tabs,
|
||||
|
||||
Reference in New Issue
Block a user