Settings now shows Proclamations, Audio Options, System Settings, System Logs, and Back. The new SystemSettingsPage groups Email Address, License Key, WiFi Setup, Time Zone Setup, and Date Format, driven by the pure settings::systemSettingEntries() list so future entries need one row, not new menu plumbing. Adds unit tests for the entry list.
261 lines
7.4 KiB
C++
261 lines
7.4 KiB
C++
#include "settings.h"
|
|
#include "app_state.h"
|
|
#include "home.h"
|
|
#include "license_blocked_page.h"
|
|
#include "audio_settings_page.h"
|
|
#include "system_settings_page.h"
|
|
#include "log_viewer_page.h"
|
|
#include "proclamation_options_page.h"
|
|
#include "logical_size.h"
|
|
#include <SDL3/SDL.h>
|
|
#include <SDL3_image/SDL_image.h>
|
|
#include <algorithm>
|
|
|
|
namespace
|
|
{
|
|
|
|
constexpr int MENU_ITEM_COUNT = 5;
|
|
// 0 = Proclamations, 1 = Audio Options, 2 = System Settings,
|
|
// 3 = System Logs, 4 = Back
|
|
constexpr const char *MENU_LABELS[MENU_ITEM_COUNT] = {
|
|
"Proclamations",
|
|
"Audio Options",
|
|
"System Settings",
|
|
"System Logs",
|
|
"Back to Home",
|
|
};
|
|
|
|
} // anonymous namespace
|
|
|
|
void Settings::onPush(AppState &state)
|
|
{
|
|
state_ = &state;
|
|
TTF_Init();
|
|
createTextures(state);
|
|
|
|
state.keybinds.bind(SDLK_UP, "menu_up", [this]()
|
|
{ selectedItem_ = (selectedItem_ - 1 + MENU_ITEM_COUNT) % MENU_ITEM_COUNT; });
|
|
state.keybinds.bind(SDLK_DOWN, "menu_down", [this]()
|
|
{ selectedItem_ = (selectedItem_ + 1) % MENU_ITEM_COUNT; });
|
|
}
|
|
|
|
void Settings::onResume(AppState &state)
|
|
{
|
|
state_ = &state;
|
|
createTextures(state);
|
|
|
|
state.keybinds.bind(SDLK_UP, "menu_up", [this]()
|
|
{ selectedItem_ = (selectedItem_ - 1 + MENU_ITEM_COUNT) % MENU_ITEM_COUNT; });
|
|
state.keybinds.bind(SDLK_DOWN, "menu_down", [this]()
|
|
{ selectedItem_ = (selectedItem_ + 1) % MENU_ITEM_COUNT; });
|
|
}
|
|
|
|
void Settings::onPause(AppState &state)
|
|
{
|
|
state.keybinds.unbindAll();
|
|
destroyTextures();
|
|
}
|
|
|
|
void Settings::onPop(AppState &state)
|
|
{
|
|
destroyTextures();
|
|
TTF_Quit();
|
|
}
|
|
|
|
void Settings::handleEvent(const SDL_Event &event)
|
|
{
|
|
if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_RETURN)
|
|
{
|
|
switch (selectedItem_)
|
|
{
|
|
case 0:
|
|
// The Proclamation list is license-gated like Play.
|
|
if (state_->licenseTracker.isValid())
|
|
{
|
|
state_->pages.push(*state_, std::make_unique<ProclamationOptionsPage>());
|
|
}
|
|
else
|
|
{
|
|
state_->pages.push(*state_, std::make_unique<LicenseBlockedPage>());
|
|
}
|
|
break;
|
|
case 1:
|
|
state_->pages.push(*state_, std::make_unique<AudioSettingsPage>());
|
|
break;
|
|
case 2:
|
|
state_->pages.push(*state_, std::make_unique<SystemSettingsPage>());
|
|
break;
|
|
case 3:
|
|
state_->pages.push(*state_, std::make_unique<LogViewerPage>());
|
|
break;
|
|
case 4:
|
|
state_->pages.replace(*state_, std::make_unique<Home>());
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
else if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_ESCAPE)
|
|
{
|
|
state_->pages.replace(*state_, std::make_unique<Home>());
|
|
}
|
|
}
|
|
|
|
void Settings::update(float dt)
|
|
{
|
|
}
|
|
|
|
void Settings::render(SDL_Renderer *renderer)
|
|
{
|
|
int winW, winH;
|
|
radiantwave::getLayoutSize(renderer, &winW, &winH);
|
|
float fw = static_cast<float>(winW);
|
|
float fh = static_cast<float>(winH);
|
|
|
|
// 1. Logo Background Image
|
|
if (bgTexture_)
|
|
{
|
|
float scaleX = fw / bgWidth_;
|
|
float scaleY = fh / bgHeight_;
|
|
float scale = std::min(scaleX, scaleY);
|
|
float dw = bgWidth_ * scale;
|
|
float dh = bgHeight_ * scale;
|
|
float dx = (fw - dw) / 2.0f;
|
|
float dy = (fh - dh) / 2.0f;
|
|
SDL_FRect bgDst{dx, dy, dw, dh};
|
|
SDL_RenderTexture(renderer, bgTexture_, nullptr, &bgDst);
|
|
}
|
|
|
|
// 2. Dark Overlay
|
|
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
|
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 160);
|
|
SDL_FRect overlayRect{0, 0, fw, fh};
|
|
SDL_RenderFillRect(renderer, &overlayRect);
|
|
|
|
// 3. Title Text
|
|
float titleY = 60.0f;
|
|
if (titleTexture_)
|
|
{
|
|
float x = (fw - titleWidth_) / 2.0f;
|
|
SDL_FRect dst{x, titleY, titleWidth_, titleHeight_};
|
|
SDL_RenderTexture(renderer, titleTexture_, nullptr, &dst);
|
|
}
|
|
|
|
// Compute bounding box for all menu items
|
|
float maxWidth = 0.0f;
|
|
for (const auto &item : items_)
|
|
{
|
|
maxWidth = std::max(maxWidth, item.width);
|
|
}
|
|
float margin = 12.0f; // 12px vertical margin rule
|
|
|
|
// Shared left edge for left-aligned items with centered bounding box
|
|
float sharedX = (fw - maxWidth) / 2.0f;
|
|
|
|
// Start menu list exactly 24 pixels below title text
|
|
float currentY = titleY + titleHeight_ + 24.0f;
|
|
|
|
for (auto &item : items_)
|
|
{
|
|
item.y = currentY;
|
|
if (item.texture)
|
|
{
|
|
SDL_FRect dst{sharedX, item.y, item.width, item.height};
|
|
SDL_RenderTexture(renderer, item.texture, nullptr, &dst);
|
|
}
|
|
currentY += item.height + margin;
|
|
}
|
|
|
|
// 6px Underline for selected item occupying half of the 12px margin space
|
|
if (selectedItem_ >= 0 && selectedItem_ < static_cast<int>(items_.size()))
|
|
{
|
|
const auto &item = items_[selectedItem_];
|
|
if (item.texture)
|
|
{
|
|
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
|
|
SDL_FRect line{sharedX, item.y + item.height + 2.0f, item.width, 6.0f};
|
|
SDL_RenderFillRect(renderer, &line);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Settings::createTextures(AppState &state)
|
|
{
|
|
destroyTextures();
|
|
|
|
// Background Image
|
|
SDL_Surface *bgSurface = IMG_Load("assets/images/RWT-Circular-Color-ForOnDark-NoText.png");
|
|
if (bgSurface)
|
|
{
|
|
bgTexture_ = SDL_CreateTextureFromSurface(state.renderer, bgSurface);
|
|
bgWidth_ = static_cast<float>(bgSurface->w);
|
|
bgHeight_ = static_cast<float>(bgSurface->h);
|
|
SDL_DestroySurface(bgSurface);
|
|
SDL_SetTextureAlphaMod(bgTexture_, 204);
|
|
}
|
|
|
|
SDL_Color white = {255, 255, 255, 255};
|
|
// Title Font
|
|
TTF_Font *fontLarge = TTF_OpenFont("assets/fonts/Roboto-Regular.ttf", 64);
|
|
if (fontLarge)
|
|
{
|
|
SDL_Surface *surface = TTF_RenderText_Blended(fontLarge, "Settings", 0, white);
|
|
if (surface)
|
|
{
|
|
titleTexture_ = SDL_CreateTextureFromSurface(state.renderer, surface);
|
|
titleWidth_ = static_cast<float>(surface->w);
|
|
titleHeight_ = static_cast<float>(surface->h);
|
|
SDL_DestroySurface(surface);
|
|
}
|
|
TTF_CloseFont(fontLarge);
|
|
}
|
|
|
|
// Item Font
|
|
TTF_Font *fontItem = TTF_OpenFont("assets/fonts/Roboto-Regular.ttf", 36);
|
|
if (!fontItem)
|
|
return;
|
|
|
|
items_.reserve(MENU_ITEM_COUNT);
|
|
|
|
for (int i = 0; i < MENU_ITEM_COUNT; ++i)
|
|
{
|
|
MenuItem item;
|
|
item.label = MENU_LABELS[i];
|
|
|
|
SDL_Surface *surface = TTF_RenderText_Blended(fontItem, MENU_LABELS[i], 0, white);
|
|
if (surface)
|
|
{
|
|
item.texture = SDL_CreateTextureFromSurface(state.renderer, surface);
|
|
item.width = static_cast<float>(surface->w);
|
|
item.height = static_cast<float>(surface->h);
|
|
SDL_DestroySurface(surface);
|
|
}
|
|
items_.push_back(std::move(item));
|
|
}
|
|
|
|
TTF_CloseFont(fontItem);
|
|
}
|
|
|
|
void Settings::destroyTextures()
|
|
{
|
|
if (bgTexture_)
|
|
{
|
|
SDL_DestroyTexture(bgTexture_);
|
|
bgTexture_ = nullptr;
|
|
}
|
|
if (titleTexture_)
|
|
{
|
|
SDL_DestroyTexture(titleTexture_);
|
|
titleTexture_ = nullptr;
|
|
}
|
|
for (auto &item : items_)
|
|
{
|
|
if (item.texture)
|
|
{
|
|
SDL_DestroyTexture(item.texture);
|
|
item.texture = nullptr;
|
|
}
|
|
}
|
|
items_.clear();
|
|
}
|