From 4a17d21d802eb83cf75b01c50a173b15cbf672b6 Mon Sep 17 00:00:00 2001 From: Justin Mitchell Date: Tue, 16 Jun 2026 00:15:18 -0400 Subject: [PATCH] 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. --- scripts/gen_icons.sh | 26 +++++++++ .../XtcReaderChapterSelectionActivity.cpp | 6 +++ src/activities/settings/SettingsActivity.cpp | 10 ++-- src/components/UITheme.cpp | 7 +++ src/components/icons/icons.manifest | 15 ++++++ src/components/themes/BaseTheme.h | 5 ++ src/components/themes/lyra/LyraTheme.cpp | 54 +++++++++---------- 7 files changed, 86 insertions(+), 37 deletions(-) create mode 100755 scripts/gen_icons.sh create mode 100644 src/components/icons/icons.manifest diff --git a/scripts/gen_icons.sh b/scripts/gen_icons.sh new file mode 100755 index 00000000..f37f2aaf --- /dev/null +++ b/scripts/gen_icons.sh @@ -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" diff --git a/src/activities/reader/XtcReaderChapterSelectionActivity.cpp b/src/activities/reader/XtcReaderChapterSelectionActivity.cpp index 99d6b28d..b95ac2b1 100644 --- a/src/activities/reader/XtcReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/XtcReaderChapterSelectionActivity.cpp @@ -56,6 +56,12 @@ void XtcReaderChapterSelectionActivity::loop() { const int pageItems = getPageItems(); const int totalItems = static_cast(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)) { const auto& chapters = xtc->getChapters(); if (!chapters.empty() && selectorIndex >= 0 && selectorIndex < static_cast(chapters.size())) { diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 8374ce4b..a7fbdcfd 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -131,15 +131,11 @@ void SettingsActivity::loop() { hasChangedCategory = true; requestUpdate(); } else if (swipe == MappedInputManager::SwipeDir::Up || swipe == MappedInputManager::SwipeDir::Down) { - // Vertical swipe scrolls the settings list a page at a time (touch navigation - // without the side buttons). Swipe up moves down the list, and vice versa. + // Vertical swipe page-scrolls the list (touch nav without the side buttons). + // count is settingsCount + 1 because row 0 is the category tab bar. const int page = std::max( 1, UITheme::getNumberOfItemsPerPage(renderer, true, true, BaseTheme::showButtonHints(), false)); - if (swipe == MappedInputManager::SwipeDir::Up) { - selectedSettingIndex = std::min(selectedSettingIndex + page, settingsCount); - } else { - selectedSettingIndex = std::max(selectedSettingIndex - page, 0); - } + mappedInput.wasListScroll(selectedSettingIndex, settingsCount + 1, page); requestUpdate(); } diff --git a/src/components/UITheme.cpp b/src/components/UITheme.cpp index ff510912..65710ea7 100644 --- a/src/components/UITheme.cpp +++ b/src/components/UITheme.cpp @@ -25,6 +25,13 @@ int sp(int v, float s) { return static_cast(std::lround(v * s)); } } // namespace 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; if (s == 1.0f) return m; m.batteryWidth = sp(b.batteryWidth, s); diff --git a/src/components/icons/icons.manifest b/src/components/icons/icons.manifest new file mode 100644 index 00000000..b43252d9 --- /dev/null +++ b/src/components/icons/icons.manifest @@ -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 diff --git a/src/components/themes/BaseTheme.h b/src/components/themes/BaseTheme.h index aff7c73c..42a3e4c4 100644 --- a/src/components/themes/BaseTheme.h +++ b/src/components/themes/BaseTheme.h @@ -104,6 +104,11 @@ struct ThemeMetrics { 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 class KeyboardKeyType { Normal, Shift, Mode, Space, Del, Ok, Disabled }; diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index dfd6eff1..9a6c8886 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -34,9 +34,29 @@ constexpr int listIconSize = 24; constexpr int mainMenuColumns = 2; int coverWidth = 0; -// Pick the generated Lucide icon variant nearest to targetPx for a UIIcon. The -// generator emits 24/32/40/48px with each icon's optical center baked in, so a -// scaled UI gets a crisp larger asset (not an upscaled one) and exact alignment. +// The generated icon sizes (px). The generator (gen_icons.py) emits these. +constexpr int kIconSizes[] = {24, 32, 40, 48}; + +// Scale a base icon size by the board uiScale. +int scaledIcon(int base) { return static_cast(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* const* v = nullptr; #define VARIANTS(n) \ @@ -61,33 +81,7 @@ const freeink::Icon* pickIcon(UIIcon icon, int targetPx) { default: return nullptr; } #undef VARIANTS - static constexpr int kSizes[] = {24, 32, 40, 48}; - 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(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; + return v[iconVariantIndex(targetPx)]; } } // namespace