feat: UI themes, Lyra (#528)
## Summary
### What is the goal of this PR?
- Visual UI overhaul
- UI theme selection
### What changes are included?
- Added a setting "UI Theme": Classic, Lyra
- The classic theme is the current Crosspoint theme
- The Lyra theme implements these mockups:
https://www.figma.com/design/UhxoV4DgUnfrDQgMPPTXog/Lyra-Theme?node-id=2003-7596&t=4CSOZqf0n9uQMxDt-0
by Discord users yagofarias, ruby and gan_shu
- New functions in GFXRenderer to render rounded rectangles, greyscale
fills (using dithering) and thick lines
- Basic UI components are factored into BaseTheme methods which can be
overridden by each additional theme. Methods that are not overridden
will fallback to BaseTheme behavior. This means any new
features/components in CrossPoint only need to be developed for the
"Classic" BaseTheme.
- Additional themes can easily be developed by the community using this
foundation



## Additional Context
- Only the Home, Library and main Settings screens have been implemented
so far, this will be extended to the transfer screens and chapter
selection screen later on, but we need to get the ball rolling somehow
:)
- Loading extra covers on the home screen in the Lyra theme takes a
little more time (about 2 seconds), I added a loading bar popup (reusing
the Indexing progress bar from the reader view, factored into a neat UI
component) but the popup adds ~400ms to the loading time.
- ~~Home screen thumbnails will need to be generated separately for each
theme, because they are displayed in different sizes. Because we're
using dithering, displaying a thumb with the wrong size causes the
picture to look janky or dark as it does on the screenshots above. No
worries this will be fixed in a future PR.~~ Thumbs are now generated
with a size parameter
- UI Icons will need to be implemented in a future PR.
---
### 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 is not a vibe coded PR. Copilot was used for autocompletion to save
time but I reviewed, understood and edited all generated code.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-05 17:50:11 +07:00
|
|
|
#include "BaseTheme.h"
|
|
|
|
|
|
|
|
|
|
#include <GfxRenderer.h>
|
2026-02-08 21:29:14 +01:00
|
|
|
#include <HalStorage.h>
|
2026-02-13 12:16:39 +01:00
|
|
|
#include <Logging.h>
|
feat: UI themes, Lyra (#528)
## Summary
### What is the goal of this PR?
- Visual UI overhaul
- UI theme selection
### What changes are included?
- Added a setting "UI Theme": Classic, Lyra
- The classic theme is the current Crosspoint theme
- The Lyra theme implements these mockups:
https://www.figma.com/design/UhxoV4DgUnfrDQgMPPTXog/Lyra-Theme?node-id=2003-7596&t=4CSOZqf0n9uQMxDt-0
by Discord users yagofarias, ruby and gan_shu
- New functions in GFXRenderer to render rounded rectangles, greyscale
fills (using dithering) and thick lines
- Basic UI components are factored into BaseTheme methods which can be
overridden by each additional theme. Methods that are not overridden
will fallback to BaseTheme behavior. This means any new
features/components in CrossPoint only need to be developed for the
"Classic" BaseTheme.
- Additional themes can easily be developed by the community using this
foundation



## Additional Context
- Only the Home, Library and main Settings screens have been implemented
so far, this will be extended to the transfer screens and chapter
selection screen later on, but we need to get the ball rolling somehow
:)
- Loading extra covers on the home screen in the Lyra theme takes a
little more time (about 2 seconds), I added a loading bar popup (reusing
the Indexing progress bar from the reader view, factored into a neat UI
component) but the popup adds ~400ms to the loading time.
- ~~Home screen thumbnails will need to be generated separately for each
theme, because they are displayed in different sizes. Because we're
using dithering, displaying a thumb with the wrong size causes the
picture to look janky or dark as it does on the screenshots above. No
worries this will be fixed in a future PR.~~ Thumbs are now generated
with a size parameter
- UI Icons will need to be implemented in a future PR.
---
### 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 is not a vibe coded PR. Copilot was used for autocompletion to save
time but I reviewed, understood and edited all generated code.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-05 17:50:11 +07:00
|
|
|
#include <Utf8.h>
|
|
|
|
|
|
|
|
|
|
#include <cstdint>
|
|
|
|
|
#include <string>
|
|
|
|
|
|
|
|
|
|
#include "Battery.h"
|
feat: User-Interface I18n System (#728)
**What is the goal of this PR?**
This PR introduces Internationalization (i18n) support, enabling users
to switch the UI language dynamically.
**What changes are included?**
- Core Logic: Added I18n class (`lib/I18n/I18n.h/cpp`) to manage
language state and string retrieval.
- Data Structures:
- `lib/I18n/I18nStrings.h/cpp`: Static string arrays for each supported
language.
- `lib/I18n/I18nKeys.h`: Enum definitions for type-safe string access.
- `lib/I18n/translations.csv`: single source of truth.
- Documentation: Added `docs/i18n.md` detailing the workflow for
developers and translators.
- New Settings activity:
`src/activities/settings/LanguageSelectActivity.h/cpp`
This implementation (building on concepts from #505) prioritizes
performance and memory efficiency.
The core approach is to store all localized strings for each language in
dedicated arrays and access them via enums. This provides O(1) access
with zero runtime overhead, and avoids the heap allocations, hashing,
and collision handling required by `std::map` or `std::unordered_map`.
The main trade-off is that enums and string arrays must remain perfectly
synchronized—any mismatch would result in incorrect strings being
displayed in the UI.
To eliminate this risk, I added a Python script that automatically
generates `I18nStrings.h/.cpp` and `I18nKeys.h` from a CSV file, which
will serve as the single source of truth for all translations. The full
design and workflow are documented in `docs/i18n.md`.
- [x] Python script `generate_i18n.py` to auto-generate C++ files from
CSV
- [x] Populate translations.csv with initial translations.
Currently available translations: English, Español, Français, Deutsch,
Čeština, Português (Brasil), Русский, Svenska.
Thanks, community!
**Status:** EDIT: ready to be merged.
As a proof of concept, the SPANISH strings currently mirror the English
ones, but are fully uppercased.
---
Did you use AI tools to help write this code? _**< PARTIALLY >**_
I used AI for the black work of replacing strings with I18n references
across the project, and for generating the documentation. EDIT: also
some help with merging changes from master.
---------
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Co-authored-by: yeyeto2788 <juanernestobiondi@gmail.com>
2026-02-16 15:28:42 +02:00
|
|
|
#include "I18n.h"
|
feat: UI themes, Lyra (#528)
## Summary
### What is the goal of this PR?
- Visual UI overhaul
- UI theme selection
### What changes are included?
- Added a setting "UI Theme": Classic, Lyra
- The classic theme is the current Crosspoint theme
- The Lyra theme implements these mockups:
https://www.figma.com/design/UhxoV4DgUnfrDQgMPPTXog/Lyra-Theme?node-id=2003-7596&t=4CSOZqf0n9uQMxDt-0
by Discord users yagofarias, ruby and gan_shu
- New functions in GFXRenderer to render rounded rectangles, greyscale
fills (using dithering) and thick lines
- Basic UI components are factored into BaseTheme methods which can be
overridden by each additional theme. Methods that are not overridden
will fallback to BaseTheme behavior. This means any new
features/components in CrossPoint only need to be developed for the
"Classic" BaseTheme.
- Additional themes can easily be developed by the community using this
foundation



## Additional Context
- Only the Home, Library and main Settings screens have been implemented
so far, this will be extended to the transfer screens and chapter
selection screen later on, but we need to get the ball rolling somehow
:)
- Loading extra covers on the home screen in the Lyra theme takes a
little more time (about 2 seconds), I added a loading bar popup (reusing
the Indexing progress bar from the reader view, factored into a neat UI
component) but the popup adds ~400ms to the loading time.
- ~~Home screen thumbnails will need to be generated separately for each
theme, because they are displayed in different sizes. Because we're
using dithering, displaying a thumb with the wrong size causes the
picture to look janky or dark as it does on the screenshots above. No
worries this will be fixed in a future PR.~~ Thumbs are now generated
with a size parameter
- UI Icons will need to be implemented in a future PR.
---
### 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 is not a vibe coded PR. Copilot was used for autocompletion to save
time but I reviewed, understood and edited all generated code.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-05 17:50:11 +07:00
|
|
|
#include "RecentBooksStore.h"
|
|
|
|
|
#include "components/UITheme.h"
|
|
|
|
|
#include "fontIds.h"
|
|
|
|
|
|
|
|
|
|
// Internal constants
|
|
|
|
|
namespace {
|
|
|
|
|
constexpr int batteryPercentSpacing = 4;
|
|
|
|
|
constexpr int homeMenuMargin = 20;
|
|
|
|
|
constexpr int homeMarginTop = 30;
|
2026-02-16 14:36:36 +01:00
|
|
|
|
|
|
|
|
// Helper: draw battery icon at given position
|
|
|
|
|
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);
|
|
|
|
|
}
|
feat: UI themes, Lyra (#528)
## Summary
### What is the goal of this PR?
- Visual UI overhaul
- UI theme selection
### What changes are included?
- Added a setting "UI Theme": Classic, Lyra
- The classic theme is the current Crosspoint theme
- The Lyra theme implements these mockups:
https://www.figma.com/design/UhxoV4DgUnfrDQgMPPTXog/Lyra-Theme?node-id=2003-7596&t=4CSOZqf0n9uQMxDt-0
by Discord users yagofarias, ruby and gan_shu
- New functions in GFXRenderer to render rounded rectangles, greyscale
fills (using dithering) and thick lines
- Basic UI components are factored into BaseTheme methods which can be
overridden by each additional theme. Methods that are not overridden
will fallback to BaseTheme behavior. This means any new
features/components in CrossPoint only need to be developed for the
"Classic" BaseTheme.
- Additional themes can easily be developed by the community using this
foundation



## Additional Context
- Only the Home, Library and main Settings screens have been implemented
so far, this will be extended to the transfer screens and chapter
selection screen later on, but we need to get the ball rolling somehow
:)
- Loading extra covers on the home screen in the Lyra theme takes a
little more time (about 2 seconds), I added a loading bar popup (reusing
the Indexing progress bar from the reader view, factored into a neat UI
component) but the popup adds ~400ms to the loading time.
- ~~Home screen thumbnails will need to be generated separately for each
theme, because they are displayed in different sizes. Because we're
using dithering, displaying a thumb with the wrong size causes the
picture to look janky or dark as it does on the screenshots above. No
worries this will be fixed in a future PR.~~ Thumbs are now generated
with a size parameter
- UI Icons will need to be implemented in a future PR.
---
### 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 is not a vibe coded PR. Copilot was used for autocompletion to save
time but I reviewed, understood and edited all generated code.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-05 17:50:11 +07:00
|
|
|
} // namespace
|
|
|
|
|
|
2026-02-16 14:36:36 +01:00
|
|
|
void BaseTheme::drawBatteryLeft(const GfxRenderer& renderer, Rect rect, const bool showPercentage) const {
|
|
|
|
|
// Left aligned: icon on left, percentage on right (reader mode)
|
feat: UI themes, Lyra (#528)
## Summary
### What is the goal of this PR?
- Visual UI overhaul
- UI theme selection
### What changes are included?
- Added a setting "UI Theme": Classic, Lyra
- The classic theme is the current Crosspoint theme
- The Lyra theme implements these mockups:
https://www.figma.com/design/UhxoV4DgUnfrDQgMPPTXog/Lyra-Theme?node-id=2003-7596&t=4CSOZqf0n9uQMxDt-0
by Discord users yagofarias, ruby and gan_shu
- New functions in GFXRenderer to render rounded rectangles, greyscale
fills (using dithering) and thick lines
- Basic UI components are factored into BaseTheme methods which can be
overridden by each additional theme. Methods that are not overridden
will fallback to BaseTheme behavior. This means any new
features/components in CrossPoint only need to be developed for the
"Classic" BaseTheme.
- Additional themes can easily be developed by the community using this
foundation



## Additional Context
- Only the Home, Library and main Settings screens have been implemented
so far, this will be extended to the transfer screens and chapter
selection screen later on, but we need to get the ball rolling somehow
:)
- Loading extra covers on the home screen in the Lyra theme takes a
little more time (about 2 seconds), I added a loading bar popup (reusing
the Indexing progress bar from the reader view, factored into a neat UI
component) but the popup adds ~400ms to the loading time.
- ~~Home screen thumbnails will need to be generated separately for each
theme, because they are displayed in different sizes. Because we're
using dithering, displaying a thumb with the wrong size causes the
picture to look janky or dark as it does on the screenshots above. No
worries this will be fixed in a future PR.~~ Thumbs are now generated
with a size parameter
- UI Icons will need to be implemented in a future PR.
---
### 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 is not a vibe coded PR. Copilot was used for autocompletion to save
time but I reviewed, understood and edited all generated code.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-05 17:50:11 +07:00
|
|
|
const uint16_t percentage = battery.readPercentage();
|
2026-02-16 14:36:36 +01:00
|
|
|
const int y = rect.y + 6;
|
|
|
|
|
|
feat: UI themes, Lyra (#528)
## Summary
### What is the goal of this PR?
- Visual UI overhaul
- UI theme selection
### What changes are included?
- Added a setting "UI Theme": Classic, Lyra
- The classic theme is the current Crosspoint theme
- The Lyra theme implements these mockups:
https://www.figma.com/design/UhxoV4DgUnfrDQgMPPTXog/Lyra-Theme?node-id=2003-7596&t=4CSOZqf0n9uQMxDt-0
by Discord users yagofarias, ruby and gan_shu
- New functions in GFXRenderer to render rounded rectangles, greyscale
fills (using dithering) and thick lines
- Basic UI components are factored into BaseTheme methods which can be
overridden by each additional theme. Methods that are not overridden
will fallback to BaseTheme behavior. This means any new
features/components in CrossPoint only need to be developed for the
"Classic" BaseTheme.
- Additional themes can easily be developed by the community using this
foundation



## Additional Context
- Only the Home, Library and main Settings screens have been implemented
so far, this will be extended to the transfer screens and chapter
selection screen later on, but we need to get the ball rolling somehow
:)
- Loading extra covers on the home screen in the Lyra theme takes a
little more time (about 2 seconds), I added a loading bar popup (reusing
the Indexing progress bar from the reader view, factored into a neat UI
component) but the popup adds ~400ms to the loading time.
- ~~Home screen thumbnails will need to be generated separately for each
theme, because they are displayed in different sizes. Because we're
using dithering, displaying a thumb with the wrong size causes the
picture to look janky or dark as it does on the screenshots above. No
worries this will be fixed in a future PR.~~ Thumbs are now generated
with a size parameter
- UI Icons will need to be implemented in a future PR.
---
### 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 is not a vibe coded PR. Copilot was used for autocompletion to save
time but I reviewed, understood and edited all generated code.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-05 17:50:11 +07:00
|
|
|
if (showPercentage) {
|
|
|
|
|
const auto percentageText = std::to_string(percentage) + "%";
|
|
|
|
|
renderer.drawText(SMALL_FONT_ID, rect.x + batteryPercentSpacing + BaseMetrics::values.batteryWidth, rect.y,
|
|
|
|
|
percentageText.c_str());
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-16 14:36:36 +01:00
|
|
|
drawBatteryIcon(renderer, rect.x, y, BaseMetrics::values.batteryWidth, rect.height, percentage);
|
|
|
|
|
}
|
feat: UI themes, Lyra (#528)
## Summary
### What is the goal of this PR?
- Visual UI overhaul
- UI theme selection
### What changes are included?
- Added a setting "UI Theme": Classic, Lyra
- The classic theme is the current Crosspoint theme
- The Lyra theme implements these mockups:
https://www.figma.com/design/UhxoV4DgUnfrDQgMPPTXog/Lyra-Theme?node-id=2003-7596&t=4CSOZqf0n9uQMxDt-0
by Discord users yagofarias, ruby and gan_shu
- New functions in GFXRenderer to render rounded rectangles, greyscale
fills (using dithering) and thick lines
- Basic UI components are factored into BaseTheme methods which can be
overridden by each additional theme. Methods that are not overridden
will fallback to BaseTheme behavior. This means any new
features/components in CrossPoint only need to be developed for the
"Classic" BaseTheme.
- Additional themes can easily be developed by the community using this
foundation



## Additional Context
- Only the Home, Library and main Settings screens have been implemented
so far, this will be extended to the transfer screens and chapter
selection screen later on, but we need to get the ball rolling somehow
:)
- Loading extra covers on the home screen in the Lyra theme takes a
little more time (about 2 seconds), I added a loading bar popup (reusing
the Indexing progress bar from the reader view, factored into a neat UI
component) but the popup adds ~400ms to the loading time.
- ~~Home screen thumbnails will need to be generated separately for each
theme, because they are displayed in different sizes. Because we're
using dithering, displaying a thumb with the wrong size causes the
picture to look janky or dark as it does on the screenshots above. No
worries this will be fixed in a future PR.~~ Thumbs are now generated
with a size parameter
- UI Icons will need to be implemented in a future PR.
---
### 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 is not a vibe coded PR. Copilot was used for autocompletion to save
time but I reviewed, understood and edited all generated code.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-05 17:50:11 +07:00
|
|
|
|
2026-02-16 14:36:36 +01:00
|
|
|
void BaseTheme::drawBatteryRight(const GfxRenderer& renderer, Rect rect, const bool showPercentage) const {
|
|
|
|
|
// Right aligned: percentage on left, icon on right (UI headers)
|
|
|
|
|
// rect.x is already positioned for the icon (drawHeader calculated it)
|
|
|
|
|
const uint16_t percentage = battery.readPercentage();
|
|
|
|
|
const int y = rect.y + 6;
|
|
|
|
|
|
|
|
|
|
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 - batteryPercentSpacing, rect.y, textWidth, textHeight, false);
|
|
|
|
|
// Draw text to the left of the icon
|
|
|
|
|
renderer.drawText(SMALL_FONT_ID, rect.x - textWidth - batteryPercentSpacing, rect.y, percentageText.c_str());
|
feat: UI themes, Lyra (#528)
## Summary
### What is the goal of this PR?
- Visual UI overhaul
- UI theme selection
### What changes are included?
- Added a setting "UI Theme": Classic, Lyra
- The classic theme is the current Crosspoint theme
- The Lyra theme implements these mockups:
https://www.figma.com/design/UhxoV4DgUnfrDQgMPPTXog/Lyra-Theme?node-id=2003-7596&t=4CSOZqf0n9uQMxDt-0
by Discord users yagofarias, ruby and gan_shu
- New functions in GFXRenderer to render rounded rectangles, greyscale
fills (using dithering) and thick lines
- Basic UI components are factored into BaseTheme methods which can be
overridden by each additional theme. Methods that are not overridden
will fallback to BaseTheme behavior. This means any new
features/components in CrossPoint only need to be developed for the
"Classic" BaseTheme.
- Additional themes can easily be developed by the community using this
foundation



## Additional Context
- Only the Home, Library and main Settings screens have been implemented
so far, this will be extended to the transfer screens and chapter
selection screen later on, but we need to get the ball rolling somehow
:)
- Loading extra covers on the home screen in the Lyra theme takes a
little more time (about 2 seconds), I added a loading bar popup (reusing
the Indexing progress bar from the reader view, factored into a neat UI
component) but the popup adds ~400ms to the loading time.
- ~~Home screen thumbnails will need to be generated separately for each
theme, because they are displayed in different sizes. Because we're
using dithering, displaying a thumb with the wrong size causes the
picture to look janky or dark as it does on the screenshots above. No
worries this will be fixed in a future PR.~~ Thumbs are now generated
with a size parameter
- UI Icons will need to be implemented in a future PR.
---
### 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 is not a vibe coded PR. Copilot was used for autocompletion to save
time but I reviewed, understood and edited all generated code.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-05 17:50:11 +07:00
|
|
|
}
|
|
|
|
|
|
2026-02-16 14:36:36 +01:00
|
|
|
// Icon is already at correct position from rect.x
|
|
|
|
|
drawBatteryIcon(renderer, rect.x, y, BaseMetrics::values.batteryWidth, rect.height, percentage);
|
feat: UI themes, Lyra (#528)
## Summary
### What is the goal of this PR?
- Visual UI overhaul
- UI theme selection
### What changes are included?
- Added a setting "UI Theme": Classic, Lyra
- The classic theme is the current Crosspoint theme
- The Lyra theme implements these mockups:
https://www.figma.com/design/UhxoV4DgUnfrDQgMPPTXog/Lyra-Theme?node-id=2003-7596&t=4CSOZqf0n9uQMxDt-0
by Discord users yagofarias, ruby and gan_shu
- New functions in GFXRenderer to render rounded rectangles, greyscale
fills (using dithering) and thick lines
- Basic UI components are factored into BaseTheme methods which can be
overridden by each additional theme. Methods that are not overridden
will fallback to BaseTheme behavior. This means any new
features/components in CrossPoint only need to be developed for the
"Classic" BaseTheme.
- Additional themes can easily be developed by the community using this
foundation



## Additional Context
- Only the Home, Library and main Settings screens have been implemented
so far, this will be extended to the transfer screens and chapter
selection screen later on, but we need to get the ball rolling somehow
:)
- Loading extra covers on the home screen in the Lyra theme takes a
little more time (about 2 seconds), I added a loading bar popup (reusing
the Indexing progress bar from the reader view, factored into a neat UI
component) but the popup adds ~400ms to the loading time.
- ~~Home screen thumbnails will need to be generated separately for each
theme, because they are displayed in different sizes. Because we're
using dithering, displaying a thumb with the wrong size causes the
picture to look janky or dark as it does on the screenshots above. No
worries this will be fixed in a future PR.~~ Thumbs are now generated
with a size parameter
- UI Icons will need to be implemented in a future PR.
---
### 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 is not a vibe coded PR. Copilot was used for autocompletion to save
time but I reviewed, understood and edited all generated code.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-05 17:50:11 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void BaseTheme::drawProgressBar(const GfxRenderer& renderer, Rect rect, const size_t current,
|
|
|
|
|
const size_t total) const {
|
|
|
|
|
if (total == 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Use 64-bit arithmetic to avoid overflow for large files
|
|
|
|
|
const int percent = static_cast<int>((static_cast<uint64_t>(current) * 100) / total);
|
|
|
|
|
|
|
|
|
|
// Draw outline
|
|
|
|
|
renderer.drawRect(rect.x, rect.y, rect.width, rect.height);
|
|
|
|
|
|
|
|
|
|
// Draw filled portion
|
|
|
|
|
const int fillWidth = (rect.width - 4) * percent / 100;
|
|
|
|
|
if (fillWidth > 0) {
|
|
|
|
|
renderer.fillRect(rect.x + 2, rect.y + 2, fillWidth, rect.height - 4);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Draw percentage text centered below bar
|
|
|
|
|
const std::string percentText = std::to_string(percent) + "%";
|
|
|
|
|
renderer.drawCenteredText(UI_10_FONT_ID, rect.y + rect.height + 15, percentText.c_str());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void BaseTheme::drawButtonHints(GfxRenderer& renderer, const char* btn1, const char* btn2, const char* btn3,
|
|
|
|
|
const char* btn4) const {
|
|
|
|
|
const GfxRenderer::Orientation orig_orientation = renderer.getOrientation();
|
|
|
|
|
renderer.setOrientation(GfxRenderer::Orientation::Portrait);
|
|
|
|
|
|
|
|
|
|
const int pageHeight = renderer.getScreenHeight();
|
|
|
|
|
constexpr int buttonWidth = 106;
|
|
|
|
|
constexpr int buttonHeight = BaseMetrics::values.buttonHintsHeight;
|
|
|
|
|
constexpr int buttonY = BaseMetrics::values.buttonHintsHeight; // Distance from bottom
|
|
|
|
|
constexpr int textYOffset = 7; // Distance from top of button to text baseline
|
|
|
|
|
constexpr int buttonPositions[] = {25, 130, 245, 350};
|
|
|
|
|
const char* labels[] = {btn1, btn2, btn3, btn4};
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < 4; i++) {
|
|
|
|
|
// Only draw if the label is non-empty
|
|
|
|
|
if (labels[i] != nullptr && labels[i][0] != '\0') {
|
|
|
|
|
const int x = buttonPositions[i];
|
|
|
|
|
renderer.fillRect(x, pageHeight - buttonY, buttonWidth, buttonHeight, false);
|
|
|
|
|
renderer.drawRect(x, pageHeight - buttonY, buttonWidth, buttonHeight);
|
|
|
|
|
const int textWidth = renderer.getTextWidth(UI_10_FONT_ID, labels[i]);
|
|
|
|
|
const int textX = x + (buttonWidth - 1 - textWidth) / 2;
|
|
|
|
|
renderer.drawText(UI_10_FONT_ID, textX, pageHeight - buttonY + textYOffset, labels[i]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderer.setOrientation(orig_orientation);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void BaseTheme::drawSideButtonHints(const GfxRenderer& renderer, const char* topBtn, const char* bottomBtn) const {
|
|
|
|
|
const int screenWidth = renderer.getScreenWidth();
|
|
|
|
|
constexpr int buttonWidth = BaseMetrics::values.sideButtonHintsWidth; // Width on screen (height when rotated)
|
|
|
|
|
constexpr int buttonHeight = 80; // Height on screen (width when rotated)
|
|
|
|
|
constexpr int buttonX = 4; // Distance from right edge
|
|
|
|
|
// Position for the button group - buttons share a border so they're adjacent
|
|
|
|
|
constexpr int topButtonY = 345; // Top button position
|
|
|
|
|
|
|
|
|
|
const char* labels[] = {topBtn, bottomBtn};
|
|
|
|
|
|
|
|
|
|
// Draw the shared border for both buttons as one unit
|
|
|
|
|
const int x = screenWidth - buttonX - buttonWidth;
|
|
|
|
|
|
|
|
|
|
// Draw top button outline (3 sides, bottom open)
|
|
|
|
|
if (topBtn != nullptr && topBtn[0] != '\0') {
|
|
|
|
|
renderer.drawLine(x, topButtonY, x + buttonWidth - 1, topButtonY); // Top
|
|
|
|
|
renderer.drawLine(x, topButtonY, x, topButtonY + buttonHeight - 1); // Left
|
|
|
|
|
renderer.drawLine(x + buttonWidth - 1, topButtonY, x + buttonWidth - 1, topButtonY + buttonHeight - 1); // Right
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Draw shared middle border
|
|
|
|
|
if ((topBtn != nullptr && topBtn[0] != '\0') || (bottomBtn != nullptr && bottomBtn[0] != '\0')) {
|
|
|
|
|
renderer.drawLine(x, topButtonY + buttonHeight, x + buttonWidth - 1, topButtonY + buttonHeight); // Shared border
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Draw bottom button outline (3 sides, top is shared)
|
|
|
|
|
if (bottomBtn != nullptr && bottomBtn[0] != '\0') {
|
|
|
|
|
renderer.drawLine(x, topButtonY + buttonHeight, x, topButtonY + 2 * buttonHeight - 1); // Left
|
|
|
|
|
renderer.drawLine(x + buttonWidth - 1, topButtonY + buttonHeight, x + buttonWidth - 1,
|
|
|
|
|
topButtonY + 2 * buttonHeight - 1); // Right
|
|
|
|
|
renderer.drawLine(x, topButtonY + 2 * buttonHeight - 1, x + buttonWidth - 1,
|
|
|
|
|
topButtonY + 2 * buttonHeight - 1); // Bottom
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Draw text for each button
|
|
|
|
|
for (int i = 0; i < 2; i++) {
|
|
|
|
|
if (labels[i] != nullptr && labels[i][0] != '\0') {
|
|
|
|
|
const int y = topButtonY + i * buttonHeight;
|
|
|
|
|
|
|
|
|
|
// Draw rotated text centered in the button
|
|
|
|
|
const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, labels[i]);
|
|
|
|
|
const int textHeight = renderer.getTextHeight(SMALL_FONT_ID);
|
|
|
|
|
|
|
|
|
|
// Center the rotated text in the button
|
|
|
|
|
const int textX = x + (buttonWidth - textHeight) / 2;
|
|
|
|
|
const int textY = y + (buttonHeight + textWidth) / 2;
|
|
|
|
|
|
|
|
|
|
renderer.drawTextRotated90CW(SMALL_FONT_ID, textX, textY, labels[i]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex,
|
|
|
|
|
const std::function<std::string(int index)>& rowTitle,
|
|
|
|
|
const std::function<std::string(int index)>& rowSubtitle,
|
|
|
|
|
const std::function<std::string(int index)>& rowIcon,
|
|
|
|
|
const std::function<std::string(int index)>& rowValue) const {
|
|
|
|
|
int rowHeight =
|
|
|
|
|
(rowSubtitle != nullptr) ? BaseMetrics::values.listWithSubtitleRowHeight : BaseMetrics::values.listRowHeight;
|
|
|
|
|
int pageItems = rect.height / rowHeight;
|
|
|
|
|
|
|
|
|
|
const int totalPages = (itemCount + pageItems - 1) / pageItems;
|
|
|
|
|
if (totalPages > 1) {
|
|
|
|
|
constexpr int indicatorWidth = 20;
|
|
|
|
|
constexpr int arrowSize = 6;
|
|
|
|
|
constexpr int margin = 15; // Offset from right edge
|
|
|
|
|
|
|
|
|
|
const int centerX = rect.x + rect.width - indicatorWidth / 2 - margin;
|
|
|
|
|
const int indicatorTop = rect.y; // Offset to avoid overlapping side button hints
|
2026-02-09 00:58:46 +07:00
|
|
|
const int indicatorBottom = rect.y + rect.height - arrowSize;
|
feat: UI themes, Lyra (#528)
## Summary
### What is the goal of this PR?
- Visual UI overhaul
- UI theme selection
### What changes are included?
- Added a setting "UI Theme": Classic, Lyra
- The classic theme is the current Crosspoint theme
- The Lyra theme implements these mockups:
https://www.figma.com/design/UhxoV4DgUnfrDQgMPPTXog/Lyra-Theme?node-id=2003-7596&t=4CSOZqf0n9uQMxDt-0
by Discord users yagofarias, ruby and gan_shu
- New functions in GFXRenderer to render rounded rectangles, greyscale
fills (using dithering) and thick lines
- Basic UI components are factored into BaseTheme methods which can be
overridden by each additional theme. Methods that are not overridden
will fallback to BaseTheme behavior. This means any new
features/components in CrossPoint only need to be developed for the
"Classic" BaseTheme.
- Additional themes can easily be developed by the community using this
foundation



## Additional Context
- Only the Home, Library and main Settings screens have been implemented
so far, this will be extended to the transfer screens and chapter
selection screen later on, but we need to get the ball rolling somehow
:)
- Loading extra covers on the home screen in the Lyra theme takes a
little more time (about 2 seconds), I added a loading bar popup (reusing
the Indexing progress bar from the reader view, factored into a neat UI
component) but the popup adds ~400ms to the loading time.
- ~~Home screen thumbnails will need to be generated separately for each
theme, because they are displayed in different sizes. Because we're
using dithering, displaying a thumb with the wrong size causes the
picture to look janky or dark as it does on the screenshots above. No
worries this will be fixed in a future PR.~~ Thumbs are now generated
with a size parameter
- UI Icons will need to be implemented in a future PR.
---
### 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 is not a vibe coded PR. Copilot was used for autocompletion to save
time but I reviewed, understood and edited all generated code.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-05 17:50:11 +07:00
|
|
|
|
|
|
|
|
// Draw up arrow at top (^) - narrow point at top, wide base at bottom
|
|
|
|
|
for (int i = 0; i < arrowSize; ++i) {
|
|
|
|
|
const int lineWidth = 1 + i * 2;
|
|
|
|
|
const int startX = centerX - i;
|
|
|
|
|
renderer.drawLine(startX, indicatorTop + i, startX + lineWidth - 1, indicatorTop + i);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Draw down arrow at bottom (v) - wide base at top, narrow point at bottom
|
|
|
|
|
for (int i = 0; i < arrowSize; ++i) {
|
|
|
|
|
const int lineWidth = 1 + (arrowSize - 1 - i) * 2;
|
|
|
|
|
const int startX = centerX - (arrowSize - 1 - i);
|
|
|
|
|
renderer.drawLine(startX, indicatorBottom - arrowSize + 1 + i, startX + lineWidth - 1,
|
|
|
|
|
indicatorBottom - arrowSize + 1 + i);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Draw selection
|
|
|
|
|
int contentWidth = rect.width - 5;
|
|
|
|
|
if (selectedIndex >= 0) {
|
|
|
|
|
renderer.fillRect(0, rect.y + selectedIndex % pageItems * rowHeight - 2, rect.width, rowHeight);
|
|
|
|
|
}
|
|
|
|
|
// Draw all items
|
|
|
|
|
const auto pageStartIndex = selectedIndex / pageItems * pageItems;
|
|
|
|
|
for (int i = pageStartIndex; i < itemCount && i < pageStartIndex + pageItems; i++) {
|
|
|
|
|
const int itemY = rect.y + (i % pageItems) * rowHeight;
|
|
|
|
|
int textWidth = contentWidth - BaseMetrics::values.contentSidePadding * 2 - (rowValue != nullptr ? 60 : 0);
|
|
|
|
|
|
|
|
|
|
// Draw name
|
|
|
|
|
auto itemName = rowTitle(i);
|
|
|
|
|
auto font = (rowSubtitle != nullptr) ? UI_12_FONT_ID : UI_10_FONT_ID;
|
|
|
|
|
auto item = renderer.truncatedText(font, itemName.c_str(), textWidth);
|
|
|
|
|
renderer.drawText(font, rect.x + BaseMetrics::values.contentSidePadding, itemY, item.c_str(), i != selectedIndex);
|
|
|
|
|
|
|
|
|
|
if (rowSubtitle != nullptr) {
|
|
|
|
|
// Draw subtitle
|
|
|
|
|
std::string subtitleText = rowSubtitle(i);
|
|
|
|
|
auto subtitle = renderer.truncatedText(UI_10_FONT_ID, subtitleText.c_str(), textWidth);
|
|
|
|
|
renderer.drawText(UI_10_FONT_ID, rect.x + BaseMetrics::values.contentSidePadding, itemY + 30, subtitle.c_str(),
|
|
|
|
|
i != selectedIndex);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (rowValue != nullptr) {
|
|
|
|
|
// Draw value
|
|
|
|
|
std::string valueText = rowValue(i);
|
|
|
|
|
const auto valueTextWidth = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str());
|
|
|
|
|
renderer.drawText(UI_10_FONT_ID, rect.x + contentWidth - BaseMetrics::values.contentSidePadding - valueTextWidth,
|
|
|
|
|
itemY, valueText.c_str(), i != selectedIndex);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void BaseTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* title) const {
|
|
|
|
|
const bool showBatteryPercentage =
|
|
|
|
|
SETTINGS.hideBatteryPercentage != CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_ALWAYS;
|
2026-02-16 14:36:36 +01:00
|
|
|
// Position icon at right edge, drawBatteryRight will place text to the left
|
|
|
|
|
const int batteryX = rect.x + rect.width - 12 - BaseMetrics::values.batteryWidth;
|
|
|
|
|
drawBatteryRight(renderer,
|
|
|
|
|
Rect{batteryX, rect.y + 5, BaseMetrics::values.batteryWidth, BaseMetrics::values.batteryHeight},
|
|
|
|
|
showBatteryPercentage);
|
feat: UI themes, Lyra (#528)
## Summary
### What is the goal of this PR?
- Visual UI overhaul
- UI theme selection
### What changes are included?
- Added a setting "UI Theme": Classic, Lyra
- The classic theme is the current Crosspoint theme
- The Lyra theme implements these mockups:
https://www.figma.com/design/UhxoV4DgUnfrDQgMPPTXog/Lyra-Theme?node-id=2003-7596&t=4CSOZqf0n9uQMxDt-0
by Discord users yagofarias, ruby and gan_shu
- New functions in GFXRenderer to render rounded rectangles, greyscale
fills (using dithering) and thick lines
- Basic UI components are factored into BaseTheme methods which can be
overridden by each additional theme. Methods that are not overridden
will fallback to BaseTheme behavior. This means any new
features/components in CrossPoint only need to be developed for the
"Classic" BaseTheme.
- Additional themes can easily be developed by the community using this
foundation



## Additional Context
- Only the Home, Library and main Settings screens have been implemented
so far, this will be extended to the transfer screens and chapter
selection screen later on, but we need to get the ball rolling somehow
:)
- Loading extra covers on the home screen in the Lyra theme takes a
little more time (about 2 seconds), I added a loading bar popup (reusing
the Indexing progress bar from the reader view, factored into a neat UI
component) but the popup adds ~400ms to the loading time.
- ~~Home screen thumbnails will need to be generated separately for each
theme, because they are displayed in different sizes. Because we're
using dithering, displaying a thumb with the wrong size causes the
picture to look janky or dark as it does on the screenshots above. No
worries this will be fixed in a future PR.~~ Thumbs are now generated
with a size parameter
- UI Icons will need to be implemented in a future PR.
---
### 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 is not a vibe coded PR. Copilot was used for autocompletion to save
time but I reviewed, understood and edited all generated code.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-05 17:50:11 +07:00
|
|
|
|
|
|
|
|
if (title) {
|
2026-02-16 14:36:36 +01:00
|
|
|
int padding = rect.width - batteryX + BaseMetrics::values.batteryWidth;
|
feat: UI themes, Lyra (#528)
## Summary
### What is the goal of this PR?
- Visual UI overhaul
- UI theme selection
### What changes are included?
- Added a setting "UI Theme": Classic, Lyra
- The classic theme is the current Crosspoint theme
- The Lyra theme implements these mockups:
https://www.figma.com/design/UhxoV4DgUnfrDQgMPPTXog/Lyra-Theme?node-id=2003-7596&t=4CSOZqf0n9uQMxDt-0
by Discord users yagofarias, ruby and gan_shu
- New functions in GFXRenderer to render rounded rectangles, greyscale
fills (using dithering) and thick lines
- Basic UI components are factored into BaseTheme methods which can be
overridden by each additional theme. Methods that are not overridden
will fallback to BaseTheme behavior. This means any new
features/components in CrossPoint only need to be developed for the
"Classic" BaseTheme.
- Additional themes can easily be developed by the community using this
foundation



## Additional Context
- Only the Home, Library and main Settings screens have been implemented
so far, this will be extended to the transfer screens and chapter
selection screen later on, but we need to get the ball rolling somehow
:)
- Loading extra covers on the home screen in the Lyra theme takes a
little more time (about 2 seconds), I added a loading bar popup (reusing
the Indexing progress bar from the reader view, factored into a neat UI
component) but the popup adds ~400ms to the loading time.
- ~~Home screen thumbnails will need to be generated separately for each
theme, because they are displayed in different sizes. Because we're
using dithering, displaying a thumb with the wrong size causes the
picture to look janky or dark as it does on the screenshots above. No
worries this will be fixed in a future PR.~~ Thumbs are now generated
with a size parameter
- UI Icons will need to be implemented in a future PR.
---
### 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 is not a vibe coded PR. Copilot was used for autocompletion to save
time but I reviewed, understood and edited all generated code.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-05 17:50:11 +07:00
|
|
|
auto truncatedTitle = renderer.truncatedText(UI_12_FONT_ID, title,
|
|
|
|
|
rect.width - padding * 2 - BaseMetrics::values.contentSidePadding * 2,
|
|
|
|
|
EpdFontFamily::BOLD);
|
|
|
|
|
renderer.drawCenteredText(UI_12_FONT_ID, rect.y + 5, truncatedTitle.c_str(), true, EpdFontFamily::BOLD);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void BaseTheme::drawTabBar(const GfxRenderer& renderer, const Rect rect, const std::vector<TabInfo>& tabs,
|
|
|
|
|
bool selected) const {
|
|
|
|
|
constexpr int underlineHeight = 2; // Height of selection underline
|
|
|
|
|
constexpr int underlineGap = 4; // Gap between text and underline
|
|
|
|
|
|
|
|
|
|
const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID);
|
|
|
|
|
|
|
|
|
|
int currentX = rect.x + BaseMetrics::values.contentSidePadding;
|
|
|
|
|
|
|
|
|
|
for (const auto& tab : tabs) {
|
|
|
|
|
const int textWidth =
|
|
|
|
|
renderer.getTextWidth(UI_12_FONT_ID, tab.label, tab.selected ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR);
|
|
|
|
|
|
|
|
|
|
// Draw underline for selected tab
|
|
|
|
|
if (tab.selected) {
|
|
|
|
|
if (selected) {
|
|
|
|
|
renderer.fillRect(currentX - 3, rect.y, textWidth + 6, lineHeight + underlineGap);
|
|
|
|
|
} else {
|
|
|
|
|
renderer.fillRect(currentX, rect.y + lineHeight + underlineGap, textWidth, underlineHeight);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Draw tab label
|
|
|
|
|
renderer.drawText(UI_12_FONT_ID, currentX, rect.y, tab.label, !(tab.selected && selected),
|
|
|
|
|
tab.selected ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR);
|
|
|
|
|
|
|
|
|
|
currentX += textWidth + BaseMetrics::values.tabSpacing;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Draw the "Recent Book" cover card on the home screen
|
|
|
|
|
// TODO: Refactor method to make it cleaner, split into smaller methods
|
|
|
|
|
void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector<RecentBook>& recentBooks,
|
|
|
|
|
const int selectorIndex, bool& coverRendered, bool& coverBufferStored,
|
|
|
|
|
bool& bufferRestored, std::function<bool()> storeCoverBuffer) const {
|
|
|
|
|
// --- Top "book" card for the current title (selectorIndex == 0) ---
|
|
|
|
|
const int bookWidth = rect.width / 2;
|
|
|
|
|
const int bookHeight = rect.height;
|
|
|
|
|
const int bookX = (rect.width - bookWidth) / 2;
|
|
|
|
|
const int bookY = rect.y;
|
|
|
|
|
const bool hasContinueReading = !recentBooks.empty();
|
|
|
|
|
const bool bookSelected = hasContinueReading && selectorIndex == 0;
|
|
|
|
|
|
|
|
|
|
// Bookmark dimensions (used in multiple places)
|
|
|
|
|
const int bookmarkWidth = bookWidth / 8;
|
|
|
|
|
const int bookmarkHeight = bookHeight / 5;
|
|
|
|
|
const int bookmarkX = bookX + bookWidth - bookmarkWidth - 10;
|
|
|
|
|
const int bookmarkY = bookY + 5;
|
|
|
|
|
|
|
|
|
|
// Draw book card regardless, fill with message based on `hasContinueReading`
|
|
|
|
|
{
|
|
|
|
|
// Draw cover image as background if available (inside the box)
|
|
|
|
|
// Only load from SD on first render, then use stored buffer
|
2026-02-06 14:58:32 +07:00
|
|
|
|
feat: UI themes, Lyra (#528)
## Summary
### What is the goal of this PR?
- Visual UI overhaul
- UI theme selection
### What changes are included?
- Added a setting "UI Theme": Classic, Lyra
- The classic theme is the current Crosspoint theme
- The Lyra theme implements these mockups:
https://www.figma.com/design/UhxoV4DgUnfrDQgMPPTXog/Lyra-Theme?node-id=2003-7596&t=4CSOZqf0n9uQMxDt-0
by Discord users yagofarias, ruby and gan_shu
- New functions in GFXRenderer to render rounded rectangles, greyscale
fills (using dithering) and thick lines
- Basic UI components are factored into BaseTheme methods which can be
overridden by each additional theme. Methods that are not overridden
will fallback to BaseTheme behavior. This means any new
features/components in CrossPoint only need to be developed for the
"Classic" BaseTheme.
- Additional themes can easily be developed by the community using this
foundation



## Additional Context
- Only the Home, Library and main Settings screens have been implemented
so far, this will be extended to the transfer screens and chapter
selection screen later on, but we need to get the ball rolling somehow
:)
- Loading extra covers on the home screen in the Lyra theme takes a
little more time (about 2 seconds), I added a loading bar popup (reusing
the Indexing progress bar from the reader view, factored into a neat UI
component) but the popup adds ~400ms to the loading time.
- ~~Home screen thumbnails will need to be generated separately for each
theme, because they are displayed in different sizes. Because we're
using dithering, displaying a thumb with the wrong size causes the
picture to look janky or dark as it does on the screenshots above. No
worries this will be fixed in a future PR.~~ Thumbs are now generated
with a size parameter
- UI Icons will need to be implemented in a future PR.
---
### 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 is not a vibe coded PR. Copilot was used for autocompletion to save
time but I reviewed, understood and edited all generated code.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-05 17:50:11 +07:00
|
|
|
if (hasContinueReading && !recentBooks[0].coverBmpPath.empty() && !coverRendered) {
|
|
|
|
|
const std::string coverBmpPath =
|
|
|
|
|
UITheme::getCoverThumbPath(recentBooks[0].coverBmpPath, BaseMetrics::values.homeCoverHeight);
|
|
|
|
|
|
|
|
|
|
// First time: load cover from SD and render
|
|
|
|
|
FsFile file;
|
2026-02-08 21:29:14 +01:00
|
|
|
if (Storage.openFileForRead("HOME", coverBmpPath, file)) {
|
feat: UI themes, Lyra (#528)
## Summary
### What is the goal of this PR?
- Visual UI overhaul
- UI theme selection
### What changes are included?
- Added a setting "UI Theme": Classic, Lyra
- The classic theme is the current Crosspoint theme
- The Lyra theme implements these mockups:
https://www.figma.com/design/UhxoV4DgUnfrDQgMPPTXog/Lyra-Theme?node-id=2003-7596&t=4CSOZqf0n9uQMxDt-0
by Discord users yagofarias, ruby and gan_shu
- New functions in GFXRenderer to render rounded rectangles, greyscale
fills (using dithering) and thick lines
- Basic UI components are factored into BaseTheme methods which can be
overridden by each additional theme. Methods that are not overridden
will fallback to BaseTheme behavior. This means any new
features/components in CrossPoint only need to be developed for the
"Classic" BaseTheme.
- Additional themes can easily be developed by the community using this
foundation



## Additional Context
- Only the Home, Library and main Settings screens have been implemented
so far, this will be extended to the transfer screens and chapter
selection screen later on, but we need to get the ball rolling somehow
:)
- Loading extra covers on the home screen in the Lyra theme takes a
little more time (about 2 seconds), I added a loading bar popup (reusing
the Indexing progress bar from the reader view, factored into a neat UI
component) but the popup adds ~400ms to the loading time.
- ~~Home screen thumbnails will need to be generated separately for each
theme, because they are displayed in different sizes. Because we're
using dithering, displaying a thumb with the wrong size causes the
picture to look janky or dark as it does on the screenshots above. No
worries this will be fixed in a future PR.~~ Thumbs are now generated
with a size parameter
- UI Icons will need to be implemented in a future PR.
---
### 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 is not a vibe coded PR. Copilot was used for autocompletion to save
time but I reviewed, understood and edited all generated code.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-05 17:50:11 +07:00
|
|
|
Bitmap bitmap(file);
|
|
|
|
|
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
2026-02-13 12:16:39 +01:00
|
|
|
LOG_DBG("THEME", "Rendering bmp");
|
feat: UI themes, Lyra (#528)
## Summary
### What is the goal of this PR?
- Visual UI overhaul
- UI theme selection
### What changes are included?
- Added a setting "UI Theme": Classic, Lyra
- The classic theme is the current Crosspoint theme
- The Lyra theme implements these mockups:
https://www.figma.com/design/UhxoV4DgUnfrDQgMPPTXog/Lyra-Theme?node-id=2003-7596&t=4CSOZqf0n9uQMxDt-0
by Discord users yagofarias, ruby and gan_shu
- New functions in GFXRenderer to render rounded rectangles, greyscale
fills (using dithering) and thick lines
- Basic UI components are factored into BaseTheme methods which can be
overridden by each additional theme. Methods that are not overridden
will fallback to BaseTheme behavior. This means any new
features/components in CrossPoint only need to be developed for the
"Classic" BaseTheme.
- Additional themes can easily be developed by the community using this
foundation



## Additional Context
- Only the Home, Library and main Settings screens have been implemented
so far, this will be extended to the transfer screens and chapter
selection screen later on, but we need to get the ball rolling somehow
:)
- Loading extra covers on the home screen in the Lyra theme takes a
little more time (about 2 seconds), I added a loading bar popup (reusing
the Indexing progress bar from the reader view, factored into a neat UI
component) but the popup adds ~400ms to the loading time.
- ~~Home screen thumbnails will need to be generated separately for each
theme, because they are displayed in different sizes. Because we're
using dithering, displaying a thumb with the wrong size causes the
picture to look janky or dark as it does on the screenshots above. No
worries this will be fixed in a future PR.~~ Thumbs are now generated
with a size parameter
- UI Icons will need to be implemented in a future PR.
---
### 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 is not a vibe coded PR. Copilot was used for autocompletion to save
time but I reviewed, understood and edited all generated code.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-05 17:50:11 +07:00
|
|
|
// Calculate position to center image within the book card
|
|
|
|
|
int coverX, coverY;
|
|
|
|
|
|
|
|
|
|
if (bitmap.getWidth() > bookWidth || bitmap.getHeight() > bookHeight) {
|
|
|
|
|
const float imgRatio = static_cast<float>(bitmap.getWidth()) / static_cast<float>(bitmap.getHeight());
|
|
|
|
|
const float boxRatio = static_cast<float>(bookWidth) / static_cast<float>(bookHeight);
|
|
|
|
|
|
|
|
|
|
if (imgRatio > boxRatio) {
|
|
|
|
|
coverX = bookX;
|
|
|
|
|
coverY = bookY + (bookHeight - static_cast<int>(bookWidth / imgRatio)) / 2;
|
|
|
|
|
} else {
|
|
|
|
|
coverX = bookX + (bookWidth - static_cast<int>(bookHeight * imgRatio)) / 2;
|
|
|
|
|
coverY = bookY;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
coverX = bookX + (bookWidth - bitmap.getWidth()) / 2;
|
|
|
|
|
coverY = bookY + (bookHeight - bitmap.getHeight()) / 2;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Draw the cover image centered within the book card
|
|
|
|
|
renderer.drawBitmap(bitmap, coverX, coverY, bookWidth, bookHeight);
|
|
|
|
|
|
|
|
|
|
// Draw border around the card
|
|
|
|
|
renderer.drawRect(bookX, bookY, bookWidth, bookHeight);
|
|
|
|
|
|
|
|
|
|
// No bookmark ribbon when cover is shown - it would just cover the art
|
|
|
|
|
|
|
|
|
|
// Store the buffer with cover image for fast navigation
|
|
|
|
|
coverBufferStored = storeCoverBuffer();
|
|
|
|
|
coverRendered = true;
|
|
|
|
|
|
|
|
|
|
// First render: if selected, draw selection indicators now
|
|
|
|
|
if (bookSelected) {
|
2026-02-13 12:16:39 +01:00
|
|
|
LOG_DBG("THEME", "Drawing selection");
|
feat: UI themes, Lyra (#528)
## Summary
### What is the goal of this PR?
- Visual UI overhaul
- UI theme selection
### What changes are included?
- Added a setting "UI Theme": Classic, Lyra
- The classic theme is the current Crosspoint theme
- The Lyra theme implements these mockups:
https://www.figma.com/design/UhxoV4DgUnfrDQgMPPTXog/Lyra-Theme?node-id=2003-7596&t=4CSOZqf0n9uQMxDt-0
by Discord users yagofarias, ruby and gan_shu
- New functions in GFXRenderer to render rounded rectangles, greyscale
fills (using dithering) and thick lines
- Basic UI components are factored into BaseTheme methods which can be
overridden by each additional theme. Methods that are not overridden
will fallback to BaseTheme behavior. This means any new
features/components in CrossPoint only need to be developed for the
"Classic" BaseTheme.
- Additional themes can easily be developed by the community using this
foundation



## Additional Context
- Only the Home, Library and main Settings screens have been implemented
so far, this will be extended to the transfer screens and chapter
selection screen later on, but we need to get the ball rolling somehow
:)
- Loading extra covers on the home screen in the Lyra theme takes a
little more time (about 2 seconds), I added a loading bar popup (reusing
the Indexing progress bar from the reader view, factored into a neat UI
component) but the popup adds ~400ms to the loading time.
- ~~Home screen thumbnails will need to be generated separately for each
theme, because they are displayed in different sizes. Because we're
using dithering, displaying a thumb with the wrong size causes the
picture to look janky or dark as it does on the screenshots above. No
worries this will be fixed in a future PR.~~ Thumbs are now generated
with a size parameter
- UI Icons will need to be implemented in a future PR.
---
### 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 is not a vibe coded PR. Copilot was used for autocompletion to save
time but I reviewed, understood and edited all generated code.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-05 17:50:11 +07:00
|
|
|
renderer.drawRect(bookX + 1, bookY + 1, bookWidth - 2, bookHeight - 2);
|
|
|
|
|
renderer.drawRect(bookX + 2, bookY + 2, bookWidth - 4, bookHeight - 4);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
file.close();
|
|
|
|
|
}
|
2026-02-06 14:58:32 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!bufferRestored && !coverRendered) {
|
feat: UI themes, Lyra (#528)
## Summary
### What is the goal of this PR?
- Visual UI overhaul
- UI theme selection
### What changes are included?
- Added a setting "UI Theme": Classic, Lyra
- The classic theme is the current Crosspoint theme
- The Lyra theme implements these mockups:
https://www.figma.com/design/UhxoV4DgUnfrDQgMPPTXog/Lyra-Theme?node-id=2003-7596&t=4CSOZqf0n9uQMxDt-0
by Discord users yagofarias, ruby and gan_shu
- New functions in GFXRenderer to render rounded rectangles, greyscale
fills (using dithering) and thick lines
- Basic UI components are factored into BaseTheme methods which can be
overridden by each additional theme. Methods that are not overridden
will fallback to BaseTheme behavior. This means any new
features/components in CrossPoint only need to be developed for the
"Classic" BaseTheme.
- Additional themes can easily be developed by the community using this
foundation



## Additional Context
- Only the Home, Library and main Settings screens have been implemented
so far, this will be extended to the transfer screens and chapter
selection screen later on, but we need to get the ball rolling somehow
:)
- Loading extra covers on the home screen in the Lyra theme takes a
little more time (about 2 seconds), I added a loading bar popup (reusing
the Indexing progress bar from the reader view, factored into a neat UI
component) but the popup adds ~400ms to the loading time.
- ~~Home screen thumbnails will need to be generated separately for each
theme, because they are displayed in different sizes. Because we're
using dithering, displaying a thumb with the wrong size causes the
picture to look janky or dark as it does on the screenshots above. No
worries this will be fixed in a future PR.~~ Thumbs are now generated
with a size parameter
- UI Icons will need to be implemented in a future PR.
---
### 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 is not a vibe coded PR. Copilot was used for autocompletion to save
time but I reviewed, understood and edited all generated code.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-05 17:50:11 +07:00
|
|
|
// No cover image: draw border or fill, plus bookmark as visual flair
|
|
|
|
|
if (bookSelected) {
|
|
|
|
|
renderer.fillRect(bookX, bookY, bookWidth, bookHeight);
|
|
|
|
|
} else {
|
|
|
|
|
renderer.drawRect(bookX, bookY, bookWidth, bookHeight);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Draw bookmark ribbon when no cover image (visual decoration)
|
|
|
|
|
if (hasContinueReading) {
|
|
|
|
|
const int notchDepth = bookmarkHeight / 3;
|
|
|
|
|
const int centerX = bookmarkX + bookmarkWidth / 2;
|
|
|
|
|
|
|
|
|
|
const int xPoints[5] = {
|
|
|
|
|
bookmarkX, // top-left
|
|
|
|
|
bookmarkX + bookmarkWidth, // top-right
|
|
|
|
|
bookmarkX + bookmarkWidth, // bottom-right
|
|
|
|
|
centerX, // center notch point
|
|
|
|
|
bookmarkX // bottom-left
|
|
|
|
|
};
|
|
|
|
|
const int yPoints[5] = {
|
|
|
|
|
bookmarkY, // top-left
|
|
|
|
|
bookmarkY, // top-right
|
|
|
|
|
bookmarkY + bookmarkHeight, // bottom-right
|
|
|
|
|
bookmarkY + bookmarkHeight - notchDepth, // center notch point
|
|
|
|
|
bookmarkY + bookmarkHeight // bottom-left
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Draw bookmark ribbon (inverted if selected)
|
|
|
|
|
renderer.fillPolygon(xPoints, yPoints, 5, !bookSelected);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If buffer was restored, draw selection indicators if needed
|
|
|
|
|
if (bufferRestored && bookSelected && coverRendered) {
|
|
|
|
|
// Draw selection border (no bookmark inversion needed since cover has no bookmark)
|
|
|
|
|
renderer.drawRect(bookX + 1, bookY + 1, bookWidth - 2, bookHeight - 2);
|
|
|
|
|
renderer.drawRect(bookX + 2, bookY + 2, bookWidth - 4, bookHeight - 4);
|
|
|
|
|
} else if (!coverRendered && !bufferRestored) {
|
|
|
|
|
// Selection border already handled above in the no-cover case
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (hasContinueReading) {
|
|
|
|
|
const std::string& lastBookTitle = recentBooks[0].title;
|
|
|
|
|
const std::string& lastBookAuthor = recentBooks[0].author;
|
|
|
|
|
|
|
|
|
|
// Invert text colors based on selection state:
|
|
|
|
|
// - With cover: selected = white text on black box, unselected = black text on white box
|
|
|
|
|
// - Without cover: selected = white text on black card, unselected = black text on white card
|
|
|
|
|
|
|
|
|
|
// Split into words (avoid stringstream to keep this light on the MCU)
|
|
|
|
|
std::vector<std::string> words;
|
|
|
|
|
words.reserve(8);
|
|
|
|
|
size_t pos = 0;
|
|
|
|
|
while (pos < lastBookTitle.size()) {
|
|
|
|
|
while (pos < lastBookTitle.size() && lastBookTitle[pos] == ' ') {
|
|
|
|
|
++pos;
|
|
|
|
|
}
|
|
|
|
|
if (pos >= lastBookTitle.size()) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
const size_t start = pos;
|
|
|
|
|
while (pos < lastBookTitle.size() && lastBookTitle[pos] != ' ') {
|
|
|
|
|
++pos;
|
|
|
|
|
}
|
|
|
|
|
words.emplace_back(lastBookTitle.substr(start, pos - start));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::vector<std::string> lines;
|
|
|
|
|
std::string currentLine;
|
|
|
|
|
// Extra padding inside the card so text doesn't hug the border
|
|
|
|
|
const int maxLineWidth = bookWidth - 40;
|
|
|
|
|
const int spaceWidth = renderer.getSpaceWidth(UI_12_FONT_ID);
|
|
|
|
|
|
|
|
|
|
for (auto& i : words) {
|
|
|
|
|
// If we just hit the line limit (3), stop processing words
|
|
|
|
|
if (lines.size() >= 3) {
|
|
|
|
|
// Limit to 3 lines
|
|
|
|
|
// Still have words left, so add ellipsis to last line
|
|
|
|
|
lines.back().append("...");
|
|
|
|
|
|
|
|
|
|
while (!lines.back().empty() && renderer.getTextWidth(UI_12_FONT_ID, lines.back().c_str()) > maxLineWidth) {
|
|
|
|
|
// Remove "..." first, then remove one UTF-8 char, then add "..." back
|
|
|
|
|
lines.back().resize(lines.back().size() - 3); // Remove "..."
|
|
|
|
|
utf8RemoveLastChar(lines.back());
|
|
|
|
|
lines.back().append("...");
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int wordWidth = renderer.getTextWidth(UI_12_FONT_ID, i.c_str());
|
|
|
|
|
while (wordWidth > maxLineWidth && !i.empty()) {
|
|
|
|
|
// Word itself is too long, trim it (UTF-8 safe)
|
|
|
|
|
utf8RemoveLastChar(i);
|
|
|
|
|
// Check if we have room for ellipsis
|
|
|
|
|
std::string withEllipsis = i + "...";
|
|
|
|
|
wordWidth = renderer.getTextWidth(UI_12_FONT_ID, withEllipsis.c_str());
|
|
|
|
|
if (wordWidth <= maxLineWidth) {
|
|
|
|
|
i = withEllipsis;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int newLineWidth = renderer.getTextWidth(UI_12_FONT_ID, currentLine.c_str());
|
|
|
|
|
if (newLineWidth > 0) {
|
|
|
|
|
newLineWidth += spaceWidth;
|
|
|
|
|
}
|
|
|
|
|
newLineWidth += wordWidth;
|
|
|
|
|
|
|
|
|
|
if (newLineWidth > maxLineWidth && !currentLine.empty()) {
|
|
|
|
|
// New line too long, push old line
|
|
|
|
|
lines.push_back(currentLine);
|
|
|
|
|
currentLine = i;
|
|
|
|
|
} else {
|
|
|
|
|
currentLine.append(" ").append(i);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If lower than the line limit, push remaining words
|
|
|
|
|
if (!currentLine.empty() && lines.size() < 3) {
|
|
|
|
|
lines.push_back(currentLine);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Book title text
|
|
|
|
|
int totalTextHeight = renderer.getLineHeight(UI_12_FONT_ID) * static_cast<int>(lines.size());
|
|
|
|
|
if (!lastBookAuthor.empty()) {
|
|
|
|
|
totalTextHeight += renderer.getLineHeight(UI_10_FONT_ID) * 3 / 2;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Vertically center the title block within the card
|
|
|
|
|
int titleYStart = bookY + (bookHeight - totalTextHeight) / 2;
|
|
|
|
|
|
|
|
|
|
// If cover image was rendered, draw box behind title and author
|
|
|
|
|
if (coverRendered) {
|
|
|
|
|
constexpr int boxPadding = 8;
|
|
|
|
|
// Calculate the max text width for the box
|
|
|
|
|
int maxTextWidth = 0;
|
|
|
|
|
for (const auto& line : lines) {
|
|
|
|
|
const int lineWidth = renderer.getTextWidth(UI_12_FONT_ID, line.c_str());
|
|
|
|
|
if (lineWidth > maxTextWidth) {
|
|
|
|
|
maxTextWidth = lineWidth;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!lastBookAuthor.empty()) {
|
|
|
|
|
std::string trimmedAuthor = lastBookAuthor;
|
|
|
|
|
while (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) > maxLineWidth && !trimmedAuthor.empty()) {
|
|
|
|
|
utf8RemoveLastChar(trimmedAuthor);
|
|
|
|
|
}
|
|
|
|
|
if (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) <
|
|
|
|
|
renderer.getTextWidth(UI_10_FONT_ID, lastBookAuthor.c_str())) {
|
|
|
|
|
trimmedAuthor.append("...");
|
|
|
|
|
}
|
|
|
|
|
const int authorWidth = renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str());
|
|
|
|
|
if (authorWidth > maxTextWidth) {
|
|
|
|
|
maxTextWidth = authorWidth;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const int boxWidth = maxTextWidth + boxPadding * 2;
|
|
|
|
|
const int boxHeight = totalTextHeight + boxPadding * 2;
|
|
|
|
|
const int boxX = (rect.width - boxWidth) / 2;
|
|
|
|
|
const int boxY = titleYStart - boxPadding;
|
|
|
|
|
|
|
|
|
|
// Draw box (inverted when selected: black box instead of white)
|
|
|
|
|
renderer.fillRect(boxX, boxY, boxWidth, boxHeight, bookSelected);
|
|
|
|
|
// Draw border around the box (inverted when selected: white border instead of black)
|
|
|
|
|
renderer.drawRect(boxX, boxY, boxWidth, boxHeight, !bookSelected);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const auto& line : lines) {
|
|
|
|
|
renderer.drawCenteredText(UI_12_FONT_ID, titleYStart, line.c_str(), !bookSelected);
|
|
|
|
|
titleYStart += renderer.getLineHeight(UI_12_FONT_ID);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!lastBookAuthor.empty()) {
|
|
|
|
|
titleYStart += renderer.getLineHeight(UI_10_FONT_ID) / 2;
|
|
|
|
|
std::string trimmedAuthor = lastBookAuthor;
|
|
|
|
|
// Trim author if too long (UTF-8 safe)
|
|
|
|
|
bool wasTrimmed = false;
|
|
|
|
|
while (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) > maxLineWidth && !trimmedAuthor.empty()) {
|
|
|
|
|
utf8RemoveLastChar(trimmedAuthor);
|
|
|
|
|
wasTrimmed = true;
|
|
|
|
|
}
|
|
|
|
|
if (wasTrimmed && !trimmedAuthor.empty()) {
|
|
|
|
|
// Make room for ellipsis
|
|
|
|
|
while (renderer.getTextWidth(UI_10_FONT_ID, (trimmedAuthor + "...").c_str()) > maxLineWidth &&
|
|
|
|
|
!trimmedAuthor.empty()) {
|
|
|
|
|
utf8RemoveLastChar(trimmedAuthor);
|
|
|
|
|
}
|
|
|
|
|
trimmedAuthor.append("...");
|
|
|
|
|
}
|
|
|
|
|
renderer.drawCenteredText(UI_10_FONT_ID, titleYStart, trimmedAuthor.c_str(), !bookSelected);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// "Continue Reading" label at the bottom
|
|
|
|
|
const int continueY = bookY + bookHeight - renderer.getLineHeight(UI_10_FONT_ID) * 3 / 2;
|
|
|
|
|
if (coverRendered) {
|
|
|
|
|
// Draw box behind "Continue Reading" text (inverted when selected: black box instead of white)
|
feat: User-Interface I18n System (#728)
**What is the goal of this PR?**
This PR introduces Internationalization (i18n) support, enabling users
to switch the UI language dynamically.
**What changes are included?**
- Core Logic: Added I18n class (`lib/I18n/I18n.h/cpp`) to manage
language state and string retrieval.
- Data Structures:
- `lib/I18n/I18nStrings.h/cpp`: Static string arrays for each supported
language.
- `lib/I18n/I18nKeys.h`: Enum definitions for type-safe string access.
- `lib/I18n/translations.csv`: single source of truth.
- Documentation: Added `docs/i18n.md` detailing the workflow for
developers and translators.
- New Settings activity:
`src/activities/settings/LanguageSelectActivity.h/cpp`
This implementation (building on concepts from #505) prioritizes
performance and memory efficiency.
The core approach is to store all localized strings for each language in
dedicated arrays and access them via enums. This provides O(1) access
with zero runtime overhead, and avoids the heap allocations, hashing,
and collision handling required by `std::map` or `std::unordered_map`.
The main trade-off is that enums and string arrays must remain perfectly
synchronized—any mismatch would result in incorrect strings being
displayed in the UI.
To eliminate this risk, I added a Python script that automatically
generates `I18nStrings.h/.cpp` and `I18nKeys.h` from a CSV file, which
will serve as the single source of truth for all translations. The full
design and workflow are documented in `docs/i18n.md`.
- [x] Python script `generate_i18n.py` to auto-generate C++ files from
CSV
- [x] Populate translations.csv with initial translations.
Currently available translations: English, Español, Français, Deutsch,
Čeština, Português (Brasil), Русский, Svenska.
Thanks, community!
**Status:** EDIT: ready to be merged.
As a proof of concept, the SPANISH strings currently mirror the English
ones, but are fully uppercased.
---
Did you use AI tools to help write this code? _**< PARTIALLY >**_
I used AI for the black work of replacing strings with I18n references
across the project, and for generating the documentation. EDIT: also
some help with merging changes from master.
---------
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Co-authored-by: yeyeto2788 <juanernestobiondi@gmail.com>
2026-02-16 15:28:42 +02:00
|
|
|
const char* continueText = tr(STR_CONTINUE_READING);
|
feat: UI themes, Lyra (#528)
## Summary
### What is the goal of this PR?
- Visual UI overhaul
- UI theme selection
### What changes are included?
- Added a setting "UI Theme": Classic, Lyra
- The classic theme is the current Crosspoint theme
- The Lyra theme implements these mockups:
https://www.figma.com/design/UhxoV4DgUnfrDQgMPPTXog/Lyra-Theme?node-id=2003-7596&t=4CSOZqf0n9uQMxDt-0
by Discord users yagofarias, ruby and gan_shu
- New functions in GFXRenderer to render rounded rectangles, greyscale
fills (using dithering) and thick lines
- Basic UI components are factored into BaseTheme methods which can be
overridden by each additional theme. Methods that are not overridden
will fallback to BaseTheme behavior. This means any new
features/components in CrossPoint only need to be developed for the
"Classic" BaseTheme.
- Additional themes can easily be developed by the community using this
foundation



## Additional Context
- Only the Home, Library and main Settings screens have been implemented
so far, this will be extended to the transfer screens and chapter
selection screen later on, but we need to get the ball rolling somehow
:)
- Loading extra covers on the home screen in the Lyra theme takes a
little more time (about 2 seconds), I added a loading bar popup (reusing
the Indexing progress bar from the reader view, factored into a neat UI
component) but the popup adds ~400ms to the loading time.
- ~~Home screen thumbnails will need to be generated separately for each
theme, because they are displayed in different sizes. Because we're
using dithering, displaying a thumb with the wrong size causes the
picture to look janky or dark as it does on the screenshots above. No
worries this will be fixed in a future PR.~~ Thumbs are now generated
with a size parameter
- UI Icons will need to be implemented in a future PR.
---
### 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 is not a vibe coded PR. Copilot was used for autocompletion to save
time but I reviewed, understood and edited all generated code.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-05 17:50:11 +07:00
|
|
|
const int continueTextWidth = renderer.getTextWidth(UI_10_FONT_ID, continueText);
|
|
|
|
|
constexpr int continuePadding = 6;
|
|
|
|
|
const int continueBoxWidth = continueTextWidth + continuePadding * 2;
|
|
|
|
|
const int continueBoxHeight = renderer.getLineHeight(UI_10_FONT_ID) + continuePadding;
|
|
|
|
|
const int continueBoxX = (rect.width - continueBoxWidth) / 2;
|
|
|
|
|
const int continueBoxY = continueY - continuePadding / 2;
|
|
|
|
|
renderer.fillRect(continueBoxX, continueBoxY, continueBoxWidth, continueBoxHeight, bookSelected);
|
|
|
|
|
renderer.drawRect(continueBoxX, continueBoxY, continueBoxWidth, continueBoxHeight, !bookSelected);
|
|
|
|
|
renderer.drawCenteredText(UI_10_FONT_ID, continueY, continueText, !bookSelected);
|
|
|
|
|
} else {
|
feat: User-Interface I18n System (#728)
**What is the goal of this PR?**
This PR introduces Internationalization (i18n) support, enabling users
to switch the UI language dynamically.
**What changes are included?**
- Core Logic: Added I18n class (`lib/I18n/I18n.h/cpp`) to manage
language state and string retrieval.
- Data Structures:
- `lib/I18n/I18nStrings.h/cpp`: Static string arrays for each supported
language.
- `lib/I18n/I18nKeys.h`: Enum definitions for type-safe string access.
- `lib/I18n/translations.csv`: single source of truth.
- Documentation: Added `docs/i18n.md` detailing the workflow for
developers and translators.
- New Settings activity:
`src/activities/settings/LanguageSelectActivity.h/cpp`
This implementation (building on concepts from #505) prioritizes
performance and memory efficiency.
The core approach is to store all localized strings for each language in
dedicated arrays and access them via enums. This provides O(1) access
with zero runtime overhead, and avoids the heap allocations, hashing,
and collision handling required by `std::map` or `std::unordered_map`.
The main trade-off is that enums and string arrays must remain perfectly
synchronized—any mismatch would result in incorrect strings being
displayed in the UI.
To eliminate this risk, I added a Python script that automatically
generates `I18nStrings.h/.cpp` and `I18nKeys.h` from a CSV file, which
will serve as the single source of truth for all translations. The full
design and workflow are documented in `docs/i18n.md`.
- [x] Python script `generate_i18n.py` to auto-generate C++ files from
CSV
- [x] Populate translations.csv with initial translations.
Currently available translations: English, Español, Français, Deutsch,
Čeština, Português (Brasil), Русский, Svenska.
Thanks, community!
**Status:** EDIT: ready to be merged.
As a proof of concept, the SPANISH strings currently mirror the English
ones, but are fully uppercased.
---
Did you use AI tools to help write this code? _**< PARTIALLY >**_
I used AI for the black work of replacing strings with I18n references
across the project, and for generating the documentation. EDIT: also
some help with merging changes from master.
---------
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Co-authored-by: yeyeto2788 <juanernestobiondi@gmail.com>
2026-02-16 15:28:42 +02:00
|
|
|
renderer.drawCenteredText(UI_10_FONT_ID, continueY, tr(STR_CONTINUE_READING), !bookSelected);
|
feat: UI themes, Lyra (#528)
## Summary
### What is the goal of this PR?
- Visual UI overhaul
- UI theme selection
### What changes are included?
- Added a setting "UI Theme": Classic, Lyra
- The classic theme is the current Crosspoint theme
- The Lyra theme implements these mockups:
https://www.figma.com/design/UhxoV4DgUnfrDQgMPPTXog/Lyra-Theme?node-id=2003-7596&t=4CSOZqf0n9uQMxDt-0
by Discord users yagofarias, ruby and gan_shu
- New functions in GFXRenderer to render rounded rectangles, greyscale
fills (using dithering) and thick lines
- Basic UI components are factored into BaseTheme methods which can be
overridden by each additional theme. Methods that are not overridden
will fallback to BaseTheme behavior. This means any new
features/components in CrossPoint only need to be developed for the
"Classic" BaseTheme.
- Additional themes can easily be developed by the community using this
foundation



## Additional Context
- Only the Home, Library and main Settings screens have been implemented
so far, this will be extended to the transfer screens and chapter
selection screen later on, but we need to get the ball rolling somehow
:)
- Loading extra covers on the home screen in the Lyra theme takes a
little more time (about 2 seconds), I added a loading bar popup (reusing
the Indexing progress bar from the reader view, factored into a neat UI
component) but the popup adds ~400ms to the loading time.
- ~~Home screen thumbnails will need to be generated separately for each
theme, because they are displayed in different sizes. Because we're
using dithering, displaying a thumb with the wrong size causes the
picture to look janky or dark as it does on the screenshots above. No
worries this will be fixed in a future PR.~~ Thumbs are now generated
with a size parameter
- UI Icons will need to be implemented in a future PR.
---
### 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 is not a vibe coded PR. Copilot was used for autocompletion to save
time but I reviewed, understood and edited all generated code.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-05 17:50:11 +07:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// No book to continue reading
|
|
|
|
|
const int y =
|
|
|
|
|
bookY + (bookHeight - renderer.getLineHeight(UI_12_FONT_ID) - renderer.getLineHeight(UI_10_FONT_ID)) / 2;
|
|
|
|
|
renderer.drawCenteredText(UI_12_FONT_ID, y, "No open book");
|
|
|
|
|
renderer.drawCenteredText(UI_10_FONT_ID, y + renderer.getLineHeight(UI_12_FONT_ID), "Start reading below");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void BaseTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount, int selectedIndex,
|
|
|
|
|
const std::function<std::string(int index)>& buttonLabel,
|
|
|
|
|
const std::function<std::string(int index)>& rowIcon) const {
|
|
|
|
|
for (int i = 0; i < buttonCount; ++i) {
|
|
|
|
|
const int tileY = BaseMetrics::values.verticalSpacing + rect.y +
|
|
|
|
|
static_cast<int>(i) * (BaseMetrics::values.menuRowHeight + BaseMetrics::values.menuSpacing);
|
|
|
|
|
|
|
|
|
|
const bool selected = selectedIndex == i;
|
|
|
|
|
|
|
|
|
|
if (selected) {
|
|
|
|
|
renderer.fillRect(rect.x + BaseMetrics::values.contentSidePadding, tileY,
|
|
|
|
|
rect.width - BaseMetrics::values.contentSidePadding * 2, BaseMetrics::values.menuRowHeight);
|
|
|
|
|
} else {
|
|
|
|
|
renderer.drawRect(rect.x + BaseMetrics::values.contentSidePadding, tileY,
|
|
|
|
|
rect.width - BaseMetrics::values.contentSidePadding * 2, BaseMetrics::values.menuRowHeight);
|
|
|
|
|
}
|
|
|
|
|
|
feat: User-Interface I18n System (#728)
**What is the goal of this PR?**
This PR introduces Internationalization (i18n) support, enabling users
to switch the UI language dynamically.
**What changes are included?**
- Core Logic: Added I18n class (`lib/I18n/I18n.h/cpp`) to manage
language state and string retrieval.
- Data Structures:
- `lib/I18n/I18nStrings.h/cpp`: Static string arrays for each supported
language.
- `lib/I18n/I18nKeys.h`: Enum definitions for type-safe string access.
- `lib/I18n/translations.csv`: single source of truth.
- Documentation: Added `docs/i18n.md` detailing the workflow for
developers and translators.
- New Settings activity:
`src/activities/settings/LanguageSelectActivity.h/cpp`
This implementation (building on concepts from #505) prioritizes
performance and memory efficiency.
The core approach is to store all localized strings for each language in
dedicated arrays and access them via enums. This provides O(1) access
with zero runtime overhead, and avoids the heap allocations, hashing,
and collision handling required by `std::map` or `std::unordered_map`.
The main trade-off is that enums and string arrays must remain perfectly
synchronized—any mismatch would result in incorrect strings being
displayed in the UI.
To eliminate this risk, I added a Python script that automatically
generates `I18nStrings.h/.cpp` and `I18nKeys.h` from a CSV file, which
will serve as the single source of truth for all translations. The full
design and workflow are documented in `docs/i18n.md`.
- [x] Python script `generate_i18n.py` to auto-generate C++ files from
CSV
- [x] Populate translations.csv with initial translations.
Currently available translations: English, Español, Français, Deutsch,
Čeština, Português (Brasil), Русский, Svenska.
Thanks, community!
**Status:** EDIT: ready to be merged.
As a proof of concept, the SPANISH strings currently mirror the English
ones, but are fully uppercased.
---
Did you use AI tools to help write this code? _**< PARTIALLY >**_
I used AI for the black work of replacing strings with I18n references
across the project, and for generating the documentation. EDIT: also
some help with merging changes from master.
---------
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Co-authored-by: yeyeto2788 <juanernestobiondi@gmail.com>
2026-02-16 15:28:42 +02:00
|
|
|
std::string labelStr = buttonLabel(i);
|
|
|
|
|
const char* label = labelStr.c_str();
|
feat: UI themes, Lyra (#528)
## Summary
### What is the goal of this PR?
- Visual UI overhaul
- UI theme selection
### What changes are included?
- Added a setting "UI Theme": Classic, Lyra
- The classic theme is the current Crosspoint theme
- The Lyra theme implements these mockups:
https://www.figma.com/design/UhxoV4DgUnfrDQgMPPTXog/Lyra-Theme?node-id=2003-7596&t=4CSOZqf0n9uQMxDt-0
by Discord users yagofarias, ruby and gan_shu
- New functions in GFXRenderer to render rounded rectangles, greyscale
fills (using dithering) and thick lines
- Basic UI components are factored into BaseTheme methods which can be
overridden by each additional theme. Methods that are not overridden
will fallback to BaseTheme behavior. This means any new
features/components in CrossPoint only need to be developed for the
"Classic" BaseTheme.
- Additional themes can easily be developed by the community using this
foundation



## Additional Context
- Only the Home, Library and main Settings screens have been implemented
so far, this will be extended to the transfer screens and chapter
selection screen later on, but we need to get the ball rolling somehow
:)
- Loading extra covers on the home screen in the Lyra theme takes a
little more time (about 2 seconds), I added a loading bar popup (reusing
the Indexing progress bar from the reader view, factored into a neat UI
component) but the popup adds ~400ms to the loading time.
- ~~Home screen thumbnails will need to be generated separately for each
theme, because they are displayed in different sizes. Because we're
using dithering, displaying a thumb with the wrong size causes the
picture to look janky or dark as it does on the screenshots above. No
worries this will be fixed in a future PR.~~ Thumbs are now generated
with a size parameter
- UI Icons will need to be implemented in a future PR.
---
### 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 is not a vibe coded PR. Copilot was used for autocompletion to save
time but I reviewed, understood and edited all generated code.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-05 17:50:11 +07:00
|
|
|
const int textWidth = renderer.getTextWidth(UI_10_FONT_ID, label);
|
|
|
|
|
const int textX = rect.x + (rect.width - textWidth) / 2;
|
|
|
|
|
const int lineHeight = renderer.getLineHeight(UI_10_FONT_ID);
|
|
|
|
|
const int textY =
|
|
|
|
|
tileY + (BaseMetrics::values.menuRowHeight - lineHeight) / 2; // vertically centered assuming y is top of text
|
|
|
|
|
// Invert text when the tile is selected, to contrast with the filled background
|
|
|
|
|
renderer.drawText(UI_10_FONT_ID, textX, textY, label, selectedIndex != i);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Rect BaseTheme::drawPopup(const GfxRenderer& renderer, const char* message) const {
|
|
|
|
|
constexpr int margin = 15;
|
|
|
|
|
constexpr int y = 60;
|
|
|
|
|
const int textWidth = renderer.getTextWidth(UI_12_FONT_ID, message, EpdFontFamily::BOLD);
|
|
|
|
|
const int textHeight = renderer.getLineHeight(UI_12_FONT_ID);
|
|
|
|
|
const int w = textWidth + margin * 2;
|
|
|
|
|
const int h = textHeight + margin * 2;
|
|
|
|
|
const int x = (renderer.getScreenWidth() - w) / 2;
|
|
|
|
|
|
|
|
|
|
renderer.fillRect(x - 2, y - 2, w + 4, h + 4, true); // frame thickness 2
|
|
|
|
|
renderer.fillRect(x, y, w, h, false);
|
|
|
|
|
|
|
|
|
|
const int textX = x + (w - textWidth) / 2;
|
|
|
|
|
const int textY = y + margin - 2;
|
|
|
|
|
renderer.drawText(UI_12_FONT_ID, textX, textY, message, true, EpdFontFamily::BOLD);
|
|
|
|
|
renderer.displayBuffer();
|
|
|
|
|
return Rect{x, y, w, h};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void BaseTheme::fillPopupProgress(const GfxRenderer& renderer, const Rect& layout, const int progress) const {
|
|
|
|
|
constexpr int barHeight = 4;
|
|
|
|
|
const int barWidth = layout.width - 30; // twice the margin in drawPopup to match text width
|
|
|
|
|
const int barX = layout.x + (layout.width - barWidth) / 2;
|
|
|
|
|
const int barY = layout.y + layout.height - 10;
|
|
|
|
|
|
|
|
|
|
int fillWidth = barWidth * progress / 100;
|
|
|
|
|
|
|
|
|
|
renderer.fillRect(barX, barY, fillWidth, barHeight, true);
|
|
|
|
|
|
|
|
|
|
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-05 10:49:38 -05:00
|
|
|
void BaseTheme::drawReadingProgressBar(const GfxRenderer& renderer, const size_t bookProgress) const {
|
feat: UI themes, Lyra (#528)
## Summary
### What is the goal of this PR?
- Visual UI overhaul
- UI theme selection
### What changes are included?
- Added a setting "UI Theme": Classic, Lyra
- The classic theme is the current Crosspoint theme
- The Lyra theme implements these mockups:
https://www.figma.com/design/UhxoV4DgUnfrDQgMPPTXog/Lyra-Theme?node-id=2003-7596&t=4CSOZqf0n9uQMxDt-0
by Discord users yagofarias, ruby and gan_shu
- New functions in GFXRenderer to render rounded rectangles, greyscale
fills (using dithering) and thick lines
- Basic UI components are factored into BaseTheme methods which can be
overridden by each additional theme. Methods that are not overridden
will fallback to BaseTheme behavior. This means any new
features/components in CrossPoint only need to be developed for the
"Classic" BaseTheme.
- Additional themes can easily be developed by the community using this
foundation



## Additional Context
- Only the Home, Library and main Settings screens have been implemented
so far, this will be extended to the transfer screens and chapter
selection screen later on, but we need to get the ball rolling somehow
:)
- Loading extra covers on the home screen in the Lyra theme takes a
little more time (about 2 seconds), I added a loading bar popup (reusing
the Indexing progress bar from the reader view, factored into a neat UI
component) but the popup adds ~400ms to the loading time.
- ~~Home screen thumbnails will need to be generated separately for each
theme, because they are displayed in different sizes. Because we're
using dithering, displaying a thumb with the wrong size causes the
picture to look janky or dark as it does on the screenshots above. No
worries this will be fixed in a future PR.~~ Thumbs are now generated
with a size parameter
- UI Icons will need to be implemented in a future PR.
---
### 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 is not a vibe coded PR. Copilot was used for autocompletion to save
time but I reviewed, understood and edited all generated code.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-05 17:50:11 +07:00
|
|
|
int vieweableMarginTop, vieweableMarginRight, vieweableMarginBottom, vieweableMarginLeft;
|
|
|
|
|
renderer.getOrientedViewableTRBL(&vieweableMarginTop, &vieweableMarginRight, &vieweableMarginBottom,
|
|
|
|
|
&vieweableMarginLeft);
|
|
|
|
|
|
|
|
|
|
const int progressBarMaxWidth = renderer.getScreenWidth() - vieweableMarginLeft - vieweableMarginRight;
|
|
|
|
|
const int progressBarY =
|
|
|
|
|
renderer.getScreenHeight() - vieweableMarginBottom - BaseMetrics::values.bookProgressBarHeight;
|
|
|
|
|
const int barWidth = progressBarMaxWidth * bookProgress / 100;
|
|
|
|
|
renderer.fillRect(vieweableMarginLeft, progressBarY, barWidth, BaseMetrics::values.bookProgressBarHeight, true);
|
|
|
|
|
}
|