Pages computed layout from SDL_GetWindowSize() (real window coordinates) while SDL_SetRenderLogicalPresentation() independently remapped every draw call into a 1920x1080 space. The two coordinate systems only agreed when the window happened to be exactly 1920x1080; at any other size, layout coordinates were transformed a second time, so content shrank/grew quadratically toward the top-left on resize or on non-1080p monitors. Commenting the call out masked the bug by making rendering 1:1 with the window, but then nothing scaled: fonts and margins stayed fixed-pixel while backgrounds cover-fit, and HiDPI displays drew at point resolution and relied on blurry compositor upsampling. Restore SDL_SetRenderLogicalPresentation(1920x1080, LETTERBOX) as the single source of scaling, and route all twelve layout sites through a new radiantwave::getLayoutSize() helper (src/logical_size.h) that returns the renderer's logical size, so pages always lay out against the design resolution regardless of window or monitor geometry. Add SDL_WINDOW_HIGH_PIXEL_DENSITY so HiDPI displays render at native pixel density instead of being upscaled by the OS. Cached per-page textures become resolution-independent since layout no longer depends on window size.
342 lines
14 KiB
C++
342 lines
14 KiB
C++
#include "timezone_page.h"
|
|
#include "app_state.h"
|
|
#include "logical_size.h"
|
|
#include <SDL3/SDL.h>
|
|
#include <algorithm>
|
|
#include <ctime>
|
|
|
|
void TimeZonePage::onPush(AppState& state) {
|
|
state_ = &state;
|
|
TTF_Init();
|
|
createFonts();
|
|
|
|
currentTzCode_ = state.config.get().timezone;
|
|
|
|
state.keybinds.bind(SDLK_UP, "tz_up", [this]() {
|
|
const auto& countries = radiantwave::timezone::getStandardTimeZones();
|
|
if (step_ == Step::Country) {
|
|
int count = static_cast<int>(countries.size());
|
|
if (count > 0) {
|
|
selectedCountryIndex_ = (selectedCountryIndex_ - 1 + count) % count;
|
|
}
|
|
} else if (step_ == Step::Zone) {
|
|
if (selectedCountryIndex_ >= 0 && selectedCountryIndex_ < static_cast<int>(countries.size())) {
|
|
int count = static_cast<int>(countries[selectedCountryIndex_].zones.size());
|
|
if (count > 0) {
|
|
selectedZoneIndex_ = (selectedZoneIndex_ - 1 + count) % count;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
state.keybinds.bind(SDLK_DOWN, "tz_down", [this]() {
|
|
const auto& countries = radiantwave::timezone::getStandardTimeZones();
|
|
if (step_ == Step::Country) {
|
|
int count = static_cast<int>(countries.size());
|
|
if (count > 0) {
|
|
selectedCountryIndex_ = (selectedCountryIndex_ + 1) % count;
|
|
}
|
|
} else if (step_ == Step::Zone) {
|
|
if (selectedCountryIndex_ >= 0 && selectedCountryIndex_ < static_cast<int>(countries.size())) {
|
|
int count = static_cast<int>(countries[selectedCountryIndex_].zones.size());
|
|
if (count > 0) {
|
|
selectedZoneIndex_ = (selectedZoneIndex_ + 1) % count;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
void TimeZonePage::onResume(AppState& state) {
|
|
state_ = &state;
|
|
createFonts();
|
|
currentTzCode_ = state.config.get().timezone;
|
|
|
|
state.keybinds.bind(SDLK_UP, "tz_up", [this]() {
|
|
const auto& countries = radiantwave::timezone::getStandardTimeZones();
|
|
if (step_ == Step::Country) {
|
|
int count = static_cast<int>(countries.size());
|
|
if (count > 0) {
|
|
selectedCountryIndex_ = (selectedCountryIndex_ - 1 + count) % count;
|
|
}
|
|
} else if (step_ == Step::Zone) {
|
|
if (selectedCountryIndex_ >= 0 && selectedCountryIndex_ < static_cast<int>(countries.size())) {
|
|
int count = static_cast<int>(countries[selectedCountryIndex_].zones.size());
|
|
if (count > 0) {
|
|
selectedZoneIndex_ = (selectedZoneIndex_ - 1 + count) % count;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
state.keybinds.bind(SDLK_DOWN, "tz_down", [this]() {
|
|
const auto& countries = radiantwave::timezone::getStandardTimeZones();
|
|
if (step_ == Step::Country) {
|
|
int count = static_cast<int>(countries.size());
|
|
if (count > 0) {
|
|
selectedCountryIndex_ = (selectedCountryIndex_ + 1) % count;
|
|
}
|
|
} else if (step_ == Step::Zone) {
|
|
if (selectedCountryIndex_ >= 0 && selectedCountryIndex_ < static_cast<int>(countries.size())) {
|
|
int count = static_cast<int>(countries[selectedCountryIndex_].zones.size());
|
|
if (count > 0) {
|
|
selectedZoneIndex_ = (selectedZoneIndex_ + 1) % count;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
void TimeZonePage::onPause(AppState& state) {
|
|
state.keybinds.unbindAll();
|
|
destroyFonts();
|
|
destroyTextures();
|
|
}
|
|
|
|
void TimeZonePage::onPop(AppState& state) {
|
|
destroyFonts();
|
|
destroyTextures();
|
|
TTF_Quit();
|
|
}
|
|
|
|
void TimeZonePage::createFonts() {
|
|
destroyFonts();
|
|
fontTitle_ = TTF_OpenFont("assets/fonts/Roboto-Regular.ttf", 52);
|
|
fontHeader_ = TTF_OpenFont("assets/fonts/Roboto-Regular.ttf", 28);
|
|
fontBody_ = TTF_OpenFont("assets/fonts/Roboto-Regular.ttf", 32);
|
|
fontSmall_ = TTF_OpenFont("assets/fonts/Roboto-Regular.ttf", 22);
|
|
}
|
|
|
|
void TimeZonePage::destroyFonts() {
|
|
if (fontTitle_) { TTF_CloseFont(fontTitle_); fontTitle_ = nullptr; }
|
|
if (fontHeader_) { TTF_CloseFont(fontHeader_); fontHeader_ = nullptr; }
|
|
if (fontBody_) { TTF_CloseFont(fontBody_); fontBody_ = nullptr; }
|
|
if (fontSmall_) { TTF_CloseFont(fontSmall_); fontSmall_ = nullptr; }
|
|
}
|
|
|
|
void TimeZonePage::destroyTextures() {
|
|
titleText_.destroy();
|
|
tzInfoText_.destroy();
|
|
sampleText_.destroy();
|
|
statusMsgText_.destroy();
|
|
footerText_.destroy();
|
|
for (auto& t : countryRows_) t.destroy();
|
|
countryRows_.clear();
|
|
for (auto& t : zoneRows_) t.destroy();
|
|
zoneRows_.clear();
|
|
}
|
|
|
|
void TimeZonePage::handleEvent(const SDL_Event& event) {
|
|
if (!state_) return;
|
|
|
|
if (event.type == SDL_EVENT_KEY_DOWN) {
|
|
SDL_Keycode key = event.key.key;
|
|
|
|
if (key == SDLK_UP) {
|
|
const auto& countries = radiantwave::timezone::getStandardTimeZones();
|
|
if (step_ == Step::Country) {
|
|
int count = static_cast<int>(countries.size());
|
|
if (count > 0) {
|
|
selectedCountryIndex_ = (selectedCountryIndex_ - 1 + count) % count;
|
|
}
|
|
} else if (step_ == Step::Zone) {
|
|
if (selectedCountryIndex_ >= 0 && selectedCountryIndex_ < static_cast<int>(countries.size())) {
|
|
int count = static_cast<int>(countries[selectedCountryIndex_].zones.size());
|
|
if (count > 0) {
|
|
selectedZoneIndex_ = (selectedZoneIndex_ - 1 + count) % count;
|
|
}
|
|
}
|
|
}
|
|
} else if (key == SDLK_DOWN) {
|
|
const auto& countries = radiantwave::timezone::getStandardTimeZones();
|
|
if (step_ == Step::Country) {
|
|
int count = static_cast<int>(countries.size());
|
|
if (count > 0) {
|
|
selectedCountryIndex_ = (selectedCountryIndex_ + 1) % count;
|
|
}
|
|
} else if (step_ == Step::Zone) {
|
|
if (selectedCountryIndex_ >= 0 && selectedCountryIndex_ < static_cast<int>(countries.size())) {
|
|
int count = static_cast<int>(countries[selectedCountryIndex_].zones.size());
|
|
if (count > 0) {
|
|
selectedZoneIndex_ = (selectedZoneIndex_ + 1) % count;
|
|
}
|
|
}
|
|
}
|
|
} else if (key == SDLK_RETURN || key == SDLK_KP_ENTER) {
|
|
const auto& countries = radiantwave::timezone::getStandardTimeZones();
|
|
if (step_ == Step::Country) {
|
|
if (selectedCountryIndex_ >= 0 && selectedCountryIndex_ < static_cast<int>(countries.size())) {
|
|
step_ = Step::Zone;
|
|
selectedZoneIndex_ = 0;
|
|
statusMessage_ = "Select timezone for " + countries[selectedCountryIndex_].country_name;
|
|
}
|
|
} else if (step_ == Step::Zone) {
|
|
if (selectedCountryIndex_ >= 0 && selectedCountryIndex_ < static_cast<int>(countries.size())) {
|
|
const auto& zones = countries[selectedCountryIndex_].zones;
|
|
if (selectedZoneIndex_ >= 0 && selectedZoneIndex_ < static_cast<int>(zones.size())) {
|
|
std::string target_code = zones[selectedZoneIndex_].iana_code;
|
|
state_->config.update([target_code](radiantwave::config::AppConfig& cfg) {
|
|
cfg.timezone = target_code;
|
|
});
|
|
currentTzCode_ = target_code;
|
|
statusMessage_ = "Timezone set to " + target_code;
|
|
step_ = Step::Country;
|
|
}
|
|
}
|
|
}
|
|
} else if (key == SDLK_ESCAPE || key == SDLK_BACKSPACE) {
|
|
if (step_ == Step::Zone) {
|
|
step_ = Step::Country;
|
|
statusMessage_ = "Select country, then timezone";
|
|
} else {
|
|
state_->pages.pop(*state_);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void TimeZonePage::update(float dt) {
|
|
}
|
|
|
|
void TimeZonePage::render(SDL_Renderer* renderer) {
|
|
int winW, winH;
|
|
radiantwave::getLayoutSize(renderer, &winW, &winH);
|
|
float fw = static_cast<float>(winW);
|
|
float fh = static_cast<float>(winH);
|
|
|
|
// Background
|
|
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
|
SDL_SetRenderDrawColor(renderer, 20, 20, 30, 255);
|
|
SDL_FRect bgRect{0, 0, fw, fh};
|
|
SDL_RenderFillRect(renderer, &bgRect);
|
|
|
|
if (!fontTitle_ || !fontHeader_ || !fontBody_ || !fontSmall_) return;
|
|
|
|
SDL_Color white = {255, 255, 255, 255};
|
|
SDL_Color green = {0, 255, 0, 255};
|
|
SDL_Color yellow = {255, 255, 0, 255};
|
|
SDL_Color cyan = {100, 220, 255, 255};
|
|
SDL_Color gray = {180, 180, 180, 255};
|
|
|
|
float currentY = 30.0f;
|
|
|
|
// 1. Title
|
|
titleText_.ensure(renderer, fontTitle_, "Time Zone Configuration", white);
|
|
if (titleText_.texture) {
|
|
float x = (fw - titleText_.width) / 2.0f;
|
|
SDL_FRect dst{x, currentY, titleText_.width, titleText_.height};
|
|
SDL_RenderTexture(renderer, titleText_.texture, nullptr, &dst);
|
|
currentY += titleText_.height + 20.0f;
|
|
}
|
|
|
|
// 2. Current Configured Timezone & Live Formatted Sample
|
|
tzInfoText_.ensure(renderer, fontHeader_, "Configured Timezone: " + currentTzCode_, yellow);
|
|
if (tzInfoText_.texture) {
|
|
SDL_FRect dst{80.0f, currentY, tzInfoText_.width, tzInfoText_.height};
|
|
SDL_RenderTexture(renderer, tzInfoText_.texture, nullptr, &dst);
|
|
}
|
|
|
|
time_t now = std::time(nullptr);
|
|
std::string localSample = "Local Time: " + radiantwave::timezone::formatUtcToTimezone(now, currentTzCode_);
|
|
sampleText_.ensure(renderer, fontHeader_, localSample, green);
|
|
if (sampleText_.texture) {
|
|
SDL_FRect dst{700.0f, currentY, sampleText_.width, sampleText_.height};
|
|
SDL_RenderTexture(renderer, sampleText_.texture, nullptr, &dst);
|
|
}
|
|
currentY += 40.0f;
|
|
|
|
// 3. Status Banner
|
|
if (!statusMessage_.empty()) {
|
|
SDL_SetRenderDrawColor(renderer, 40, 45, 65, 255);
|
|
SDL_FRect bannerRect{80.0f, currentY, fw - 160.0f, 44.0f};
|
|
SDL_RenderFillRect(renderer, &bannerRect);
|
|
|
|
SDL_SetRenderDrawColor(renderer, 100, 150, 255, 255);
|
|
SDL_RenderRect(renderer, &bannerRect);
|
|
|
|
statusMsgText_.ensure(renderer, fontHeader_, statusMessage_, cyan);
|
|
if (statusMsgText_.texture) {
|
|
SDL_FRect dst{95.0f, currentY + (44.0f - statusMsgText_.height) / 2.0f, statusMsgText_.width, statusMsgText_.height};
|
|
SDL_RenderTexture(renderer, statusMsgText_.texture, nullptr, &dst);
|
|
}
|
|
currentY += 60.0f;
|
|
}
|
|
|
|
// 4. List Options (Countries or Timezones)
|
|
{
|
|
const auto& countries = radiantwave::timezone::getStandardTimeZones();
|
|
float listY = currentY;
|
|
float rowHeight = 50.0f;
|
|
|
|
if (step_ == Step::Country) {
|
|
if (countryRows_.size() != countries.size()) countryRows_.resize(countries.size());
|
|
for (int i = 0; i < static_cast<int>(countries.size()); ++i) {
|
|
if (listY + rowHeight > fh - 90.0f) break;
|
|
|
|
const auto& c = countries[i];
|
|
SDL_Color color = white;
|
|
|
|
// Highlight if any zone in this country matches currentTzCode_
|
|
for (const auto& z : c.zones) {
|
|
if (z.iana_code == currentTzCode_) {
|
|
color = green;
|
|
break;
|
|
}
|
|
}
|
|
|
|
countryRows_[i].ensure(renderer, fontBody_, c.country_name, color);
|
|
if (countryRows_[i].texture) {
|
|
SDL_FRect dst{100.0f, listY, countryRows_[i].width, countryRows_[i].height};
|
|
SDL_RenderTexture(renderer, countryRows_[i].texture, nullptr, &dst);
|
|
|
|
if (i == selectedCountryIndex_) {
|
|
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
|
|
SDL_FRect selLine{100.0f, listY + countryRows_[i].height + 2.0f, countryRows_[i].width, 6.0f};
|
|
SDL_RenderFillRect(renderer, &selLine);
|
|
}
|
|
}
|
|
|
|
listY += rowHeight;
|
|
}
|
|
} else if (step_ == Step::Zone) {
|
|
if (selectedCountryIndex_ >= 0 && selectedCountryIndex_ < static_cast<int>(countries.size())) {
|
|
const auto& zones = countries[selectedCountryIndex_].zones;
|
|
if (zoneRows_.size() != zones.size()) zoneRows_.resize(zones.size());
|
|
for (int i = 0; i < static_cast<int>(zones.size()); ++i) {
|
|
if (listY + rowHeight > fh - 90.0f) break;
|
|
|
|
const auto& z = zones[i];
|
|
SDL_Color color = (z.iana_code == currentTzCode_) ? green : white;
|
|
|
|
std::string label = z.display_name + " [" + z.iana_code + "]";
|
|
zoneRows_[i].ensure(renderer, fontBody_, label, color);
|
|
if (zoneRows_[i].texture) {
|
|
SDL_FRect dst{100.0f, listY, zoneRows_[i].width, zoneRows_[i].height};
|
|
SDL_RenderTexture(renderer, zoneRows_[i].texture, nullptr, &dst);
|
|
|
|
if (i == selectedZoneIndex_) {
|
|
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
|
|
SDL_FRect selLine{100.0f, listY + zoneRows_[i].height + 2.0f, zoneRows_[i].width, 6.0f};
|
|
SDL_RenderFillRect(renderer, &selLine);
|
|
}
|
|
}
|
|
|
|
listY += rowHeight;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 5. Footer Hint
|
|
std::string hintStr = (step_ == Step::Country)
|
|
? "[UP/DOWN] Select Country [ENTER] Choose [ESC] Back to Settings"
|
|
: "[UP/DOWN] Select Timezone [ENTER] Apply Timezone [ESC/BACKSPACE] Back to Countries";
|
|
|
|
footerText_.ensure(renderer, fontSmall_, hintStr, gray);
|
|
if (footerText_.texture) {
|
|
float x = (fw - footerText_.width) / 2.0f;
|
|
SDL_FRect dst{x, fh - 40.0f, footerText_.width, footerText_.height};
|
|
SDL_RenderTexture(renderer, footerText_.texture, nullptr, &dst);
|
|
}
|
|
}
|