7 Commits
Author SHA1 Message Date
NeilClack 43cd93bbd7 Switch from window-size to render-output-size 2026-08-25 11:30:20 -05:00
NeilClack 92ba9f39fb Switch from window-size to render-output-size 2026-08-25 11:28:02 -05:00
NeilClack 400c6a782b Scroller: use full window height on rotated/tall displays
build-dev / nix-build (push) Successful in 41s
Override the LETTERBOX logical presentation while the scroller is
active so it renders into the full physical window.  On a 1920x6480
rotated display the scroller now scrolls through all 6480 pixels of
vertical space instead of being confined to the 1920x1080 letterbox
region.  Text scale is unchanged (same font size, same pixel
rasterization); more lines are simply visible at once.

- onPush: save current logical presentation, switch to DISABLED with
  real window dimensions
- onPop: restore the saved LETTERBOX presentation
- resize handler in main.cpp: skip re-apply while scroller is active
  (it owns its own presentation and restores on pop)
2026-08-25 10:55:53 -05:00
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
7 changed files with 701 additions and 738 deletions
+13 -5
View File
@@ -41,10 +41,17 @@ jobs:
# 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:
@@ -59,10 +66,11 @@ jobs:
./build/test_license_service ./build/test_license_service
' '
# Release closure lands in the release cache. # Release closure (the actual package, not the check output) lands in the
# release cache.
- name: Push to Attic release cache - name: Push to Attic release cache
env: env:
ATTIC_TOKEN: ${{ secrets.ATTIC_TOKEN_PROD }} ATTIC_TOKEN: ${{ secrets.ATTIC_TOKEN_PROD }}
run: | run: |
attic login radiantwave https://attic.radiantwavetech.com "$ATTIC_TOKEN" attic login radiantwave https://attic.radiantwavetech.com "$ATTIC_TOKEN"
attic push radiantwave:radiantwave ./result attic push radiantwave:radiantwave ./result-pkg
+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;)
{ {
+6 -1
View File
@@ -7,6 +7,7 @@
#include "logical_size.h" #include "logical_size.h"
#include "logger/logger.h" #include "logger/logger.h"
#include "provisioning.h" #include "provisioning.h"
#include "scroller_page.h"
namespace { namespace {
// How long audio fades out before the app tears down on quit. Only applied // How long audio fades out before the app tears down on quit. Only applied
@@ -201,12 +202,16 @@ SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event) {
std::to_string(event->window.data1) + "x" + std::to_string(event->window.data1) + "x" +
std::to_string(event->window.data2)); std::to_string(event->window.data2));
// Logical presentation persists across resizes; re-apply defensively // Logical presentation persists across resizes; re-apply defensively
// in case backend state was reset underneath us. // in case backend state was reset underneath us. Skip while the
// scroller is active — it owns its own presentation (NONE, full
// window) and restores LETTERBOX in onPop.
if (!dynamic_cast<ScrollerPage *>(state.pages.current())) {
SDL_SetRenderLogicalPresentation(state.renderer, SDL_SetRenderLogicalPresentation(state.renderer,
radiantwave::kDesignWidth, radiantwave::kDesignWidth,
radiantwave::kDesignHeight, radiantwave::kDesignHeight,
SDL_LOGICAL_PRESENTATION_LETTERBOX); 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;
} }
+203 -270
View File
@@ -2,27 +2,26 @@
#include "app_state.h" #include "app_state.h"
#include "audio/audio_downloader.h" #include "audio/audio_downloader.h"
#include "logical_size.h"
#include "proclamation_options_page.h" #include "proclamation_options_page.h"
#include "scramble/fibonacci_scrambler.h" #include "scramble/fibonacci_scrambler.h"
#include "utf8_utils.h" #include "utf8_utils.h"
#include "logical_size.h"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h> #include <SDL3_ttf/SDL_ttf.h>
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <cstdint>
#include <cstdio> #include <cstdio>
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
#include <cstdint>
#include <filesystem> #include <filesystem>
#include <vector> #include <vector>
using radiantwave::FibonacciLinePattern; using radiantwave::FibonacciLinePattern;
using radiantwave::scroller::ProclamationStream; using radiantwave::scroller::ProclamationStream;
namespace namespace {
{
constexpr float kFadeSeconds = 5.0f; constexpr float kFadeSeconds = 5.0f;
constexpr float kVelocityMin = -3500.0f; constexpr float kVelocityMin = -3500.0f;
@@ -30,17 +29,41 @@ namespace
} // anonymous namespace } // anonymous namespace
void ScrollerPage::onPush(AppState &state) void ScrollerPage::onPush(AppState &state) {
{
state_ = &state; state_ = &state;
int winW = 0; // Save the current logical presentation (LETTERBOX at design resolution)
int winH = 0; // and switch to NONE so we render into the full physical window. This
radiantwave::getLayoutSize(state.renderer, &winW, &winH); // lets the scroller scroll the entire vertical extent of tall/rotated
screenW_ = static_cast<float>(winW); // displays while keeping text at the same pixel scale. onPop restores
screenH_ = static_cast<float>(winH); // the original presentation for other pages.
// Lines are capped at the full screen width; the widest Fibonacci lines are SDL_GetRenderLogicalPresentation(state.renderer, &savedLogicalW_,
// sized to fill it with whole words (peak word count derived from the &savedLogicalH_, &savedMode_);
// Use the renderer's actual output size in pixels, not the window size in
// points. SDL_GetWindowSize() returns screen coordinates, which can be
// smaller than the real drawable if the display has a content scale
// applied (common on some monitors/compositors) — that mismatch caused
// this page to lay out and center within a sub-region of the screen
// instead of the full physical canvas. SDL_GetRenderOutputSize() gives us
// the true pixel dimensions of whatever we're actually rendering into.
int windowW = 0;
int windowH = 0;
SDL_GetRenderOutputSize(state.renderer, &windowW, &windowH);
if (windowW <= 0) {
windowW = radiantwave::kDesignWidth;
}
if (windowH <= 0) {
windowH = radiantwave::kDesignHeight;
}
SDL_SetRenderLogicalPresentation(state.renderer, windowW, windowH,
SDL_LOGICAL_PRESENTATION_DISABLED);
screenW_ = static_cast<float>(windowW);
screenH_ = static_cast<float>(windowH);
// Lines are capped at the full screen width; the widest Fibonacci lines
// are sized to fill it with whole words (peak word count derived from the
// screen), independent of resolution. // screen), independent of resolution.
maxLineWidth_ = screenW_; maxLineWidth_ = screenW_;
@@ -58,63 +81,59 @@ void ScrollerPage::onPush(AppState &state)
// plays. A frequency file that is not on disk yet falls back to the manual // plays. A frequency file that is not on disk yet falls back to the manual
// track rather than playing nothing. // track rather than playing nothing.
std::string track = state.config.get().audio.current_track; std::string track = state.config.get().audio.current_track;
if (state.config.get().audio.dynamic_audio && !streams_.empty()) if (state.config.get().audio.dynamic_audio && !streams_.empty()) {
{
const ProclamationStream &stream = streams_[streamIndex_]; const ProclamationStream &stream = streams_[streamIndex_];
if (stream.solfeggio_frequency_id != 0 && !stream.audioFilename.empty() && if (stream.solfeggio_frequency_id != 0 &&
std::filesystem::exists(audioFilePath(stream.audioFilename))) !stream.audioFilename.empty() &&
{ std::filesystem::exists(audioFilePath(stream.audioFilename))) {
track = stream.audioFilename; track = stream.audioFilename;
} }
} }
if (std::filesystem::exists(audioFilePath(track))) if (std::filesystem::exists(audioFilePath(track))) {
{
state.audio.playTrackFadeIn(track, kFadeSeconds); state.audio.playTrackFadeIn(track, kFadeSeconds);
} }
state.keybinds.bind(SDLK_ESCAPE, "scroller_exit", [this]() state.keybinds.bind(SDLK_ESCAPE, "scroller_exit", [this]() {
{
if (state_) { if (state_) {
state_->pages.pop(*state_); state_->pages.pop(*state_);
} }); }
});
} }
void ScrollerPage::onResume(AppState &state) void ScrollerPage::onResume(AppState &state) {
{
state_ = &state; state_ = &state;
createFont(state); createFont(state);
createPatterns(); createPatterns();
rebuildWordTextures(); rebuildWordTextures();
state.keybinds.bind(SDLK_ESCAPE, "scroller_exit", [this]() state.keybinds.bind(SDLK_ESCAPE, "scroller_exit", [this]() {
{
if (state_) { if (state_) {
state_->pages.pop(*state_); state_->pages.pop(*state_);
} }); }
});
} }
void ScrollerPage::onPause(AppState &state) void ScrollerPage::onPause(AppState &state) {
{
state.keybinds.unbindAll(); state.keybinds.unbindAll();
destroyResources(); destroyResources();
} }
void ScrollerPage::onPop(AppState &state) void ScrollerPage::onPop(AppState &state) {
{ // Restore the original logical presentation so other pages (Home,
// Settings, etc.) render in the design-resolution LETTERBOX space.
SDL_SetRenderLogicalPresentation(state.renderer, savedLogicalW_,
savedLogicalH_, savedMode_);
state.audio.stopTrackFadeOut(kFadeSeconds); state.audio.stopTrackFadeOut(kFadeSeconds);
state.keybinds.unbindAll(); state.keybinds.unbindAll();
destroyResources(); destroyResources();
TTF_Quit(); TTF_Quit();
} }
void ScrollerPage::handleEvent(const SDL_Event &) void ScrollerPage::handleEvent(const SDL_Event &) {}
{
}
void ScrollerPage::update(float dt) void ScrollerPage::update(float dt) {
{ if (state_) {
if (state_)
{
state_->audio.update(dt); state_->audio.update(dt);
} }
@@ -122,69 +141,55 @@ void ScrollerPage::update(float dt)
scrollOffset_ += velocity_ * dt; scrollOffset_ += velocity_ * dt;
float move = std::floor(scrollOffset_); float move = std::floor(scrollOffset_);
if (move != 0.0f) if (move != 0.0f) {
{
scrollOffset_ -= move; scrollOffset_ -= move;
for (auto &line : lines_) for (auto &line : lines_) {
{ for (auto &w : line.words) {
for (auto &w : line.words)
{
w.y += move; w.y += move;
} }
} }
} }
if (lines_.empty()) if (lines_.empty()) {
{
return; return;
} }
if (velocity_ > 0.0f) if (velocity_ > 0.0f) {
{
// Lines move downward. Recycle every line that scrolled fully off the // Lines move downward. Recycle every line that scrolled fully off the
// bottom this frame (movement is continuous, so one frame can cover // bottom this frame (movement is continuous, so one frame can cover
// several line heights) and add a replacement just above the top. // several line heights) and add a replacement just above the top.
while (!lines_.empty() && lines_.back().words[0].y > screenH_) while (!lines_.empty() && lines_.back().words[0].y > screenH_) {
{
float baseY = lines_.front().words[0].y - lineHeight_; float baseY = lines_.front().words[0].y - lineHeight_;
lines_.pop_back(); lines_.pop_back();
if (!addLineAt(baseY)) if (!addLineAt(baseY)) {
{
break; break;
} }
} }
} } else if (velocity_ < 0.0f) {
else if (velocity_ < 0.0f)
{
// Lines move upward. Recycle every line that scrolled fully off the top // Lines move upward. Recycle every line that scrolled fully off the top
// this frame and add a replacement just below the bottom. // this frame and add a replacement just below the bottom.
while (!lines_.empty() && while (!lines_.empty() &&
lines_.front().words[0].y + lines_.front().words[0].height < 0.0f) lines_.front().words[0].y + lines_.front().words[0].height <
{ 0.0f) {
float baseY = lines_.back().words[0].y + lineHeight_; float baseY = lines_.back().words[0].y + lineHeight_;
lines_.pop_front(); lines_.pop_front();
if (!addLineAt(baseY)) if (!addLineAt(baseY)) {
{
break; break;
} }
} }
} }
} }
void ScrollerPage::render(SDL_Renderer *renderer) void ScrollerPage::render(SDL_Renderer *renderer) {
{
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0); SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
SDL_RenderClear(renderer); SDL_RenderClear(renderer);
for (const auto &line : lines_) for (const auto &line : lines_) {
{ if (line.words.empty()) {
if (line.words.empty())
{
continue; continue;
} }
float startX = std::floor((screenW_ - line.width) / 2.0f); float startX = std::floor((screenW_ - line.width) / 2.0f);
for (const auto &w : line.words) for (const auto &w : line.words) {
{
SDL_SetTextureColorMod(w.texture, line.r, line.g, line.b); SDL_SetTextureColorMod(w.texture, line.r, line.g, line.b);
SDL_FRect dst{startX + w.x, w.y, w.width, w.height}; SDL_FRect dst{startX + w.x, w.y, w.width, w.height};
SDL_RenderTexture(renderer, w.texture, nullptr, &dst); SDL_RenderTexture(renderer, w.texture, nullptr, &dst);
@@ -192,53 +197,46 @@ void ScrollerPage::render(SDL_Renderer *renderer)
} }
} }
void ScrollerPage::buildStreams(AppState &state) void ScrollerPage::buildStreams(AppState &state) {
{
streams_.clear(); streams_.clear();
fallbackPalette_.clear(); fallbackPalette_.clear();
lines_.clear(); lines_.clear();
streamIndex_ = 0; streamIndex_ = 0;
auto proclamations = state.db.getActiveProclamations(); auto proclamations = state.db.getActiveProclamations();
if (proclamations.empty()) if (proclamations.empty()) {
{
return; return;
} }
for (const auto &c : state.db.getPaletteForFrequencyId(5)) for (const auto &c :
{ // 528 Hz palette state.db.getPaletteForFrequencyId(5)) { // 528 Hz palette
fallbackPalette_.push_back({c.r, c.g, c.b, c.a}); fallbackPalette_.push_back({c.r, c.g, c.b, c.a});
} }
std::unordered_map<int, std::vector<radiantwave::scroller::StreamColor>> palettesByFreqId; std::unordered_map<int, std::vector<radiantwave::scroller::StreamColor>>
for (const auto &rec : proclamations) palettesByFreqId;
{ for (const auto &rec : proclamations) {
int freqId = rec.solfeggio_frequency_id; int freqId = rec.solfeggio_frequency_id;
if (palettesByFreqId.count(freqId) != 0) if (palettesByFreqId.count(freqId) != 0) {
{
continue; continue;
} }
std::vector<radiantwave::scroller::StreamColor> colors; std::vector<radiantwave::scroller::StreamColor> colors;
for (const auto &c : state.db.getPaletteForFrequencyId(freqId)) for (const auto &c : state.db.getPaletteForFrequencyId(freqId)) {
{
colors.push_back({c.r, c.g, c.b, c.a}); colors.push_back({c.r, c.g, c.b, c.a});
} }
palettesByFreqId[freqId] = std::move(colors); palettesByFreqId[freqId] = std::move(colors);
} }
std::unordered_map<int, radiantwave::db::AudioFileRecord> audioByFreqId; std::unordered_map<int, radiantwave::db::AudioFileRecord> audioByFreqId;
for (const auto &af : state.db.getAudioFiles()) for (const auto &af : state.db.getAudioFiles()) {
{
int freqId = af.solfeggio_frequency_id; int freqId = af.solfeggio_frequency_id;
// Self-heal for databases seeded from a legacy manifest that left the // Self-heal for databases seeded from a legacy manifest that left the
// frequency id at 0 for every row: recover the id from the filename. // frequency id at 0 for every row: recover the id from the filename.
if (freqId == 0) if (freqId == 0) {
{
freqId = radiantwave::audio::frequencyIdForHz( freqId = radiantwave::audio::frequencyIdForHz(
radiantwave::audio::hzFromFilename(af.filename)); radiantwave::audio::hzFromFilename(af.filename));
} }
if (freqId != 0) if (freqId != 0) {
{
audioByFreqId[freqId] = af; audioByFreqId[freqId] = af;
} }
} }
@@ -248,10 +246,8 @@ void ScrollerPage::buildStreams(AppState &state)
DEFAULT_PROPLEAMATION_ID); DEFAULT_PROPLEAMATION_ID);
} }
void ScrollerPage::createFont(AppState &state) void ScrollerPage::createFont(AppState &state) {
{ if (font_) {
if (font_)
{
TTF_CloseFont(font_); TTF_CloseFont(font_);
font_ = nullptr; font_ = nullptr;
} }
@@ -262,46 +258,41 @@ void ScrollerPage::createFont(AppState &state)
clearCharTextures(); clearCharTextures();
fontPxSize_ = state.config.get().proclamation.font_size; fontPxSize_ = state.config.get().proclamation.font_size;
if (fontPxSize_ <= 0) if (fontPxSize_ <= 0) {
{
fontPxSize_ = 32; fontPxSize_ = 32;
} }
font_ = TTF_OpenFont("assets/fonts/Roboto-Regular.ttf", fontPxSize_); font_ = TTF_OpenFont("assets/fonts/Roboto-Regular.ttf", fontPxSize_);
if (!font_) if (!font_) {
{
return; return;
} }
int w = 0; int w = 0;
int h = 0; int h = 0;
TTF_GetStringSize(font_, "Mg", 0, &w, &h); TTF_GetStringSize(font_, "Mg", 0, &w, &h);
lineHeight_ = h > 0 ? static_cast<float>(h) : static_cast<float>(fontPxSize_); lineHeight_ =
h > 0 ? static_cast<float>(h) : static_cast<float>(fontPxSize_);
spaceWidth_ = measureWidth(" "); spaceWidth_ = measureWidth(" ");
if (spaceWidth_ <= 0.0f) if (spaceWidth_ <= 0.0f) {
{
spaceWidth_ = static_cast<float>(fontPxSize_) * 0.5f; spaceWidth_ = static_cast<float>(fontPxSize_) * 0.5f;
} }
} }
void ScrollerPage::createPatterns() void ScrollerPage::createPatterns() {
{
patterns_.clear(); patterns_.clear();
patterns_.reserve(streams_.size()); patterns_.reserve(streams_.size());
for (size_t i = 0; i < streams_.size(); ++i) for (size_t i = 0; i < streams_.size(); ++i) {
{
int peak = computePeakWordCount(streams_[i].words); int peak = computePeakWordCount(streams_[i].words);
patterns_.emplace_back(peak, maxLineWidth_, spaceWidth_, patterns_.emplace_back(
[this](const std::string &word) peak, maxLineWidth_, spaceWidth_,
{ return measureWidth(word); }); [this](const std::string &word) { return measureWidth(word); });
} }
} }
int ScrollerPage::computePeakWordCount(const std::vector<std::string> &words) const int ScrollerPage::computePeakWordCount(
{ const std::vector<std::string> &words) const {
if (words.empty() || screenW_ <= 0.0f) if (words.empty() || screenW_ <= 0.0f) {
{
return 1; return 1;
} }
@@ -309,14 +300,12 @@ int ScrollerPage::computePeakWordCount(const std::vector<std::string> &words) co
// spacing. The peak is the number of whole words that fit across the // spacing. The peak is the number of whole words that fit across the
// screen, so the widest Fibonacci lines fill the width at natural spacing. // screen, so the widest Fibonacci lines fill the width at natural spacing.
double total = 0.0; double total = 0.0;
for (const auto &w : words) for (const auto &w : words) {
{
total += measureWidth(w); total += measureWidth(w);
} }
double n = static_cast<double>(words.size()); double n = static_cast<double>(words.size());
double perWord = (total + (n - 1.0) * spaceWidth_) / n; double perWord = (total + (n - 1.0) * spaceWidth_) / n;
if (perWord <= 0.0) if (perWord <= 0.0) {
{
perWord = 1.0; perWord = 1.0;
} }
@@ -324,45 +313,36 @@ int ScrollerPage::computePeakWordCount(const std::vector<std::string> &words) co
return fit < 1 ? 1 : fit; return fit < 1 ? 1 : fit;
} }
void ScrollerPage::destroyResources() void ScrollerPage::destroyResources() {
{
clearCharTextures(); clearCharTextures();
for (auto &entry : wordTextures_) for (auto &entry : wordTextures_) {
{ if (entry.second.texture) {
if (entry.second.texture)
{
SDL_DestroyTexture(entry.second.texture); SDL_DestroyTexture(entry.second.texture);
} }
} }
wordTextures_.clear(); wordTextures_.clear();
if (font_) if (font_) {
{
TTF_CloseFont(font_); TTF_CloseFont(font_);
font_ = nullptr; font_ = nullptr;
} }
} }
void ScrollerPage::rebuildWordTextures() void ScrollerPage::rebuildWordTextures() {
{
wordTextures_.clear(); wordTextures_.clear();
for (auto &line : lines_) for (auto &line : lines_) {
{
float width = 0.0f; float width = 0.0f;
for (auto &item : line.words) for (auto &item : line.words) {
{
ensureWordTexture(item.text); ensureWordTexture(item.text);
const CachedWord &cached = wordTextures_[item.text]; const CachedWord &cached = wordTextures_[item.text];
if (!cached.texture) if (!cached.texture) {
{
continue; continue;
} }
item.texture = cached.texture; item.texture = cached.texture;
item.width = cached.width; item.width = cached.width;
item.height = cached.height; item.height = cached.height;
if (width > 0.0f) if (width > 0.0f) {
{
width += spaceWidth_; width += spaceWidth_;
} }
item.x = width; item.x = width;
@@ -372,31 +352,25 @@ void ScrollerPage::rebuildWordTextures()
} }
} }
void ScrollerPage::prefillLines() void ScrollerPage::prefillLines() {
{ if (streams_.empty() || lineHeight_ <= 0.0f) {
if (streams_.empty() || lineHeight_ <= 0.0f)
{
return; return;
} }
int n = static_cast<int>(std::ceil(screenH_ / lineHeight_)) + 2; int n = static_cast<int>(std::ceil(screenH_ / lineHeight_)) + 2;
for (int i = 0; i < n; ++i) for (int i = 0; i < n; ++i) {
{
float baseY = -lineHeight_ + static_cast<float>(i) * lineHeight_; float baseY = -lineHeight_ + static_cast<float>(i) * lineHeight_;
addLineAt(baseY); addLineAt(baseY);
} }
} }
bool ScrollerPage::addLineAt(float baseY) bool ScrollerPage::addLineAt(float baseY) {
{ if (streams_.empty() || !font_) {
if (streams_.empty() || !font_)
{
return false; return false;
} }
ProclamationStream &stream = streams_[streamIndex_]; ProclamationStream &stream = streams_[streamIndex_];
if (stream.words.empty()) if (stream.words.empty()) {
{
advanceStream(); advanceStream();
return false; return false;
} }
@@ -404,7 +378,8 @@ bool ScrollerPage::addLineAt(float baseY)
FibonacciLinePattern &pattern = patterns_[streamIndex_]; FibonacciLinePattern &pattern = patterns_[streamIndex_];
// Capture palette state before any stream advance. // Capture palette state before any stream advance.
std::vector<radiantwave::scroller::StreamColor> paletteCopy = stream.palette; std::vector<radiantwave::scroller::StreamColor> paletteCopy =
stream.palette;
int lineColorIndex = stream.colorIndex; int lineColorIndex = stream.colorIndex;
stream.colorIndex++; stream.colorIndex++;
@@ -414,17 +389,15 @@ bool ScrollerPage::addLineAt(float baseY)
stream.wordsPlaced += placed; stream.wordsPlaced += placed;
// First word of a fresh pass is the "start" of the proclamation; print to // First word of a fresh pass is the "start" of the proclamation; print to
// stdout once so the console shows the current title while it plays. // stdout once so the console shows the current title while it plays.
if (stream.wordsPlaced == placed) if (stream.wordsPlaced == placed) {
{
std::printf("Playing %s\n", stream.name.c_str()); std::printf("Playing %s\n", stream.name.c_str());
} }
bool advanced = false; bool advanced = false;
while (!advanced && !stream.words.empty() && stream.wordsPlaced >= static_cast<int>(stream.words.size())) while (!advanced && !stream.words.empty() &&
{ stream.wordsPlaced >= static_cast<int>(stream.words.size())) {
stream.wordsPlaced -= static_cast<int>(stream.words.size()); stream.wordsPlaced -= static_cast<int>(stream.words.size());
stream.completed++; stream.completed++;
if (stream.completed >= stream.repeat) if (stream.completed >= stream.repeat) {
{
stream.completed = 0; stream.completed = 0;
advanceStream(); advanceStream();
advanced = true; advanced = true;
@@ -433,12 +406,10 @@ bool ScrollerPage::addLineAt(float baseY)
ScrollerLine line; ScrollerLine line;
float width = 0.0f; float width = 0.0f;
for (const auto &text : wordStrs) for (const auto &text : wordStrs) {
{
ensureWordTexture(text); ensureWordTexture(text);
const CachedWord &cached = wordTextures_[text]; const CachedWord &cached = wordTextures_[text];
if (!cached.texture) if (!cached.texture) {
{
continue; continue;
} }
WordItem item; WordItem item;
@@ -447,8 +418,7 @@ bool ScrollerPage::addLineAt(float baseY)
item.width = cached.width; item.width = cached.width;
item.height = cached.height; item.height = cached.height;
item.y = baseY; item.y = baseY;
if (width > 0.0f) if (width > 0.0f) {
{
width += spaceWidth_; width += spaceWidth_;
} }
item.x = width; item.x = width;
@@ -456,77 +426,63 @@ bool ScrollerPage::addLineAt(float baseY)
line.words.push_back(item); line.words.push_back(item);
} }
if (line.words.empty()) if (line.words.empty()) {
{
return false; return false;
} }
line.width = width; line.width = width;
if (!paletteCopy.empty()) if (!paletteCopy.empty()) {
{ const auto &c =
const auto &c = paletteCopy[lineColorIndex % static_cast<int>(paletteCopy.size())]; paletteCopy[lineColorIndex % static_cast<int>(paletteCopy.size())];
line.r = c.r; line.r = c.r;
line.g = c.g; line.g = c.g;
line.b = c.b; line.b = c.b;
} }
if (!lines_.empty() && baseY < lines_.front().words[0].y) if (!lines_.empty() && baseY < lines_.front().words[0].y) {
{
lines_.push_front(std::move(line)); lines_.push_front(std::move(line));
} } else {
else
{
lines_.push_back(std::move(line)); lines_.push_back(std::move(line));
} }
return true; return true;
} }
void ScrollerPage::advanceStream() void ScrollerPage::advanceStream() {
{ if (streams_.empty()) {
if (streams_.empty())
{
return; return;
} }
streamIndex_ = (streamIndex_ + 1) % static_cast<int>(streams_.size()); streamIndex_ = (streamIndex_ + 1) % static_cast<int>(streams_.size());
// Diagnostic: report the palette the incoming stream will render so the // Diagnostic: report the palette the incoming stream will render so the
// console shows whether colors should change when this stream starts. // console shows whether colors should change when this stream starts.
const ProclamationStream &stream = streams_[streamIndex_]; const ProclamationStream &stream = streams_[streamIndex_];
if (!stream.palette.empty()) if (!stream.palette.empty()) {
{
const auto &c = stream.palette[0]; const auto &c = stream.palette[0];
std::printf("[Scroller] -> %s freqId=%d palette[0]=#%02X%02X%02X\n", std::printf("[Scroller] -> %s freqId=%d palette[0]=#%02X%02X%02X\n",
stream.name.c_str(), stream.solfeggio_frequency_id, stream.name.c_str(), stream.solfeggio_frequency_id, c.r,
c.r, c.g, c.b); c.g, c.b);
} } else {
else
{
std::printf("[Scroller] -> %s freqId=%d palette=EMPTY\n", std::printf("[Scroller] -> %s freqId=%d palette=EMPTY\n",
stream.name.c_str(), stream.solfeggio_frequency_id); stream.name.c_str(), stream.solfeggio_frequency_id);
} }
switchAudioForCurrentStream(); switchAudioForCurrentStream();
} }
void ScrollerPage::switchAudioForCurrentStream() void ScrollerPage::switchAudioForCurrentStream() {
{ if (prefilling_ || streams_.empty() || !state_) {
if (prefilling_ || streams_.empty() || !state_)
{
return; return;
} }
// Dynamic audio only: with a fixed manual track selected the audio must // Dynamic audio only: with a fixed manual track selected the audio must
// never switch as proclamations change. // never switch as proclamations change.
if (!state_->config.get().audio.dynamic_audio) if (!state_->config.get().audio.dynamic_audio) {
{
return; return;
} }
const ProclamationStream &stream = streams_[streamIndex_]; const ProclamationStream &stream = streams_[streamIndex_];
if (stream.solfeggio_frequency_id == 0 || stream.audioFilename.empty()) if (stream.solfeggio_frequency_id == 0 || stream.audioFilename.empty()) {
{
return; // default stream: never touch audio return; // default stream: never touch audio
} }
if (!std::filesystem::exists(audioFilePath(stream.audioFilename))) if (!std::filesystem::exists(audioFilePath(stream.audioFilename))) {
{
return; // keep current track; retried on the next activation return; // keep current track; retried on the next activation
} }
// Crossfades over kFadeSeconds; a no-op when the requested track is the // Crossfades over kFadeSeconds; a no-op when the requested track is the
@@ -534,14 +490,11 @@ void ScrollerPage::switchAudioForCurrentStream()
state_->audio.playTrackCrossfade(stream.audioFilename, kFadeSeconds); state_->audio.playTrackCrossfade(stream.audioFilename, kFadeSeconds);
} }
void ScrollerPage::updateAutoVelocity(float dt) void ScrollerPage::updateAutoVelocity(float dt) {
{ switch (velocityState_) {
switch (velocityState_)
{
case 0: // ramping to the negative extreme case 0: // ramping to the negative extreme
velocity_ -= velocityStep_ * dt; velocity_ -= velocityStep_ * dt;
if (velocity_ <= kVelocityMin) if (velocity_ <= kVelocityMin) {
{
velocity_ = kVelocityMin; velocity_ = kVelocityMin;
velocityTimer_ = velocityDelay_; velocityTimer_ = velocityDelay_;
velocityState_ = 1; velocityState_ = 1;
@@ -549,15 +502,13 @@ void ScrollerPage::updateAutoVelocity(float dt)
break; break;
case 1: // holding at the negative extreme case 1: // holding at the negative extreme
velocityTimer_ -= dt; velocityTimer_ -= dt;
if (velocityTimer_ <= 0.0f) if (velocityTimer_ <= 0.0f) {
{
velocityState_ = 2; velocityState_ = 2;
} }
break; break;
case 2: // ramping to the positive extreme case 2: // ramping to the positive extreme
velocity_ += velocityStep_ * dt; velocity_ += velocityStep_ * dt;
if (velocity_ >= kVelocityMax) if (velocity_ >= kVelocityMax) {
{
velocity_ = kVelocityMax; velocity_ = kVelocityMax;
velocityTimer_ = velocityDelay_; velocityTimer_ = velocityDelay_;
velocityState_ = 3; velocityState_ = 3;
@@ -565,8 +516,7 @@ void ScrollerPage::updateAutoVelocity(float dt)
break; break;
case 3: // holding at the positive extreme case 3: // holding at the positive extreme
velocityTimer_ -= dt; velocityTimer_ -= dt;
if (velocityTimer_ <= 0.0f) if (velocityTimer_ <= 0.0f) {
{
velocityState_ = 0; velocityState_ = 0;
} }
break; break;
@@ -576,10 +526,8 @@ void ScrollerPage::updateAutoVelocity(float dt)
} }
} }
void ScrollerPage::ensureWordTexture(const std::string &word) void ScrollerPage::ensureWordTexture(const std::string &word) {
{ if (wordTextures_.count(word) != 0) {
if (wordTextures_.count(word) != 0)
{
return; return;
} }
@@ -602,69 +550,64 @@ void ScrollerPage::ensureWordTexture(const std::string &word)
// breaking, centering, and the Fibonacci line pattern are unchanged; only // breaking, centering, and the Fibonacci line pattern are unchanged; only
// the glyph pixels move. // the glyph pixels move.
CachedWord cached; CachedWord cached;
if (font_ && state_) if (font_ && state_) {
{
std::vector<uint32_t> codePoints; std::vector<uint32_t> codePoints;
if (radiantwave::splitUtf8(word, codePoints) && !codePoints.empty()) if (radiantwave::splitUtf8(word, codePoints) && !codePoints.empty()) {
{
std::vector<CharEntry *> glyphs; std::vector<CharEntry *> glyphs;
float totalWidth = 0.0f; float totalWidth = 0.0f;
float glyphHeight = 0.0f; float glyphHeight = 0.0f;
for (uint32_t cp : codePoints) for (uint32_t cp : codePoints) {
{
const std::string glyphText = radiantwave::encodeUtf8(cp); const std::string glyphText = radiantwave::encodeUtf8(cp);
if (glyphText.empty()) if (glyphText.empty()) {
{
continue; continue;
} }
ensureCharTexture(glyphText); ensureCharTexture(glyphText);
CharEntry &entry = charTextures_[glyphText]; CharEntry &entry = charTextures_[glyphText];
if (!entry.surface) if (!entry.surface) {
{
continue; continue;
} }
glyphs.push_back(&entry); glyphs.push_back(&entry);
totalWidth += entry.advance; totalWidth += entry.advance;
if (entry.glyphH > glyphHeight) if (entry.glyphH > glyphHeight) {
{
glyphHeight = entry.glyphH; glyphHeight = entry.glyphH;
} }
} }
if (!glyphs.empty()) if (!glyphs.empty()) {
{ const int wordW =
const int wordW = std::max(1, static_cast<int>(std::lround(totalWidth))); std::max(1, static_cast<int>(std::lround(totalWidth)));
const int wordH = std::max(1, static_cast<int>(std::lround(glyphHeight))); const int wordH =
SDL_Surface *wordSurface = SDL_CreateSurface(wordW, wordH, SDL_PIXELFORMAT_RGBA8888); std::max(1, static_cast<int>(std::lround(glyphHeight)));
if (wordSurface) SDL_Surface *wordSurface =
{ SDL_CreateSurface(wordW, wordH, SDL_PIXELFORMAT_RGBA8888);
if (wordSurface) {
// Every glyph surface carries the font's full line height // Every glyph surface carries the font's full line height
// (probe-verified), so y = 0 keeps every crop on the shared // (probe-verified), so y = 0 keeps every crop on the shared
// baseline and per-word textures stay line-height tall. // baseline and per-word textures stay line-height tall.
float x = 0.0f; float x = 0.0f;
for (const CharEntry *entry : glyphs) for (const CharEntry *entry : glyphs) {
{
SDL_Rect srcRect{0, 0, static_cast<int>(entry->glyphW), SDL_Rect srcRect{0, 0, static_cast<int>(entry->glyphW),
static_cast<int>(entry->glyphH)}; static_cast<int>(entry->glyphH)};
SDL_Rect dstRect{static_cast<int>(std::lround(x)), 0, SDL_Rect dstRect{static_cast<int>(std::lround(x)), 0,
static_cast<int>(entry->glyphW), static_cast<int>(entry->glyphW),
static_cast<int>(entry->glyphH)}; static_cast<int>(entry->glyphH)};
SDL_BlitSurface(entry->surface, &srcRect, wordSurface, &dstRect); SDL_BlitSurface(entry->surface, &srcRect, wordSurface,
&dstRect);
x += entry->advance; x += entry->advance;
} }
cached.texture = SDL_CreateTextureFromSurface(state_->renderer, wordSurface); cached.texture = SDL_CreateTextureFromSurface(
state_->renderer, wordSurface);
// The scroller steps glyphs in whole pixels, so bilinear // The scroller steps glyphs in whole pixels, so bilinear
// filtering can only smear them: nearest-neighbor keeps // filtering can only smear them: nearest-neighbor keeps
// every frame pixel-sharp. // every frame pixel-sharp.
if (cached.texture) if (cached.texture) {
{ SDL_SetTextureScaleMode(cached.texture,
SDL_SetTextureScaleMode(cached.texture, SDL_SCALEMODE_NEAREST); SDL_SCALEMODE_NEAREST);
} }
cached.width = static_cast<float>(wordSurface->w); cached.width = static_cast<float>(wordSurface->w);
cached.height = static_cast<float>(wordSurface->h); cached.height = static_cast<float>(wordSurface->h);
SDL_DestroySurface(wordSurface); SDL_DestroySurface(wordSurface);
if (!cached.texture) if (!cached.texture) {
{
cached.width = 0.0f; cached.width = 0.0f;
cached.height = 0.0f; cached.height = 0.0f;
} }
@@ -675,10 +618,8 @@ void ScrollerPage::ensureWordTexture(const std::string &word)
wordTextures_[word] = cached; wordTextures_[word] = cached;
} }
void ScrollerPage::ensureCharTexture(const std::string &glyph) void ScrollerPage::ensureCharTexture(const std::string &glyph) {
{ if (charTextures_.count(glyph) != 0) {
if (charTextures_.count(glyph) != 0)
{
return; return;
} }
@@ -687,25 +628,23 @@ void ScrollerPage::ensureCharTexture(const std::string &glyph)
// font size; createFont() clears the atlas whenever the font or its size // font size; createFont() clears the atlas whenever the font or its size
// changes, and pause/pop clear it as part of destroyResources(). // changes, and pause/pop clear it as part of destroyResources().
CharEntry entry; CharEntry entry;
if (font_ && state_) if (font_ && state_) {
{
SDL_Surface *surface = TTF_RenderText_Blended(font_, glyph.c_str(), 0, SDL_Surface *surface = TTF_RenderText_Blended(font_, glyph.c_str(), 0,
{255, 255, 255, 255}); {255, 255, 255, 255});
if (surface) if (surface) {
{
const int gW = surface->w; const int gW = surface->w;
const int gH = surface->h; const int gH = surface->h;
const int n = radiantwave::scramble::canvasSize(gW, gH); const int n = radiantwave::scramble::canvasSize(gW, gH);
if (n > 0) if (n > 0) {
{
// The scrambler needs tightly packed RGBA8888; a TTF surface's // The scrambler needs tightly packed RGBA8888; a TTF surface's
// pitch can exceed gW * 4, so copy row by row. // pitch can exceed gW * 4, so copy row by row.
std::vector<uint8_t> source(static_cast<size_t>(gW) * gH * 4); std::vector<uint8_t> source(static_cast<size_t>(gW) * gH * 4);
const uint8_t *pixels = static_cast<const uint8_t *>(surface->pixels); const uint8_t *pixels =
for (int y = 0; y < gH; ++y) static_cast<const uint8_t *>(surface->pixels);
{ for (int y = 0; y < gH; ++y) {
std::memcpy(source.data() + static_cast<size_t>(y) * gW * 4, std::memcpy(source.data() + static_cast<size_t>(y) * gW * 4,
pixels + static_cast<size_t>(y) * surface->pitch, pixels +
static_cast<size_t>(y) * surface->pitch,
static_cast<size_t>(gW) * 4); static_cast<size_t>(gW) * 4);
} }
@@ -714,25 +653,24 @@ void ScrollerPage::ensureCharTexture(const std::string &glyph)
// The fixed seed makes every device render the same glyph as // The fixed seed makes every device render the same glyph as
// the same scrambled pattern, with no key to persist or ship. // the same scrambled pattern, with no key to persist or ship.
radiantwave::scramble::ScrambleConfig config; radiantwave::scramble::ScrambleConfig config;
if (radiantwave::scramble::scramble(source.data(), scrambled.data(), if (radiantwave::scramble::scramble(
gW, gH, config)) source.data(), scrambled.data(), gW, gH, config)) {
{ SDL_Surface *tile =
SDL_Surface *tile = SDL_CreateSurface(n, n, SDL_PIXELFORMAT_RGBA8888); SDL_CreateSurface(n, n, SDL_PIXELFORMAT_RGBA8888);
if (tile) if (tile) {
{
uint8_t *dst = static_cast<uint8_t *>(tile->pixels); uint8_t *dst = static_cast<uint8_t *>(tile->pixels);
for (int y = 0; y < n; ++y) for (int y = 0; y < n; ++y) {
{ std::memcpy(dst + static_cast<size_t>(y) *
std::memcpy(dst + static_cast<size_t>(y) * tile->pitch, tile->pitch,
scrambled.data() + static_cast<size_t>(y) * n * 4, scrambled.data() +
static_cast<size_t>(y) * n * 4,
static_cast<size_t>(n) * 4); static_cast<size_t>(n) * 4);
} }
entry.surface = tile; entry.surface = tile;
entry.glyphW = static_cast<float>(gW); entry.glyphW = static_cast<float>(gW);
entry.glyphH = static_cast<float>(gH); entry.glyphH = static_cast<float>(gH);
entry.advance = measureWidth(glyph); entry.advance = measureWidth(glyph);
if (entry.advance <= 0.0f) if (entry.advance <= 0.0f) {
{
entry.advance = entry.glyphW; entry.advance = entry.glyphW;
} }
} }
@@ -744,22 +682,17 @@ void ScrollerPage::ensureCharTexture(const std::string &glyph)
charTextures_[glyph] = entry; charTextures_[glyph] = entry;
} }
void ScrollerPage::clearCharTextures() void ScrollerPage::clearCharTextures() {
{ for (auto &entry : charTextures_) {
for (auto &entry : charTextures_) if (entry.second.surface) {
{
if (entry.second.surface)
{
SDL_DestroySurface(entry.second.surface); SDL_DestroySurface(entry.second.surface);
} }
} }
charTextures_.clear(); charTextures_.clear();
} }
float ScrollerPage::measureWidth(const std::string &text) const float ScrollerPage::measureWidth(const std::string &text) const {
{ if (!font_) {
if (!font_)
{
return 0.0f; return 0.0f;
} }
int w = 0; int w = 0;
@@ -768,9 +701,9 @@ float ScrollerPage::measureWidth(const std::string &text) const
return static_cast<float>(w); return static_cast<float>(w);
} }
std::string ScrollerPage::audioFilePath(const std::string &filename) const std::string ScrollerPage::audioFilePath(const std::string &filename) const {
{ std::string path1 =
std::string path1 = std::string(std::getenv("HOME") ? std::getenv("HOME") : "/tmp") + std::string(std::getenv("HOME") ? std::getenv("HOME") : "/tmp") +
"/.local/radiantwave/audio/" + filename; "/.local/radiantwave/audio/" + filename;
std::string path2 = "assets/audio/" + filename; std::string path2 = "assets/audio/" + filename;
return std::filesystem::exists(path1) ? path1 : path2; return std::filesystem::exists(path1) ? path1 : path2;
+4
View File
@@ -115,4 +115,8 @@ private:
float velocityDelay_ = 2.5f; float velocityDelay_ = 2.5f;
int velocityState_ = 0; int velocityState_ = 0;
float velocityTimer_ = 0.0f; float velocityTimer_ = 0.0f;
int savedLogicalW_ = 0;
int savedLogicalH_ = 0;
SDL_RendererLogicalPresentation savedMode_ = SDL_LOGICAL_PRESENTATION_DISABLED;
}; };
+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;