feat: Status bar for XTC files (#1849)

## Summary
Add ability to show a status bar for 1-bit XTC files  (closes #1848) 

Overlays a status bar (similar style to that for epubs) over the image.
Defaults to hidden, users can set to show and either top or bottom of
the screen within "Customize Status Bar" reader settings.

I've leaned toward a simple overlay approach rather than trying anything
clever e.g. resizing the original image or allowing for shifting the
image around. I think it's more straight forward for the user to create
a buffer zone when generating the XTC files.

For 2-bit image files, it'd require more work to handle the multiple
render passes so I'm holding off on that.

Sample from a Japanese text XTC
<img width="400" height="600" alt="image"
src="https://github.com/user-attachments/assets/03ac67cf-acec-4b49-969f-73e3656760ce"
/>

### 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? _**< YES | PARTIALLY | NO
>**_
YES - Codex

---------

Co-authored-by: Zach Nelson <zach@zdnelson.com>
This commit is contained in:
Chun Ming Lee
2026-05-08 14:34:42 -05:00
committed by GitHub
co-authored by Zach Nelson
parent b463966045
commit 29fd29f537
6 changed files with 117 additions and 3 deletions
+3
View File
@@ -229,6 +229,9 @@ STR_EXAMPLE_BOOK: "Book Title"
STR_PREVIEW: "Preview"
STR_TITLE: "Title"
STR_BATTERY: "Battery"
STR_XTC_STATUS_BAR: "XTC Status Bar"
STR_BOTTOM: "Bottom"
STR_TOP: "Top"
STR_UI_THEME: "UI Theme"
STR_THEME_CLASSIC: "Classic"
STR_THEME_LYRA: "Lyra"
+7
View File
@@ -57,6 +57,12 @@ class CrossPointSettings {
STATUS_BAR_PROGRESS_BAR_THICKNESS_COUNT
};
enum STATUS_BAR_TITLE { BOOK_TITLE = 0, CHAPTER_TITLE = 1, HIDE_TITLE = 2, STATUS_BAR_TITLE_COUNT };
enum XTC_STATUS_BAR_MODE {
XTC_STATUS_BAR_HIDE = 0,
XTC_STATUS_BAR_BOTTOM = 1,
XTC_STATUS_BAR_TOP = 2,
XTC_STATUS_BAR_MODE_COUNT
};
enum ORIENTATION {
PORTRAIT = 0, // 480x800 logical coordinates (current default)
@@ -161,6 +167,7 @@ class CrossPointSettings {
uint8_t statusBarProgressBarThickness = PROGRESS_BAR_NORMAL;
uint8_t statusBarTitle = CHAPTER_TITLE;
uint8_t statusBarBattery = 1;
uint8_t xtcStatusBarMode = XTC_STATUS_BAR_HIDE;
// Text rendering settings
uint8_t extraParagraphSpacing = 1;
uint8_t textAntiAliasing = 1;
+3
View File
@@ -131,6 +131,9 @@ inline const std::vector<SettingInfo>& getSettingsList() {
StrId::STR_CUSTOMISE_STATUS_BAR),
SettingInfo::Toggle(StrId::STR_BATTERY, &CrossPointSettings::statusBarBattery, "statusBarBattery",
StrId::STR_CUSTOMISE_STATUS_BAR),
SettingInfo::Enum(StrId::STR_XTC_STATUS_BAR, &CrossPointSettings::xtcStatusBarMode,
{StrId::STR_HIDE, StrId::STR_BOTTOM, StrId::STR_TOP}, "xtcStatusBarMode",
StrId::STR_CUSTOMISE_STATUS_BAR),
};
// Only show tilt page turn setting when the QMI8658 IMU is present (X3)
if (halTiltSensor.isAvailable()) {
+77 -1
View File
@@ -12,6 +12,8 @@
#include <HalStorage.h>
#include <I18n.h>
#include <algorithm>
#include "CrossPointSettings.h"
#include "CrossPointState.h"
#include "MappedInputManager.h"
@@ -131,6 +133,76 @@ void XtcReaderActivity::render(RenderLock&&) {
saveProgress();
}
XtcReaderActivity::StatusBarInfo XtcReaderActivity::getStatusBarInfo() const {
const int bookPageCount = static_cast<int>(xtc->getPageCount());
const int bookPage = static_cast<int>(currentPage) + 1;
std::string title =
SETTINGS.statusBarTitle == CrossPointSettings::STATUS_BAR_TITLE::BOOK_TITLE ? xtc->getTitle() : "";
if (!xtc->hasChapters()) {
return StatusBarInfo{bookPage, bookPageCount, std::move(title)};
}
const auto& chapters = xtc->getChapters();
const auto chapterIt = std::find_if(chapters.begin(), chapters.end(), [this](const xtc::ChapterInfo& chapter) {
return currentPage >= chapter.startPage && currentPage <= chapter.endPage;
});
if (chapterIt == chapters.end() || chapterIt->endPage < chapterIt->startPage) {
return StatusBarInfo{bookPage, bookPageCount, std::move(title)};
}
if (SETTINGS.statusBarTitle == CrossPointSettings::STATUS_BAR_TITLE::CHAPTER_TITLE) {
title = chapterIt->name.empty() ? tr(STR_UNNAMED) : chapterIt->name;
}
return StatusBarInfo{static_cast<int>(currentPage - chapterIt->startPage) + 1,
static_cast<int>(chapterIt->endPage - chapterIt->startPage) + 1, std::move(title)};
}
void XtcReaderActivity::renderStatusBarOverlay(const StatusBarOverlayPosition position) const {
const bool drawBottom = SETTINGS.xtcStatusBarMode == CrossPointSettings::XTC_STATUS_BAR_MODE::XTC_STATUS_BAR_BOTTOM &&
position == StatusBarOverlayPosition::Bottom;
const bool drawTop = SETTINGS.xtcStatusBarMode == CrossPointSettings::XTC_STATUS_BAR_MODE::XTC_STATUS_BAR_TOP &&
position == StatusBarOverlayPosition::Top;
if (!drawBottom && !drawTop) {
return;
}
const int statusBarHeight = UITheme::getInstance().getStatusBarHeight();
if (statusBarHeight <= 0) {
return;
}
int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft;
renderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom,
&orientedMarginLeft);
int clearY;
int paddingBottom = 0;
if (position == StatusBarOverlayPosition::Bottom) {
clearY = renderer.getScreenHeight() - orientedMarginBottom - statusBarHeight - 4;
if (clearY < 0) {
clearY = 0;
}
} else {
clearY = orientedMarginTop;
paddingBottom = renderer.getScreenHeight() - statusBarHeight - orientedMarginBottom - orientedMarginTop - 4;
}
const int clearHeight = position == StatusBarOverlayPosition::Bottom
? renderer.getScreenHeight() - orientedMarginBottom - clearY
: statusBarHeight + 4;
if (clearHeight > 0) {
renderer.fillRect(0, clearY, renderer.getScreenWidth(), clearHeight, false);
}
const int pageCount = static_cast<int>(xtc->getPageCount());
const int displayPage = static_cast<int>(currentPage) + 1;
const float progress = pageCount > 0 ? (static_cast<float>(displayPage) * 100.0f) / pageCount : 0.0f;
const auto pageInfo = getStatusBarInfo();
GUI.drawStatusBar(renderer, progress, pageInfo.currentPage, pageInfo.pageCount, pageInfo.title, paddingBottom);
}
void XtcReaderActivity::renderPage() {
const uint16_t pageWidth = xtc->getPageWidth();
const uint16_t pageHeight = xtc->getPageHeight();
@@ -291,7 +363,11 @@ void XtcReaderActivity::renderPage() {
free(pageBuffer);
// XTC pages already have status bar pre-rendered, no need to add our own
if (SETTINGS.xtcStatusBarMode == CrossPointSettings::XTC_STATUS_BAR_MODE::XTC_STATUS_BAR_TOP) {
renderStatusBarOverlay(StatusBarOverlayPosition::Top);
} else {
renderStatusBarOverlay(StatusBarOverlayPosition::Bottom);
}
ReaderUtils::displayWithRefreshCycle(renderer, pagesUntilFullRefresh);
+12
View File
@@ -9,6 +9,9 @@
#include <Xtc.h>
#include <string>
#include <utility>
#include "activities/Activity.h"
class XtcReaderActivity final : public Activity {
@@ -17,7 +20,16 @@ class XtcReaderActivity final : public Activity {
uint32_t currentPage = 0;
int pagesUntilFullRefresh = 0;
enum class StatusBarOverlayPosition { Bottom, Top };
struct StatusBarInfo {
int currentPage;
int pageCount;
std::string title;
};
void renderPage();
void renderStatusBarOverlay(StatusBarOverlayPosition position) const;
StatusBarInfo getStatusBarInfo() const;
void saveProgress() const;
void loadProgress();
@@ -11,13 +11,14 @@
#include "fontIds.h"
namespace {
constexpr int MENU_ITEMS = 6;
constexpr int MENU_ITEMS = 7;
const StrId menuNames[MENU_ITEMS] = {StrId::STR_CHAPTER_PAGE_COUNT,
StrId::STR_BOOK_PROGRESS_PERCENTAGE,
StrId::STR_PROGRESS_BAR,
StrId::STR_PROGRESS_BAR_THICKNESS,
StrId::STR_TITLE,
StrId::STR_BATTERY};
StrId::STR_BATTERY,
StrId::STR_XTC_STATUS_BAR};
constexpr int PROGRESS_BAR_ITEMS = 3;
const StrId progressBarNames[PROGRESS_BAR_ITEMS] = {StrId::STR_BOOK, StrId::STR_CHAPTER, StrId::STR_HIDE};
@@ -28,6 +29,9 @@ const StrId progressBarThicknessNames[PROGRESS_BAR_THICKNESS_ITEMS] = {
constexpr int TITLE_ITEMS = 3;
const StrId titleNames[TITLE_ITEMS] = {StrId::STR_BOOK, StrId::STR_CHAPTER, StrId::STR_HIDE};
constexpr int XTC_STATUS_BAR_ITEMS = 3;
const StrId xtcStatusBarNames[XTC_STATUS_BAR_ITEMS] = {StrId::STR_HIDE, StrId::STR_BOTTOM, StrId::STR_TOP};
const int widthMargin = 10;
const int verticalPreviewPadding = 50;
const int verticalPreviewTextPadding = 40;
@@ -51,6 +55,10 @@ void StatusBarSettingsActivity::onEnter() {
SETTINGS.statusBarTitle = CrossPointSettings::STATUS_BAR_TITLE::HIDE_TITLE;
}
if (SETTINGS.xtcStatusBarMode >= XTC_STATUS_BAR_ITEMS) {
SETTINGS.xtcStatusBarMode = CrossPointSettings::XTC_STATUS_BAR_MODE::XTC_STATUS_BAR_HIDE;
}
requestUpdate();
}
@@ -110,6 +118,9 @@ void StatusBarSettingsActivity::handleSelection() {
} else if (selectedIndex == 5) {
// Show Battery
SETTINGS.statusBarBattery = (SETTINGS.statusBarBattery + 1) % 2;
} else if (selectedIndex == 6) {
// XTC Status Bar
SETTINGS.xtcStatusBarMode = (SETTINGS.xtcStatusBarMode + 1) % XTC_STATUS_BAR_ITEMS;
}
SETTINGS.saveToFile();
}
@@ -143,6 +154,8 @@ void StatusBarSettingsActivity::render(RenderLock&&) {
return I18N.get(titleNames[SETTINGS.statusBarTitle]);
} else if (index == 5) {
return SETTINGS.statusBarBattery ? tr(STR_SHOW) : tr(STR_HIDE);
} else if (index == 6) {
return I18N.get(xtcStatusBarNames[SETTINGS.xtcStatusBarMode]);
} else {
return tr(STR_HIDE);
}