Add icon generation script and list scroll helper
Introduce gen_icons.sh to regenerate UI icons from a manifest file using the FreeInk SDK's Lucide icon set. Add icons.manifest mapping UI icons to Lucide names. Refactor vertical swipe list scrolling into a reusable wasListScroll() helper method used by both reader chapter selection and settings activities. Add static assertion to ensure ThemeMetrics scaling stays in sync with struct changes.
This commit is contained in:
Executable
+26
@@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Regenerate src/components/icons/generated_icons.h from icons.manifest, using the
|
||||||
|
# FreeInk SDK icon generator and its vendored Lucide submodule. generated_icons.h is
|
||||||
|
# a committed build product of these inputs — re-run this after editing the manifest.
|
||||||
|
#
|
||||||
|
# Requires: rsvg-convert (librsvg) + Pillow, and the Lucide submodule fetched
|
||||||
|
# (git submodule update --init in the SDK).
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
SDK="$ROOT/freeink-sdk"
|
||||||
|
SVGDIR="$SDK/libs/assets/Icons/lucide/icons"
|
||||||
|
|
||||||
|
if [ ! -d "$SVGDIR" ]; then
|
||||||
|
echo "Lucide SVGs not found at $SVGDIR" >&2
|
||||||
|
echo "Fetch the submodule: git -C \"$SDK\" submodule update --init libs/assets/Icons/lucide" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
python3 "$SDK/libs/assets/Icons/tools/gen_icons.py" \
|
||||||
|
--manifest "$ROOT/src/components/icons/icons.manifest" \
|
||||||
|
--svgdir "$SVGDIR" \
|
||||||
|
--sizes 24,32,40,48 \
|
||||||
|
--out "$ROOT/src/components/icons/generated_icons.h"
|
||||||
|
|
||||||
|
echo "Regenerated src/components/icons/generated_icons.h"
|
||||||
@@ -56,6 +56,12 @@ void XtcReaderChapterSelectionActivity::loop() {
|
|||||||
const int pageItems = getPageItems();
|
const int pageItems = getPageItems();
|
||||||
const int totalItems = static_cast<int>(xtc->getChapters().size());
|
const int totalItems = static_cast<int>(xtc->getChapters().size());
|
||||||
|
|
||||||
|
// Vertical swipe page-scrolls the list (touch nav without the side buttons).
|
||||||
|
if (mappedInput.wasListScroll(selectorIndex, totalItems, pageItems)) {
|
||||||
|
requestUpdate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
const auto& chapters = xtc->getChapters();
|
const auto& chapters = xtc->getChapters();
|
||||||
if (!chapters.empty() && selectorIndex >= 0 && selectorIndex < static_cast<int>(chapters.size())) {
|
if (!chapters.empty() && selectorIndex >= 0 && selectorIndex < static_cast<int>(chapters.size())) {
|
||||||
|
|||||||
@@ -131,15 +131,11 @@ void SettingsActivity::loop() {
|
|||||||
hasChangedCategory = true;
|
hasChangedCategory = true;
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
} else if (swipe == MappedInputManager::SwipeDir::Up || swipe == MappedInputManager::SwipeDir::Down) {
|
} else if (swipe == MappedInputManager::SwipeDir::Up || swipe == MappedInputManager::SwipeDir::Down) {
|
||||||
// Vertical swipe scrolls the settings list a page at a time (touch navigation
|
// Vertical swipe page-scrolls the list (touch nav without the side buttons).
|
||||||
// without the side buttons). Swipe up moves down the list, and vice versa.
|
// count is settingsCount + 1 because row 0 is the category tab bar.
|
||||||
const int page = std::max(
|
const int page = std::max(
|
||||||
1, UITheme::getNumberOfItemsPerPage(renderer, true, true, BaseTheme::showButtonHints(), false));
|
1, UITheme::getNumberOfItemsPerPage(renderer, true, true, BaseTheme::showButtonHints(), false));
|
||||||
if (swipe == MappedInputManager::SwipeDir::Up) {
|
mappedInput.wasListScroll(selectedSettingIndex, settingsCount + 1, page);
|
||||||
selectedSettingIndex = std::min(selectedSettingIndex + page, settingsCount);
|
|
||||||
} else {
|
|
||||||
selectedSettingIndex = std::max(selectedSettingIndex - page, 0);
|
|
||||||
}
|
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,13 @@ int sp(int v, float s) { return static_cast<int>(std::lround(v * s)); }
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
ThemeMetrics scaleThemeMetrics(const ThemeMetrics& b, float s) {
|
ThemeMetrics scaleThemeMetrics(const ThemeMetrics& b, float s) {
|
||||||
|
// Every pixel field is scaled explicitly below. Counts, percents, ratios, bools,
|
||||||
|
// and the intentionally-unscaled chrome (homeCover*, statusBar*, progressBar*) are
|
||||||
|
// NOT touched. This guard fails when a ThemeMetrics field is added or removed —
|
||||||
|
// classify the new field (scale it, or leave it and note it here) and bump the
|
||||||
|
// expected size, so scaling can never silently miss a field.
|
||||||
|
static_assert(sizeof(ThemeMetrics) == THEME_METRICS_SIZEOF,
|
||||||
|
"ThemeMetrics changed: review scaleThemeMetrics() and update THEME_METRICS_SIZEOF");
|
||||||
ThemeMetrics m = b;
|
ThemeMetrics m = b;
|
||||||
if (s == 1.0f) return m;
|
if (s == 1.0f) return m;
|
||||||
m.batteryWidth = sp(b.batteryWidth, s);
|
m.batteryWidth = sp(b.batteryWidth, s);
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# CrossPoint UI icon set: each UIIcon -> a Lucide icon name.
|
||||||
|
# Edit this, then run scripts/gen_icons.sh to regenerate generated_icons.h.
|
||||||
|
# Browse names in freeink-sdk/libs/assets/Icons/lucide/icons/*.svg.
|
||||||
|
folder = folder
|
||||||
|
text = file-text
|
||||||
|
image = image
|
||||||
|
book = book-open
|
||||||
|
file = file
|
||||||
|
recent = clock
|
||||||
|
settings = settings
|
||||||
|
transfer = arrow-down-up
|
||||||
|
library = library-big
|
||||||
|
wifi = wifi
|
||||||
|
hotspot = router
|
||||||
|
bookmark = bookmark
|
||||||
@@ -104,6 +104,11 @@ struct ThemeMetrics {
|
|||||||
int textFieldLineEndOffset;
|
int textFieldLineEndOffset;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Expected sizeof(ThemeMetrics), in bytes. scaleThemeMetrics() static_asserts
|
||||||
|
// against this so a new/removed metric field can't slip through unclassified;
|
||||||
|
// update it after adding a field (and decide there whether the field scales).
|
||||||
|
inline constexpr unsigned THEME_METRICS_SIZEOF = 216;
|
||||||
|
|
||||||
enum UIIcon { None = 0, Folder, Text, Image, Book, File, Recent, Settings, Transfer, Library, Wifi, Hotspot, Bookmark };
|
enum UIIcon { None = 0, Folder, Text, Image, Book, File, Recent, Settings, Transfer, Library, Wifi, Hotspot, Bookmark };
|
||||||
|
|
||||||
enum class KeyboardKeyType { Normal, Shift, Mode, Space, Del, Ok, Disabled };
|
enum class KeyboardKeyType { Normal, Shift, Mode, Space, Del, Ok, Disabled };
|
||||||
|
|||||||
@@ -34,9 +34,29 @@ constexpr int listIconSize = 24;
|
|||||||
constexpr int mainMenuColumns = 2;
|
constexpr int mainMenuColumns = 2;
|
||||||
int coverWidth = 0;
|
int coverWidth = 0;
|
||||||
|
|
||||||
// Pick the generated Lucide icon variant nearest to targetPx for a UIIcon. The
|
// The generated icon sizes (px). The generator (gen_icons.py) emits these.
|
||||||
// generator emits 24/32/40/48px with each icon's optical center baked in, so a
|
constexpr int kIconSizes[] = {24, 32, 40, 48};
|
||||||
// scaled UI gets a crisp larger asset (not an upscaled one) and exact alignment.
|
|
||||||
|
// Scale a base icon size by the board uiScale.
|
||||||
|
int scaledIcon(int base) { return static_cast<int>(base * UITheme::uiScale() + 0.5f); }
|
||||||
|
|
||||||
|
// Index of the generated size nearest targetPx, and that size in px.
|
||||||
|
int iconVariantIndex(int targetPx) {
|
||||||
|
int best = 0, bestDist = 1 << 30;
|
||||||
|
for (int i = 0; i < 4; ++i) {
|
||||||
|
const int d = kIconSizes[i] > targetPx ? kIconSizes[i] - targetPx : targetPx - kIconSizes[i];
|
||||||
|
if (d < bestDist) {
|
||||||
|
bestDist = d;
|
||||||
|
best = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
int nearestIconSize(int targetPx) { return kIconSizes[iconVariantIndex(targetPx)]; }
|
||||||
|
|
||||||
|
// Generated Lucide icon variant nearest targetPx for a UIIcon. Each icon ships at
|
||||||
|
// every size with its optical center baked in, so a scaled UI gets a crisp larger
|
||||||
|
// asset (not an upscale) with exact alignment.
|
||||||
const freeink::Icon* pickIcon(UIIcon icon, int targetPx) {
|
const freeink::Icon* pickIcon(UIIcon icon, int targetPx) {
|
||||||
const freeink::Icon* const* v = nullptr;
|
const freeink::Icon* const* v = nullptr;
|
||||||
#define VARIANTS(n) \
|
#define VARIANTS(n) \
|
||||||
@@ -61,33 +81,7 @@ const freeink::Icon* pickIcon(UIIcon icon, int targetPx) {
|
|||||||
default: return nullptr;
|
default: return nullptr;
|
||||||
}
|
}
|
||||||
#undef VARIANTS
|
#undef VARIANTS
|
||||||
static constexpr int kSizes[] = {24, 32, 40, 48};
|
return v[iconVariantIndex(targetPx)];
|
||||||
int best = 0, bestDist = 1 << 30;
|
|
||||||
for (int i = 0; i < 4; ++i) {
|
|
||||||
const int d = kSizes[i] > targetPx ? kSizes[i] - targetPx : targetPx - kSizes[i];
|
|
||||||
if (d < bestDist) {
|
|
||||||
bestDist = d;
|
|
||||||
best = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return v[best];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scale a base icon size by the board uiScale.
|
|
||||||
int scaledIcon(int base) { return static_cast<int>(base * UITheme::uiScale() + 0.5f); }
|
|
||||||
|
|
||||||
// Nearest generated icon size (24/32/40/48) to targetPx.
|
|
||||||
int nearestIconSize(int targetPx) {
|
|
||||||
static constexpr int kSizes[] = {24, 32, 40, 48};
|
|
||||||
int best = kSizes[0], bestDist = 1 << 30;
|
|
||||||
for (int s : kSizes) {
|
|
||||||
const int d = s > targetPx ? s - targetPx : targetPx - s;
|
|
||||||
if (d < bestDist) {
|
|
||||||
bestDist = d;
|
|
||||||
best = s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return best;
|
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user