refactor: Deduplicate battery drawing code and fix Lyra charging indicator (#1437)

## Summary

**What is the goal of this PR?**

Following up on #1427:
- Extracted shared battery drawing logic, including lightning bolt, to
reduce duplication.
- In Lyra with segmented battery the lightning bolt was hard to see, so
when charging Lyra now uses a solid battery fill.

---

### 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:
Zach Nelson
2026-04-14 17:05:06 -05:00
committed by GitHub
parent 23aad213fc
commit 1bd7a1de67
3 changed files with 59 additions and 63 deletions
+34 -26
View File
@@ -16,24 +16,14 @@
// Internal constants // Internal constants
namespace { namespace {
constexpr int batteryPercentSpacing = 4;
constexpr int homeMenuMargin = 20; constexpr int homeMenuMargin = 20;
constexpr int homeMarginTop = 30; constexpr int homeMarginTop = 30;
constexpr int subtitleY = 738; constexpr int subtitleY = 738;
// Helper: draw battery icon at given position // Helper: draw battery icon at given position
void drawBatteryIcon(const GfxRenderer& renderer, int x, int y, int battWidth, int rectHeight, uint16_t percentage) { void drawBatteryIcon(const GfxRenderer& renderer, int x, int y, int battWidth, int rectHeight, uint16_t percentage) {
// Top line // Draw battery outline (shared code)
renderer.drawLine(x + 1, y, x + battWidth - 3, y); BaseTheme::drawBatteryOutline(renderer, x, y, battWidth, rectHeight);
// 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);
const bool charging = gpio.isUsbConnected(); const bool charging = gpio.isUsbConnected();
@@ -58,20 +48,37 @@ void drawBatteryIcon(const GfxRenderer& renderer, int x, int y, int battWidth, i
// Draw lightning bolt when charging (white/inverted on black fill for visibility) // Draw lightning bolt when charging (white/inverted on black fill for visibility)
if (charging) { if (charging) {
const int boltX = x + 4; BaseTheme::drawBatteryLightningBolt(renderer, x + 4, y + 2);
const int boltY = y + 2;
renderer.drawLine(boltX + 4, boltY + 0, boltX + 5, boltY + 0, false);
renderer.drawLine(boltX + 3, boltY + 1, boltX + 4, boltY + 1, false);
renderer.drawLine(boltX + 2, boltY + 2, boltX + 5, boltY + 2, false);
renderer.drawLine(boltX + 3, boltY + 3, boltX + 4, boltY + 3, false);
renderer.drawLine(boltX + 2, boltY + 4, boltX + 3, boltY + 4, false);
renderer.drawLine(boltX + 1, boltY + 5, boltX + 4, boltY + 5, false);
renderer.drawLine(boltX + 2, boltY + 6, boltX + 3, boltY + 6, false);
renderer.drawLine(boltX + 1, boltY + 7, boltX + 2, boltY + 7, false);
} }
} }
} // namespace } // namespace
void BaseTheme::drawBatteryOutline(const GfxRenderer& renderer, int x, int y, int battWidth, int rectHeight) {
// 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);
}
void BaseTheme::drawBatteryLightningBolt(const GfxRenderer& renderer, int boltX, int boltY) {
// Draw lightning bolt (white/inverted on black fill for visibility)
renderer.drawLine(boltX + 4, boltY + 0, boltX + 5, boltY + 0, false);
renderer.drawLine(boltX + 3, boltY + 1, boltX + 4, boltY + 1, false);
renderer.drawLine(boltX + 2, boltY + 2, boltX + 5, boltY + 2, false);
renderer.drawLine(boltX + 3, boltY + 3, boltX + 4, boltY + 3, false);
renderer.drawLine(boltX + 2, boltY + 4, boltX + 3, boltY + 4, false);
renderer.drawLine(boltX + 1, boltY + 5, boltX + 4, boltY + 5, false);
renderer.drawLine(boltX + 2, boltY + 6, boltX + 3, boltY + 6, false);
renderer.drawLine(boltX + 1, boltY + 7, boltX + 2, boltY + 7, false);
}
void BaseTheme::drawBatteryLeft(const GfxRenderer& renderer, Rect rect, const bool showPercentage) const { void BaseTheme::drawBatteryLeft(const GfxRenderer& renderer, Rect rect, const bool showPercentage) const {
// Left aligned: icon on left, percentage on right (reader mode) // Left aligned: icon on left, percentage on right (reader mode)
const uint16_t percentage = powerManager.getBatteryPercentage(); const uint16_t percentage = powerManager.getBatteryPercentage();
@@ -79,8 +86,8 @@ void BaseTheme::drawBatteryLeft(const GfxRenderer& renderer, Rect rect, const bo
if (showPercentage) { if (showPercentage) {
const auto percentageText = std::to_string(percentage) + "%"; const auto percentageText = std::to_string(percentage) + "%";
renderer.drawText(SMALL_FONT_ID, rect.x + batteryPercentSpacing + BaseMetrics::values.batteryWidth, rect.y, renderer.drawText(SMALL_FONT_ID, rect.x + BaseTheme::batteryPercentSpacing + BaseMetrics::values.batteryWidth,
percentageText.c_str()); rect.y, percentageText.c_str());
} }
drawBatteryIcon(renderer, rect.x, y, BaseMetrics::values.batteryWidth, rect.height, percentage); drawBatteryIcon(renderer, rect.x, y, BaseMetrics::values.batteryWidth, rect.height, percentage);
@@ -97,9 +104,10 @@ void BaseTheme::drawBatteryRight(const GfxRenderer& renderer, Rect rect, const b
const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, percentageText.c_str()); 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 // Clear the area where we're going to draw the text to prevent ghosting
const auto textHeight = renderer.getTextHeight(SMALL_FONT_ID); const auto textHeight = renderer.getTextHeight(SMALL_FONT_ID);
renderer.fillRect(rect.x - textWidth - batteryPercentSpacing, rect.y, textWidth, textHeight, false); renderer.fillRect(rect.x - textWidth - BaseTheme::batteryPercentSpacing, rect.y, textWidth, textHeight, false);
// Draw text to the left of the icon // Draw text to the left of the icon
renderer.drawText(SMALL_FONT_ID, rect.x - textWidth - batteryPercentSpacing, rect.y, percentageText.c_str()); renderer.drawText(SMALL_FONT_ID, rect.x - textWidth - BaseTheme::batteryPercentSpacing, rect.y,
percentageText.c_str());
} }
// Icon is already at correct position from rect.x // Icon is already at correct position from rect.x
+5
View File
@@ -142,4 +142,9 @@ class BaseTheme {
virtual void drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth) const; virtual void drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth) const;
virtual void drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected) const; virtual void drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected) const;
virtual bool showsFileIcons() const { return false; } virtual bool showsFileIcons() const { return false; }
// Shared constants and helpers for battery drawing (used by all themes)
static constexpr int batteryPercentSpacing = 4;
static void drawBatteryOutline(const GfxRenderer& renderer, int x, int y, int battWidth, int rectHeight);
static void drawBatteryLightningBolt(const GfxRenderer& renderer, int boltX, int boltY);
}; };
+20 -37
View File
@@ -30,7 +30,6 @@
// Internal constants // Internal constants
namespace { namespace {
constexpr int batteryPercentSpacing = 4;
constexpr int hPaddingInSelection = 8; constexpr int hPaddingInSelection = 8;
constexpr int cornerRadius = 6; constexpr int cornerRadius = 6;
constexpr int topHintButtonY = 345; constexpr int topHintButtonY = 345;
@@ -45,42 +44,25 @@ int coverWidth = 0;
void drawLyraBatteryIcon(const GfxRenderer& renderer, int x, int y, int battWidth, int rectHeight, void drawLyraBatteryIcon(const GfxRenderer& renderer, int x, int y, int battWidth, int rectHeight,
uint16_t percentage) { uint16_t percentage) {
// Top line BaseTheme::drawBatteryOutline(renderer, x, y, battWidth, rectHeight);
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);
const bool charging = gpio.isUsbConnected(); const bool charging = gpio.isUsbConnected();
// Draw bars
if (percentage > 10 || charging) {
renderer.fillRect(x + 2, y + 2, 3, rectHeight - 4);
}
if (percentage > 40 || charging) {
renderer.fillRect(x + 6, y + 2, 3, rectHeight - 4);
}
if (percentage > 70) {
renderer.fillRect(x + 10, y + 2, 3, rectHeight - 4);
}
if (charging) { if (charging) {
const int boltX = x + 4; // Draw solid fill when charging so lightning bolt is visible
const int boltY = y + 2; renderer.fillRect(x + 2, y + 2, battWidth - 5, rectHeight - 4);
renderer.drawLine(boltX + 4, boltY + 0, boltX + 5, boltY + 0, false); BaseTheme::drawBatteryLightningBolt(renderer, x + 4, y + 2);
renderer.drawLine(boltX + 3, boltY + 1, boltX + 4, boltY + 1, false); } else {
renderer.drawLine(boltX + 2, boltY + 2, boltX + 5, boltY + 2, false); // Draw bars when not charging
renderer.drawLine(boltX + 3, boltY + 3, boltX + 4, boltY + 3, false); if (percentage > 10) {
renderer.drawLine(boltX + 2, boltY + 4, boltX + 3, boltY + 4, false); renderer.fillRect(x + 2, y + 2, 3, rectHeight - 4);
renderer.drawLine(boltX + 1, boltY + 5, boltX + 4, boltY + 5, false); }
renderer.drawLine(boltX + 2, boltY + 6, boltX + 3, boltY + 6, false); if (percentage > 40) {
renderer.drawLine(boltX + 1, boltY + 7, boltX + 2, boltY + 7, false); renderer.fillRect(x + 6, y + 2, 3, rectHeight - 4);
}
if (percentage > 70) {
renderer.fillRect(x + 10, y + 2, 3, rectHeight - 4);
}
} }
} }
@@ -132,8 +114,8 @@ void LyraTheme::drawBatteryLeft(const GfxRenderer& renderer, Rect rect, const bo
if (showPercentage) { if (showPercentage) {
const auto percentageText = std::to_string(percentage) + "%"; const auto percentageText = std::to_string(percentage) + "%";
renderer.drawText(SMALL_FONT_ID, rect.x + batteryPercentSpacing + LyraMetrics::values.batteryWidth, rect.y, renderer.drawText(SMALL_FONT_ID, rect.x + BaseTheme::batteryPercentSpacing + LyraMetrics::values.batteryWidth,
percentageText.c_str()); rect.y, percentageText.c_str());
} }
drawLyraBatteryIcon(renderer, rect.x, rect.y + 6, LyraMetrics::values.batteryWidth, rect.height, percentage); drawLyraBatteryIcon(renderer, rect.x, rect.y + 6, LyraMetrics::values.batteryWidth, rect.height, percentage);
@@ -148,9 +130,10 @@ void LyraTheme::drawBatteryRight(const GfxRenderer& renderer, Rect rect, const b
const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, percentageText.c_str()); 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 // Clear the area where we're going to draw the text to prevent ghosting
const auto textHeight = renderer.getTextHeight(SMALL_FONT_ID); const auto textHeight = renderer.getTextHeight(SMALL_FONT_ID);
renderer.fillRect(rect.x - textWidth - batteryPercentSpacing, rect.y, textWidth, textHeight, false); renderer.fillRect(rect.x - textWidth - BaseTheme::batteryPercentSpacing, rect.y, textWidth, textHeight, false);
// Draw text to the left of the icon // Draw text to the left of the icon
renderer.drawText(SMALL_FONT_ID, rect.x - textWidth - batteryPercentSpacing, rect.y, percentageText.c_str()); 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); drawLyraBatteryIcon(renderer, rect.x, rect.y + 6, LyraMetrics::values.batteryWidth, rect.height, percentage);