9 Commits
Author SHA1 Message Date
NeilClack 9b0bd517c6 pad footer 2026-08-25 10:33:04 -05:00
NeilClack babb74c7ad Fix attic upload
build-dev / nix-build (push) Successful in 40s
2026-08-25 07:55:32 -05:00
NeilClack 47785cd4b9 Proclamations: sub-cats always expanded, stable list width
build-dev / nix-build (push) Successful in 43s
- Sub-categories start expanded and cannot be collapsed
- Only root categories toggle open/closed
- LEFT on a sub-category is ignored (navigates to parent for depth-2)
- [+]/[-] indicators only shown on root categories
- Bounding box width computed over all items to prevent
  horizontal shifting when groups expand or collapse
2026-08-25 07:15:59 -05:00
NeilClack 15583edef8 Re-order settings menu 2026-08-25 06:57:43 -05:00
NeilClack 63265cc22f Dynamically size the fullscreen window; present design space via LETTERBOX
build-dev / nix-build (push) Successful in 42s
SDL_CreateWindow hardcoded a 1920x1080 backbuffer while SDL_WINDOW_FULLSCREEN
let compositors run a different output mode; the mismatched buffer reached
the panel unscaled — quarter-size in the top-left on 4K sets, cropped ("4x
too large") on smaller non-1080p panels under cage/Wayland. Logical
presentation never got to map the fixed design space because its target
(the backbuffer) was pinned to stale dimensions.

Size the fullscreen window from SDL_GetCurrentDisplayMode so the first
commit already matches scanout 1:1, switch logical presentation to LETTERBOX
(aspect-safe bars on non-16:9 panels), and route both through new
kDesignWidth/kDesignHeight constants. Harden getLayoutSize() to always write
its out params and null-check the renderer/render-window before use.

Log requested/window/pixel-buffer/render-output sizes at startup and on
every resize event so field units self-diagnose this bug class into
~/.local/radiantwave/log.txt. SDL_WINDOW_HIGH_PIXEL_DENSITY stays off on
purpose: kiosk panels are not HiDPI, and keeping the pixel buffer equal to
the display mode keeps compositor scanout unscaled.
2026-08-23 17:06:05 -05:00
NeilClack 9e73c82641 Fix attic push
build-dev / nix-build (push) Successful in 43s
2026-08-23 15:12:35 -05:00
NeilClack f2a40cd019 Remove attic-client install step
build-dev / nix-build (push) Successful in 39s
2026-08-23 12:46:08 -05:00
NeilClack 5caf1d6ef8 Move to checksums.md5
build-dev / nix-build (push) Failing after 13s
Server is scheduled to generate this file nightly
2026-08-23 12:39:05 -05:00
NeilClack e17e881c00 Split builds 2026-08-23 12:38:44 -05:00
8 changed files with 249 additions and 82 deletions
+68
View File
@@ -0,0 +1,68 @@
name: build-dev
on:
# Every push to dev: build, run the offline test suite, publish to the
# dev Attic cache.
push:
branches: [dev]
jobs:
nix-build:
runs-on: build-server
steps:
# actions/checkout@v4 is unreliable on host-mode runners (its files are
# copied into the workspace instead of the repo), so clone explicitly.
- name: Checkout
run: |
rm -rf ./* ./.[!.]* 2>/dev/null || true
git config --global http.extraheader "Authorization: token ${{ github.token }}"
git clone --branch "${{ gitea.ref_name }}" \
"${{ gitea.server_url }}/${{ gitea.repository }}.git" .
test -f flake.nix
git log --oneline -1
- name: Inspect workspace
run: |
pwd
ls -la
git rev-parse --show-toplevel 2>&1 || true
git remote -v 2>&1 || true
git describe --tags --always 2>&1 || true
test -f flake.nix && echo "flake.nix present" || echo "NO flake.nix"
# Dev builds fingerprint as the short commit hash.
# Persisted via GITHUB_ENV so the impure nix eval below reads it via
# builtins.getEnv.
- name: Compute version
run: |
echo "APP_VERSION=$(git rev-parse --short HEAD)" >> "$GITHUB_ENV"
# checks is keyed per-system (checks.<system>.default); nix build's
# #attr shorthand can't resolve it, so resolve the current system inline.
# --impure lets the flake read APP_VERSION via builtins.getEnv
# (pure evals always get the package defaults).
- name: Build package
run: |
nix build -L ".#packages.$(nix eval --impure --raw --expr 'builtins.currentSystem').default" \
--impure --out-link result-pkg
# The offline test suite runs in the check derivation's checkPhase.
# This is a separate build/output from the package itself.
- name: Build + offline test suite
run: |
nix build -L ".#checks.$(nix eval --impure --raw --expr 'builtins.currentSystem').default" \
--impure --out-link result-checks
- name: Live portal tests
env:
RWT_LIVE_ACCOUNT_EMAIL: ${{ secrets.RWT_LIVE_ACCOUNT_EMAIL }}
RWT_LIVE_LICENSE_EMAIL: ${{ secrets.RWT_LIVE_LICENSE_EMAIL }}
RWT_LIVE_LICENSE_KEY: ${{ secrets.RWT_LIVE_LICENSE_KEY }}
run: |
nix develop --command bash -c '
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug
cmake --build build --target test_api_client test_license_service
./build/test_api_client
./build/test_license_service
'
# Dev closure (the actual package, not the check output) lands in the
# dev cache.
- name: Push to Attic dev cache
env:
ATTIC_TOKEN: ${{ secrets.ATTIC_TOKEN_DEV }}
run: |
attic login radiantwave https://attic.radiantwavetech.com "$ATTIC_TOKEN"
attic push radiantwave:radiantwave-dev ./result-pkg
@@ -1,9 +1,8 @@
name: build name: build-main
on: on:
# Manual only for now. Select the branch to build when dispatching: # Manual only. Dispatch against main: build, run the offline test suite,
# main -> APP_VERSION is the current git tag # publish to the release Attic cache.
# dev -> APP_VERSION is the current commit hash
workflow_dispatch: workflow_dispatch:
jobs: jobs:
@@ -12,8 +11,8 @@ jobs:
steps: steps:
# actions/checkout@v4 is unreliable on host-mode runners (its files are # actions/checkout@v4 is unreliable on host-mode runners (its files are
# copied into the workspace instead of the repo), so clone explicitly. # copied into the workspace instead of the repo), so clone explicitly.
# A full clone (no --depth) is required so `git describe --tags` on main # A full clone (no --depth) is required so `git describe --tags` resolves
# resolves the real nearest tag instead of falling back to a hash. # the real nearest tag instead of falling back to a hash.
- name: Checkout - name: Checkout
run: | run: |
rm -rf ./* ./.[!.]* 2>/dev/null || true rm -rf ./* ./.[!.]* 2>/dev/null || true
@@ -32,26 +31,27 @@ jobs:
git describe --tags --always 2>&1 || true git describe --tags --always 2>&1 || true
test -f flake.nix && echo "flake.nix present" || echo "NO flake.nix" test -f flake.nix && echo "flake.nix present" || echo "NO flake.nix"
# Compute the version string baked into the binary's Home footer: # Release builds report the current git tag.
# main reports the git tag, dev reports the commit hash fingerprint. # Persisted via GITHUB_ENV so the impure nix eval below reads it via
# The key stays in the build step's environment (never echoed). # builtins.getEnv.
- name: Compute version - name: Compute version
id: version
run: | run: |
if [ "${{ gitea.ref_name }}" = "main" ]; then echo "APP_VERSION=$(git describe --tags --always)" >> "$GITHUB_ENV"
APP_VERSION="$(git describe --tags --always)"
else
APP_VERSION="$(git rev-parse --short HEAD)"
fi
echo "Branch: ${{ gitea.ref_name }} -> APP_VERSION=$APP_VERSION"
# checks is keyed per-system (checks.<system>.default); nix build's # checks is keyed per-system (checks.<system>.default); nix build's
# #attr shorthand can't resolve it, so resolve the current system inline. # #attr shorthand can't resolve it, so resolve the current system inline.
# --impure lets the flake read APP_VERSION via builtins.getEnv # --impure lets the flake read APP_VERSION via builtins.getEnv
# (pure evals always get the package defaults). The offline test suite # (pure evals always get the package defaults).
# runs in the package's checkPhase. - name: Build package
run: |
nix build -L ".#packages.$(nix eval --impure --raw --expr 'builtins.currentSystem').default" \
--impure --out-link result-pkg
# The offline test suite runs in the check derivation's checkPhase.
# This is a separate build/output from the package itself.
- name: Build + offline test suite - name: Build + offline test suite
run: nix build -L ".#checks.$(nix eval --impure --raw --expr 'builtins.currentSystem').default" --impure run: |
nix build -L ".#checks.$(nix eval --impure --raw --expr 'builtins.currentSystem').default" \
--impure --out-link result-checks
- name: Live portal tests - name: Live portal tests
env: env:
@@ -66,10 +66,11 @@ jobs:
./build/test_license_service ./build/test_license_service
' '
- name: Push to Attic cache # Release closure (the actual package, not the check output) lands in the
# release cache.
- name: Push to Attic release cache
env:
ATTIC_TOKEN: ${{ secrets.ATTIC_TOKEN_PROD }}
run: | run: |
nix profile install github:zhaofengli/attic#attic-client \ attic login radiantwave https://attic.radiantwavetech.com "$ATTIC_TOKEN"
--extra-experimental-features "nix-command flakes" attic push radiantwave:radiantwave ./result-pkg
attic login radiantwave https://attic.radiantwavetech.com "${{ secrets.ATTIC_TOKEN }}"
attic push radiantwave ./result-kiosk ./result-android ./result-commercial
+3 -3
View File
@@ -37,7 +37,7 @@ struct AudioManifestEntry {
// and keeps the database's audio_files table in sync with what is actually // and keeps the database's audio_files table in sync with what is actually
// on disk. // on disk.
// //
// Fetches new_checksums.md5, compares each entry against the local file's MD5, // Fetches checksums.md5, compares each entry against the local file's MD5,
// and re-downloads anything that is missing or whose checksum differs. // and re-downloads anything that is missing or whose checksum differs.
// Files present on disk but absent from the manifest are deleted, and the // Files present on disk but absent from the manifest are deleted, and the
// audio_files table is reconciled on every pass (and after each individual // audio_files table is reconciled on every pass (and after each individual
@@ -68,7 +68,7 @@ public:
static constexpr const char *AUDIO_BASE_URL = static constexpr const char *AUDIO_BASE_URL =
"https://files.radiantwavetech.com/audio/"; "https://files.radiantwavetech.com/audio/";
static constexpr const char *MANIFEST_FILENAME = "new_checksums.md5"; static constexpr const char *MANIFEST_FILENAME = "checksums.md5";
private: private:
void run(); void run();
@@ -88,7 +88,7 @@ private:
db::DatabaseManager *db_ = nullptr; db::DatabaseManager *db_ = nullptr;
}; };
// Parses new_checksums.md5 manifest text into entries. Blank lines and lines // Parses checksums.md5 manifest text into entries. Blank lines and lines
// starting with '#' are ignored. Lines that don't match a known format are // starting with '#' are ignored. Lines that don't match a known format are
// skipped. Returns false if no valid entries were found. // skipped. Returns false if no valid entries were found.
bool parseManifest(const std::string &text, bool parseManifest(const std::string &text,
+2 -2
View File
@@ -281,10 +281,10 @@ void Home::render(SDL_Renderer *renderer)
fourSpaces.destroy(); fourSpaces.destroy();
const float margin = 20.0f; const float margin = 20.0f;
float rectRight = fw - margin; float rectRight = fw - margin - 50.0f;
float rectLeft = rectRight - (maxNameW + spaceW4 + maxValueW); float rectLeft = rectRight - (maxNameW + spaceW4 + maxValueW);
float lineY = fh - margin; float lineY = fh - margin - 50.0f;
const float lineGap = 6.0f; const float lineGap = 6.0f;
for (size_t i = footerNames_.size(); i-- > 0;) for (size_t i = footerNames_.size(); i-- > 0;)
{ {
+41 -7
View File
@@ -4,14 +4,48 @@
namespace radiantwave { namespace radiantwave {
inline void getLayoutSize(SDL_Renderer* renderer, int* w, int* h) // Design resolution every page lays out against. The renderer's logical
// presentation (LETTERBOX) scales this fixed space onto whatever
// window/backbuffer the display actually provides.
inline constexpr int kDesignWidth = 1920;
inline constexpr int kDesignHeight = 1080;
// Returns the size pages should lay out against: the renderer's logical size
// when presentation is active, otherwise the real window size. The output
// params are always written — design constants are the last-resort fallback.
inline void getLayoutSize(SDL_Renderer *renderer, int *w, int *h)
{ {
SDL_RendererLogicalPresentation mode = SDL_LOGICAL_PRESENTATION_DISABLED; if (!w || !h)
if (!SDL_GetRenderLogicalPresentation(renderer, w, h, &mode) || return;
mode == SDL_LOGICAL_PRESENTATION_DISABLED || *w <= 0 || *h <= 0) *w = kDesignWidth;
{ *h = kDesignHeight;
SDL_GetWindowSize(SDL_GetRenderWindow(renderer), w, h);
} if (!renderer)
return;
SDL_RendererLogicalPresentation mode = SDL_LOGICAL_PRESENTATION_DISABLED;
int logicalW = 0;
int logicalH = 0;
if (SDL_GetRenderLogicalPresentation(renderer, &logicalW, &logicalH,
&mode) &&
mode != SDL_LOGICAL_PRESENTATION_DISABLED && logicalW > 0 &&
logicalH > 0) {
*w = logicalW;
*h = logicalH;
return;
}
SDL_Window *window = SDL_GetRenderWindow(renderer);
if (!window)
return;
int windowW = 0;
int windowH = 0;
SDL_GetWindowSize(window, &windowW, &windowH);
if (windowW > 0 && windowH > 0) {
*w = windowW;
*h = windowH;
}
} }
} // namespace radiantwave } // namespace radiantwave
+61 -10
View File
@@ -4,6 +4,7 @@
#include "app_state.h" #include "app_state.h"
#include "home.h" #include "home.h"
#include "logical_size.h"
#include "logger/logger.h" #include "logger/logger.h"
#include "provisioning.h" #include "provisioning.h"
@@ -32,11 +33,26 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[]) {
auto *state = new AppState{}; auto *state = new AppState{};
// SDL_WINDOW_FULLSCREEN fills the desktop's native resolution, so the // Size the fullscreen window from the display's actual mode so the very
// compositor never has to scale the moving scroller (scaling moving text // first buffer commit already matches what the compositor scanouts 1:1.
// causes flicker). // Requesting a hardcoded size left the backbuffer at that size on
state->window = // displays running a different mode, and frames reached the panel
SDL_CreateWindow("RadiantWave", 1920, 1080, SDL_WINDOW_FULLSCREEN); // unscaled (quarter-size on 4K, cropped on smaller panels).
const SDL_DisplayMode *displayMode =
SDL_GetCurrentDisplayMode(SDL_GetPrimaryDisplay());
int windowWidth =
(displayMode && displayMode->w > 0) ? displayMode->w
: radiantwave::kDesignWidth;
int windowHeight =
(displayMode && displayMode->h > 0) ? displayMode->h
: radiantwave::kDesignHeight;
// Borderless desktop fullscreen: the compositor never has to scale the
// moving scroller (scaling moving text causes flicker). Deliberately no
// SDL_WINDOW_HIGH_PIXEL_DENSITY — kiosk panels are not HiDPI, so keeping
// the pixel buffer equal to the display mode keeps scanout 1:1.
state->window = SDL_CreateWindow("RadiantWave", windowWidth, windowHeight,
SDL_WINDOW_FULLSCREEN);
if (!state->window) { if (!state->window) {
SDL_Log("Failed to create window: %s", SDL_GetError()); SDL_Log("Failed to create window: %s", SDL_GetError());
delete state; delete state;
@@ -59,16 +75,38 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[]) {
std::string(SDL_GetError()) + ")"); std::string(SDL_GetError()) + ")");
} }
// Lock the renderer to a 1920x1080 logical coordinate space. All page // All page layout targets the fixed design resolution; LETTERBOX maps it
// layout code already targets this resolution, so everything renders at // onto the real window with aspect preserved (bars on non-16:9 panels).
// the correct size on any display — the GPU scales to native pixels. if (!SDL_SetRenderLogicalPresentation(state->renderer,
if (!SDL_SetRenderLogicalPresentation(state->renderer, 1920, 1080, radiantwave::kDesignWidth,
SDL_LOGICAL_PRESENTATION_STRETCH)) { radiantwave::kDesignHeight,
SDL_LOGICAL_PRESENTATION_LETTERBOX)) {
radiantwave::logger::Logger::instance().warning( radiantwave::logger::Logger::instance().warning(
"Graphics", "Could not set logical render size; layout may be " "Graphics", "Could not set logical render size; layout may be "
"wrong on HiDPI displays"); "wrong on HiDPI displays");
} }
{
// Record how the requested mode, window, pixel buffer, and render
// output actually compare — this is the evidence trail for
// display-scaling problems on field units.
int windowW = 0, windowH = 0;
SDL_GetWindowSize(state->window, &windowW, &windowH);
int pixelW = 0, pixelH = 0;
SDL_GetWindowSizeInPixels(state->window, &pixelW, &pixelH);
int outputW = 0, outputH = 0;
SDL_GetRenderOutputSize(state->renderer, &outputW, &outputH);
radiantwave::logger::Logger::instance().info(
"Graphics",
"Requested " + std::to_string(windowWidth) + "x" +
std::to_string(windowHeight) + ", window " +
std::to_string(windowW) + "x" + std::to_string(windowH) +
", pixels " + std::to_string(pixelW) + "x" +
std::to_string(pixelH) + ", render output " +
std::to_string(outputW) + "x" + std::to_string(outputH) +
", presentation LETTERBOX");
}
SDL_HideCursor(); SDL_HideCursor();
// Initialize configuration, database, and network manager // Initialize configuration, database, and network manager
@@ -157,6 +195,19 @@ SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event) {
state.quitRequested = true; state.quitRequested = true;
} }
if (event->type == SDL_EVENT_WINDOW_RESIZED) {
radiantwave::logger::Logger::instance().info(
"Graphics", "Window resized to " +
std::to_string(event->window.data1) + "x" +
std::to_string(event->window.data2));
// Logical presentation persists across resizes; re-apply defensively
// in case backend state was reset underneath us.
SDL_SetRenderLogicalPresentation(state.renderer,
radiantwave::kDesignWidth,
radiantwave::kDesignHeight,
SDL_LOGICAL_PRESENTATION_LETTERBOX);
}
if (!state.keybinds.handleEvent(*event)) { if (!state.keybinds.handleEvent(*event)) {
state.pages.handleEvent(*event); state.pages.handleEvent(*event);
} }
+30 -17
View File
@@ -198,7 +198,7 @@ void ProclamationOptionsPage::rebuildItems()
FlatItem::Type::SubCategory, FlatItem::Type::SubCategory,
std::to_string(sub.id), std::to_string(sub.id),
sub.name, sub.name,
1, false, false, false, 1, true, false, false,
}); });
for (const auto& p : proclamations_) { for (const auto& p : proclamations_) {
@@ -268,7 +268,7 @@ void ProclamationOptionsPage::rebuildVisible()
parentVisible = true; // the always-on top-level proclamation parentVisible = true; // the always-on top-level proclamation
} else if (currentRoot >= 0 && items_[currentRoot].expanded) { } else if (currentRoot >= 0 && items_[currentRoot].expanded) {
parentVisible = (item.depth == 1) || parentVisible = (item.depth == 1) ||
(item.depth == 2 && currentSub >= 0 && items_[currentSub].expanded); (item.depth == 2 && currentSub >= 0);
} }
if (parentVisible) visible_.push_back(i); if (parentVisible) visible_.push_back(i);
} }
@@ -283,7 +283,7 @@ void ProclamationOptionsPage::toggleItem()
if (visible_.empty()) return; if (visible_.empty()) return;
FlatItem& item = items_[visible_[highlightIndex_]]; FlatItem& item = items_[visible_[highlightIndex_]];
if (item.type == FlatItem::Type::RootCategory || item.type == FlatItem::Type::SubCategory) { if (item.type == FlatItem::Type::RootCategory) {
item.expanded = !item.expanded; item.expanded = !item.expanded;
rebuildVisible(); rebuildVisible();
return; return;
@@ -388,7 +388,7 @@ void ProclamationOptionsPage::collapseCurrent()
{ {
if (visible_.empty()) return; if (visible_.empty()) return;
FlatItem& item = items_[visible_[highlightIndex_]]; FlatItem& item = items_[visible_[highlightIndex_]];
if (item.type == FlatItem::Type::RootCategory || item.type == FlatItem::Type::SubCategory) { if (item.type == FlatItem::Type::RootCategory) {
if (item.expanded) { if (item.expanded) {
item.expanded = false; item.expanded = false;
rebuildVisible(); rebuildVisible();
@@ -400,7 +400,7 @@ void ProclamationOptionsPage::expandCurrent()
{ {
if (visible_.empty()) return; if (visible_.empty()) return;
FlatItem& item = items_[visible_[highlightIndex_]]; FlatItem& item = items_[visible_[highlightIndex_]];
if (item.type == FlatItem::Type::RootCategory || item.type == FlatItem::Type::SubCategory) { if (item.type == FlatItem::Type::RootCategory) {
if (!item.expanded) { if (!item.expanded) {
item.expanded = true; item.expanded = true;
rebuildVisible(); rebuildVisible();
@@ -526,8 +526,7 @@ void ProclamationOptionsPage::handleEvent(const SDL_Event& event)
if (searchActive_) return; if (searchActive_) return;
if (!visible_.empty()) { if (!visible_.empty()) {
const FlatItem& item = items_[visible_[highlightIndex_]]; const FlatItem& item = items_[visible_[highlightIndex_]];
if ((item.type == FlatItem::Type::RootCategory || if (item.type == FlatItem::Type::RootCategory && item.expanded) {
item.type == FlatItem::Type::SubCategory) && item.expanded) {
collapseCurrent(); collapseCurrent();
} else { } else {
moveToParent(); moveToParent();
@@ -655,14 +654,18 @@ void ProclamationOptionsPage::render(SDL_Renderer* renderer)
SDL_RenderTexture(renderer, emptyHintText_.texture, nullptr, &dst); SDL_RenderTexture(renderer, emptyHintText_.texture, nullptr, &dst);
} }
} else { } else {
// Compute the widest row so the block can be centered. // Stable width: compute from every result so the list doesn't shift.
float maxTotalWidth = 0.0f; float maxTotalWidth = 0.0f;
for (const auto& r : searchResults_) {
int w = 0, h = 0;
TTF_GetStringSize(fontItem_, proclamations_[r].title.c_str(), 0, &w, &h);
maxTotalWidth = std::max(maxTotalWidth, static_cast<float>(w));
}
std::vector<float> textWidths(static_cast<size_t>(listSize)); std::vector<float> textWidths(static_cast<size_t>(listSize));
for (int i = scrollOffset_; i < scrollOffset_ + visibleCount_ && i < listSize; ++i) { for (int i = scrollOffset_; i < scrollOffset_ + visibleCount_ && i < listSize; ++i) {
const std::string& label = proclamations_[searchResults_[i]].title;
int w = 0, h = 0; int w = 0, h = 0;
TTF_GetStringSize(fontItem_, label.c_str(), 0, &w, &h); TTF_GetStringSize(fontItem_, proclamations_[searchResults_[i]].title.c_str(), 0, &w, &h);
maxTotalWidth = std::max(maxTotalWidth, static_cast<float>(w));
textWidths[i] = static_cast<float>(w); textWidths[i] = static_cast<float>(w);
} }
float sharedX = (fw - maxTotalWidth) / 2.0f; float sharedX = (fw - maxTotalWidth) / 2.0f;
@@ -695,19 +698,29 @@ void ProclamationOptionsPage::render(SDL_Renderer* renderer)
} }
} else { } else {
// --- Browse tree --- // --- Browse tree ---
// Compute the widest row so the block can be centered. // Stable width: compute from every item so the list doesn't
// shift left/right when groups expand or collapse.
float maxTotalWidth = 0.0f; float maxTotalWidth = 0.0f;
std::vector<float> textWidths(visible_.size()); for (const auto& item : items_) {
for (int i = scrollOffset_; i < scrollOffset_ + visibleCount_ && i < static_cast<int>(visible_.size()); ++i) {
const FlatItem& item = items_[visible_[i]];
std::string label = item.label; std::string label = item.label;
if (item.type == FlatItem::Type::RootCategory || item.type == FlatItem::Type::SubCategory) { if (item.type == FlatItem::Type::RootCategory) {
label = (item.expanded ? "[-] " : "[+] ") + label; label = (item.expanded ? "[-] " : "[+] ") + label;
} }
int w = 0, h = 0; int w = 0, h = 0;
TTF_GetStringSize(fontItem_, label.c_str(), 0, &w, &h); TTF_GetStringSize(fontItem_, label.c_str(), 0, &w, &h);
float total = item.depth * indent + static_cast<float>(w); float total = item.depth * indent + static_cast<float>(w);
maxTotalWidth = std::max(maxTotalWidth, total); maxTotalWidth = std::max(maxTotalWidth, total);
}
std::vector<float> textWidths(visible_.size());
for (int i = scrollOffset_; i < scrollOffset_ + visibleCount_ && i < static_cast<int>(visible_.size()); ++i) {
const FlatItem& item = items_[visible_[i]];
std::string label = item.label;
if (item.type == FlatItem::Type::RootCategory) {
label = (item.expanded ? "[-] " : "[+] ") + label;
}
int w = 0, h = 0;
TTF_GetStringSize(fontItem_, label.c_str(), 0, &w, &h);
textWidths[i] = static_cast<float>(w); textWidths[i] = static_cast<float>(w);
} }
@@ -721,7 +734,7 @@ void ProclamationOptionsPage::render(SDL_Renderer* renderer)
float y = listTop + rowIndex * rowPitch; float y = listTop + rowIndex * rowPitch;
std::string label = item.label; std::string label = item.label;
if (item.type == FlatItem::Type::RootCategory || item.type == FlatItem::Type::SubCategory) { if (item.type == FlatItem::Type::RootCategory) {
label = (item.expanded ? "[-] " : "[+] ") + label; label = (item.expanded ? "[-] " : "[+] ") + label;
} }
+18 -18
View File
@@ -19,18 +19,18 @@ namespace
{ {
constexpr int MENU_ITEM_COUNT = 9; constexpr int MENU_ITEM_COUNT = 9;
// 0 = WiFi Setup, 1 = Audio Options, 2 = Time Zone Setup, 3 = Proclamations, // 0 = Proclamations, 1 = Audio Options, 2 = Email Address, 3 = License Key,
// 4 = Email Address, 5 = License Key, 6 = Date Format, // 4 = WiFi Setup, 5 = Time Zone Setup, 6 = Date Format,
// 7 = View System Logs, 8 = Back // 7 = System Logs, 8 = Back
constexpr const char *MENU_LABELS[MENU_ITEM_COUNT] = { constexpr const char *MENU_LABELS[MENU_ITEM_COUNT] = {
"WiFi Setup",
"Audio Options",
"Time Zone Setup",
"Proclamations", "Proclamations",
"Audio Options",
"Email Address", "Email Address",
"License Key", "License Key",
"WiFi Setup",
"Time Zone Setup",
"Date Format", "Date Format",
"View System Logs", "System Logs",
"Back to Home", "Back to Home",
}; };
@@ -78,15 +78,6 @@ void Settings::handleEvent(const SDL_Event &event)
switch (selectedItem_) switch (selectedItem_)
{ {
case 0: case 0:
state_->pages.push(*state_, std::make_unique<WiFiSetupPage>());
break;
case 1:
state_->pages.push(*state_, std::make_unique<AudioSettingsPage>());
break;
case 2:
state_->pages.push(*state_, std::make_unique<TimeZonePage>());
break;
case 3:
// The Proclamation list is license-gated like Play. // The Proclamation list is license-gated like Play.
if (state_->licenseTracker.isValid()) if (state_->licenseTracker.isValid())
{ {
@@ -97,12 +88,21 @@ void Settings::handleEvent(const SDL_Event &event)
state_->pages.push(*state_, std::make_unique<LicenseBlockedPage>()); state_->pages.push(*state_, std::make_unique<LicenseBlockedPage>());
} }
break; break;
case 4: case 1:
state_->pages.push(*state_, std::make_unique<AudioSettingsPage>());
break;
case 2:
state_->pages.push(*state_, std::make_unique<EmailAddressPage>()); state_->pages.push(*state_, std::make_unique<EmailAddressPage>());
break; break;
case 5: case 3:
state_->pages.push(*state_, std::make_unique<LicenseKeyPage>()); state_->pages.push(*state_, std::make_unique<LicenseKeyPage>());
break; break;
case 4:
state_->pages.push(*state_, std::make_unique<WiFiSetupPage>());
break;
case 5:
state_->pages.push(*state_, std::make_unique<TimeZonePage>());
break;
case 6: case 6:
state_->pages.push(*state_, std::make_unique<DateFormatPage>()); state_->pages.push(*state_, std::make_unique<DateFormatPage>());
break; break;