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.
492 lines
19 KiB
C++
492 lines
19 KiB
C++
#include "wifi_setup_page.h"
|
|
#include "app_state.h"
|
|
#include "provisioning.h"
|
|
#include "logical_size.h"
|
|
#include <SDL3/SDL.h>
|
|
#include <algorithm>
|
|
#include <exception>
|
|
#include <iostream>
|
|
|
|
void WiFiSetupPage::onPush(AppState& state) {
|
|
state_ = &state;
|
|
TTF_Init();
|
|
createFonts();
|
|
|
|
// Register F5 keybind for rescan
|
|
state.keybinds.bind(SDLK_F5, "rescan_wifi", [this]() {
|
|
triggerScan();
|
|
});
|
|
|
|
state.keybinds.bind(SDLK_UP, "wifi_up", [this]() {
|
|
if (inputMode_ == InputMode::Normal && !networks_.empty()) {
|
|
selectedIndex_ = (selectedIndex_ - 1 + static_cast<int>(networks_.size())) % static_cast<int>(networks_.size());
|
|
}
|
|
});
|
|
|
|
state.keybinds.bind(SDLK_DOWN, "wifi_down", [this]() {
|
|
if (inputMode_ == InputMode::Normal && !networks_.empty()) {
|
|
selectedIndex_ = (selectedIndex_ + 1) % static_cast<int>(networks_.size());
|
|
}
|
|
});
|
|
|
|
triggerScan();
|
|
refreshNetworkState();
|
|
}
|
|
|
|
void WiFiSetupPage::onResume(AppState& state) {
|
|
state_ = &state;
|
|
createFonts();
|
|
|
|
state.keybinds.bind(SDLK_F5, "rescan_wifi", [this]() {
|
|
triggerScan();
|
|
});
|
|
|
|
state.keybinds.bind(SDLK_UP, "wifi_up", [this]() {
|
|
if (inputMode_ == InputMode::Normal && !networks_.empty()) {
|
|
selectedIndex_ = (selectedIndex_ - 1 + static_cast<int>(networks_.size())) % static_cast<int>(networks_.size());
|
|
}
|
|
});
|
|
|
|
state.keybinds.bind(SDLK_DOWN, "wifi_down", [this]() {
|
|
if (inputMode_ == InputMode::Normal && !networks_.empty()) {
|
|
selectedIndex_ = (selectedIndex_ + 1) % static_cast<int>(networks_.size());
|
|
}
|
|
});
|
|
|
|
refreshNetworkState();
|
|
}
|
|
|
|
void WiFiSetupPage::onPause(AppState& state) {
|
|
state.keybinds.unbindAll();
|
|
if (inputMode_ == InputMode::PasswordEntry && state.window) {
|
|
SDL_StopTextInput(state.window);
|
|
}
|
|
destroyFonts();
|
|
destroyTextures();
|
|
}
|
|
|
|
void WiFiSetupPage::onPop(AppState& state) {
|
|
state.keybinds.unbindAll();
|
|
if (inputMode_ == InputMode::PasswordEntry && state.window) {
|
|
SDL_StopTextInput(state.window);
|
|
}
|
|
destroyFonts();
|
|
destroyTextures();
|
|
TTF_Quit();
|
|
}
|
|
|
|
void WiFiSetupPage::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 WiFiSetupPage::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 WiFiSetupPage::destroyTextures() {
|
|
titleText_.destroy();
|
|
wifiStatusText_.destroy();
|
|
networkText_.destroy();
|
|
internetText_.destroy();
|
|
statusMsgText_.destroy();
|
|
emptyText_.destroy();
|
|
for (auto& t : ssidRows_) t.destroy();
|
|
ssidRows_.clear();
|
|
for (auto& t : secBadges_) t.destroy();
|
|
secBadges_.clear();
|
|
for (auto& t : sigTexts_) t.destroy();
|
|
sigTexts_.clear();
|
|
passwordPromptText_.destroy();
|
|
passwordValueText_.destroy();
|
|
passwordHintText_.destroy();
|
|
footerText_.destroy();
|
|
}
|
|
|
|
void WiFiSetupPage::triggerScan() {
|
|
if (!state_) return;
|
|
statusMessage_ = "Scanning for available networks...";
|
|
state_->network.requestScan();
|
|
refreshNetworkState();
|
|
}
|
|
|
|
void WiFiSetupPage::refreshNetworkState() {
|
|
if (!state_) return;
|
|
|
|
isConnected_ = state_->network.isConnected();
|
|
connectedSsid_ = state_->network.currentSSID();
|
|
hasInternet_ = state_->network.hasInternet();
|
|
savedSsids_ = state_->network.savedNetworks();
|
|
networks_ = state_->network.getScanResults();
|
|
|
|
if (inputMode_ != InputMode::Normal && !selectedSsid_.empty()) {
|
|
// Pin the selection to the SSID while a password is being entered or a
|
|
// connection is in progress, so list reordering (strength changes,
|
|
// APs appearing/vanishing) cannot silently switch the target network.
|
|
auto it = std::find_if(networks_.begin(), networks_.end(),
|
|
[this](const radiantwave::network::NetworkInfo& n) { return n.ssid == selectedSsid_; });
|
|
if (it != networks_.end()) {
|
|
selectedIndex_ = static_cast<int>(std::distance(networks_.begin(), it));
|
|
}
|
|
}
|
|
|
|
if (selectedIndex_ >= static_cast<int>(networks_.size())) {
|
|
selectedIndex_ = std::max(0, static_cast<int>(networks_.size()) - 1);
|
|
}
|
|
}
|
|
|
|
void WiFiSetupPage::startConnectAsync(const std::string& ssid, const std::string& password) {
|
|
if (connectInProgress_) return;
|
|
|
|
connectInProgress_ = true;
|
|
inputMode_ = InputMode::Connecting;
|
|
statusMessage_ = "Connecting to " + ssid + "...";
|
|
|
|
connectFuture_ = std::async(std::launch::async, [this, ssid, password]() {
|
|
// Never let an exception escape the worker thread — a transient
|
|
// D-Bus failure must surface as a graceful connection error, not a
|
|
// crash when update() calls future.get().
|
|
try {
|
|
return state_->network.connect(ssid, password);
|
|
} catch (const std::exception& e) {
|
|
return radiantwave::network::ConnectResult{
|
|
radiantwave::network::ConnectionState::Failed, e.what(), ""};
|
|
}
|
|
});
|
|
}
|
|
|
|
void WiFiSetupPage::handleEvent(const SDL_Event& event) {
|
|
if (!state_) return;
|
|
|
|
if (inputMode_ == InputMode::PasswordEntry) {
|
|
if (event.type == SDL_EVENT_TEXT_INPUT) {
|
|
passwordInput_ += event.text.text;
|
|
return;
|
|
}
|
|
|
|
if (event.type == SDL_EVENT_KEY_DOWN) {
|
|
if (event.key.key == SDLK_BACKSPACE) {
|
|
if (!passwordInput_.empty()) {
|
|
passwordInput_.pop_back();
|
|
}
|
|
} else if (event.key.key == SDLK_RETURN || event.key.key == SDLK_KP_ENTER) {
|
|
if (state_->window) {
|
|
SDL_StopTextInput(state_->window);
|
|
}
|
|
if (!selectedSsid_.empty()) {
|
|
startConnectAsync(selectedSsid_, passwordInput_);
|
|
}
|
|
} else if (event.key.key == SDLK_ESCAPE) {
|
|
if (state_->window) {
|
|
SDL_StopTextInput(state_->window);
|
|
}
|
|
inputMode_ = InputMode::Normal;
|
|
statusMessage_ = "Password entry cancelled";
|
|
}
|
|
return;
|
|
}
|
|
} else if (inputMode_ == InputMode::Normal) {
|
|
if (event.type == SDL_EVENT_KEY_DOWN) {
|
|
SDL_Keycode key = event.key.key;
|
|
if (key == SDLK_F5) {
|
|
triggerScan();
|
|
} else if (key == SDLK_UP) {
|
|
if (!networks_.empty()) {
|
|
selectedIndex_ = (selectedIndex_ - 1 + static_cast<int>(networks_.size())) % static_cast<int>(networks_.size());
|
|
}
|
|
} else if (key == SDLK_DOWN) {
|
|
if (!networks_.empty()) {
|
|
selectedIndex_ = (selectedIndex_ + 1) % static_cast<int>(networks_.size());
|
|
}
|
|
} else if (key == SDLK_RETURN || key == SDLK_KP_ENTER) {
|
|
if (selectedIndex_ >= 0 && selectedIndex_ < static_cast<int>(networks_.size())) {
|
|
const auto& net = networks_[selectedIndex_];
|
|
if (net.security == radiantwave::network::SecurityType::Open) {
|
|
startConnectAsync(net.ssid, "");
|
|
} else {
|
|
selectedSsid_ = net.ssid;
|
|
inputMode_ = InputMode::PasswordEntry;
|
|
passwordInput_ = "";
|
|
statusMessage_ = "Type password and press ENTER to submit";
|
|
if (state_->window) {
|
|
SDL_StartTextInput(state_->window);
|
|
}
|
|
}
|
|
}
|
|
} else if (key == SDLK_ESCAPE) {
|
|
exitPage(*state_);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// In the First-Run walkthrough a finished step moves on to the next missing
|
|
// prerequisite; otherwise the page returns to whatever pushed it (Settings).
|
|
void WiFiSetupPage::exitPage(AppState& state)
|
|
{
|
|
if (state.provisioningMode) {
|
|
radiantwave::provisioning::advance(state);
|
|
} else {
|
|
state.pages.pop(state);
|
|
}
|
|
}
|
|
|
|
void WiFiSetupPage::update(float dt) {
|
|
refreshTimer_ += dt;
|
|
if (refreshTimer_ >= 3.0f) {
|
|
refreshTimer_ = 0.0f;
|
|
|
|
// Re-issue a scan on every tick rather than only re-reading results:
|
|
// NetworkManager only reports access points from completed scans, so a
|
|
// slow or failed first scan would otherwise leave the list empty
|
|
// forever. Skipped while a connection attempt is running to avoid
|
|
// churning the radio mid-connect.
|
|
if (state_ && !connectInProgress_) {
|
|
state_->network.requestScan();
|
|
}
|
|
refreshNetworkState();
|
|
|
|
// During the First-Run walkthrough, leave the WiFi page automatically
|
|
// as soon as the internet is reachable (fresh WiFi connection, an
|
|
// Ethernet drop, etc.). Uses the already-refreshed status so no extra
|
|
// blocking probe is needed here.
|
|
if (state_ && state_->provisioningMode && hasInternet_ &&
|
|
!connectInProgress_ && returnTimer_ < 0.0f) {
|
|
statusMessage_ = "Internet connected. Continuing setup...";
|
|
returnTimer_ = 1.5f;
|
|
}
|
|
}
|
|
|
|
if (returnTimer_ > 0.0f) {
|
|
returnTimer_ -= dt;
|
|
if (returnTimer_ <= 0.0f) {
|
|
returnTimer_ = -1.0f;
|
|
if (state_ && state_->provisioningMode) {
|
|
exitPage(*state_);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check async connection result
|
|
if (connectInProgress_ && connectFuture_.valid()) {
|
|
auto status = connectFuture_.wait_for(std::chrono::milliseconds(0));
|
|
if (status == std::future_status::ready) {
|
|
radiantwave::network::ConnectResult result;
|
|
try {
|
|
result = connectFuture_.get();
|
|
} catch (const std::exception& e) {
|
|
// Backstop: a stray exception must never reach the render
|
|
// loop. Convert it to a visible, non-fatal connection error.
|
|
result = radiantwave::network::ConnectResult{
|
|
radiantwave::network::ConnectionState::Failed, e.what(), ""};
|
|
}
|
|
connectInProgress_ = false;
|
|
|
|
if (result.state == radiantwave::network::ConnectionState::Activated) {
|
|
statusMessage_ = "Connected successfully!";
|
|
inputMode_ = InputMode::Normal;
|
|
} else {
|
|
statusMessage_ = "Connection failed: " + (result.error_message.empty() ? "Incorrect password or authentication error" : result.error_message);
|
|
inputMode_ = InputMode::Normal;
|
|
}
|
|
refreshNetworkState();
|
|
}
|
|
}
|
|
}
|
|
|
|
void WiFiSetupPage::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 red = {255, 80, 80, 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_, "WiFi 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. Status Section (Top Panel)
|
|
std::string wifiStatusStr = isConnected_ ? "WiFi: Connected" : "WiFi: Disconnected";
|
|
SDL_Color statusColor = isConnected_ ? green : red;
|
|
wifiStatusText_.ensure(renderer, fontHeader_, wifiStatusStr, statusColor);
|
|
if (wifiStatusText_.texture) {
|
|
SDL_FRect dst{80.0f, currentY, wifiStatusText_.width, wifiStatusText_.height};
|
|
SDL_RenderTexture(renderer, wifiStatusText_.texture, nullptr, &dst);
|
|
}
|
|
|
|
networkText_.ensure(renderer, fontHeader_, "Network: " + (connectedSsid_.empty() ? "None" : connectedSsid_), white);
|
|
if (networkText_.texture) {
|
|
SDL_FRect dst{400.0f, currentY, networkText_.width, networkText_.height};
|
|
SDL_RenderTexture(renderer, networkText_.texture, nullptr, &dst);
|
|
}
|
|
|
|
std::string inetStr = hasInternet_ ? "Internet: Reachable" : "Internet: Unreachable";
|
|
SDL_Color inetColor = hasInternet_ ? green : red;
|
|
internetText_.ensure(renderer, fontHeader_, inetStr, inetColor);
|
|
if (internetText_.texture) {
|
|
SDL_FRect dst{900.0f, currentY, internetText_.width, internetText_.height};
|
|
SDL_RenderTexture(renderer, internetText_.texture, nullptr, &dst);
|
|
}
|
|
currentY += 40.0f;
|
|
|
|
// 3. Status/Error Message 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. Network List
|
|
if (networks_.empty()) {
|
|
emptyText_.ensure(renderer, fontBody_, "No WiFi networks found. Press F5 to scan.", gray);
|
|
if (emptyText_.texture) {
|
|
SDL_FRect dst{80.0f, currentY + 40.0f, emptyText_.width, emptyText_.height};
|
|
SDL_RenderTexture(renderer, emptyText_.texture, nullptr, &dst);
|
|
}
|
|
} else {
|
|
float listY = currentY;
|
|
float rowHeight = 50.0f;
|
|
|
|
if (ssidRows_.size() != networks_.size()) ssidRows_.resize(networks_.size());
|
|
if (secBadges_.size() != networks_.size()) secBadges_.resize(networks_.size());
|
|
if (sigTexts_.size() != networks_.size()) sigTexts_.resize(networks_.size());
|
|
|
|
for (int i = 0; i < static_cast<int>(networks_.size()); ++i) {
|
|
if (listY + rowHeight > fh - 100.0f) break; // Screen boundaries
|
|
|
|
const auto& net = networks_[i];
|
|
|
|
// Color coding rules:
|
|
// GREEN = currently connected
|
|
// YELLOW = saved/remembered
|
|
// WHITE = unknown
|
|
SDL_Color textColor = white;
|
|
if (net.ssid == connectedSsid_ && isConnected_) {
|
|
textColor = green;
|
|
} else if (std::find(savedSsids_.begin(), savedSsids_.end(), net.ssid) != savedSsids_.end()) {
|
|
textColor = yellow;
|
|
}
|
|
|
|
// Render SSID Name
|
|
ssidRows_[i].ensure(renderer, fontBody_, net.ssid, textColor);
|
|
if (ssidRows_[i].texture) {
|
|
SDL_FRect dst{100.0f, listY, ssidRows_[i].width, ssidRows_[i].height};
|
|
SDL_RenderTexture(renderer, ssidRows_[i].texture, nullptr, &dst);
|
|
|
|
// 6px Selection Underline for currently highlighted item
|
|
if (i == selectedIndex_ && inputMode_ == InputMode::Normal) {
|
|
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
|
|
SDL_FRect selLine{100.0f, listY + ssidRows_[i].height + 2.0f, ssidRows_[i].width, 6.0f};
|
|
SDL_RenderFillRect(renderer, &selLine);
|
|
}
|
|
}
|
|
|
|
// Render Security Type Badge
|
|
std::string secStr = "[" + std::string(radiantwave::network::securityTypeToString(net.security)) + "]";
|
|
secBadges_[i].ensure(renderer, fontSmall_, secStr, gray);
|
|
if (secBadges_[i].texture) {
|
|
SDL_FRect dst{700.0f, listY + 6.0f, secBadges_[i].width, secBadges_[i].height};
|
|
SDL_RenderTexture(renderer, secBadges_[i].texture, nullptr, &dst);
|
|
}
|
|
|
|
// Render Signal Strength
|
|
std::string sigStr = std::to_string(net.signal_strength) + "%";
|
|
sigTexts_[i].ensure(renderer, fontSmall_, sigStr, gray);
|
|
if (sigTexts_[i].texture) {
|
|
SDL_FRect dst{950.0f, listY + 6.0f, sigTexts_[i].width, sigTexts_[i].height};
|
|
SDL_RenderTexture(renderer, sigTexts_[i].texture, nullptr, &dst);
|
|
}
|
|
|
|
listY += rowHeight;
|
|
}
|
|
}
|
|
|
|
// 5. Password Entry Overlay Modal
|
|
if (inputMode_ == InputMode::PasswordEntry) {
|
|
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 210);
|
|
SDL_FRect modalBg{0, 0, fw, fh};
|
|
SDL_RenderFillRect(renderer, &modalBg);
|
|
|
|
float boxW = 800.0f;
|
|
float boxH = 260.0f;
|
|
float boxX = (fw - boxW) / 2.0f;
|
|
float boxY = (fh - boxH) / 2.0f;
|
|
|
|
SDL_SetRenderDrawColor(renderer, 35, 40, 60, 255);
|
|
SDL_FRect boxRect{boxX, boxY, boxW, boxH};
|
|
SDL_RenderFillRect(renderer, &boxRect);
|
|
|
|
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
|
|
SDL_RenderRect(renderer, &boxRect);
|
|
|
|
if (!selectedSsid_.empty()) {
|
|
std::string promptStr = "Enter Password for " + selectedSsid_ + ":";
|
|
passwordPromptText_.ensure(renderer, fontHeader_, promptStr, white);
|
|
if (passwordPromptText_.texture) {
|
|
SDL_FRect dst{boxX + 30.0f, boxY + 25.0f, passwordPromptText_.width, passwordPromptText_.height};
|
|
SDL_RenderTexture(renderer, passwordPromptText_.texture, nullptr, &dst);
|
|
}
|
|
}
|
|
|
|
// Render plain text unmasked password
|
|
std::string displayPass = passwordInput_ + "_"; // cursor
|
|
passwordValueText_.ensure(renderer, fontBody_, displayPass, cyan);
|
|
if (passwordValueText_.texture) {
|
|
SDL_FRect dst{boxX + 30.0f, boxY + 95.0f, passwordValueText_.width, passwordValueText_.height};
|
|
SDL_RenderTexture(renderer, passwordValueText_.texture, nullptr, &dst);
|
|
}
|
|
|
|
passwordHintText_.ensure(renderer, fontSmall_, "[ENTER] Submit Password [ESC] Cancel", gray);
|
|
if (passwordHintText_.texture) {
|
|
SDL_FRect dst{boxX + 30.0f, boxY + 195.0f, passwordHintText_.width, passwordHintText_.height};
|
|
SDL_RenderTexture(renderer, passwordHintText_.texture, nullptr, &dst);
|
|
}
|
|
}
|
|
|
|
// 6. Footer Hints (Bottom Bar)
|
|
footerText_.ensure(renderer, fontSmall_,
|
|
"[UP/DOWN] Select [ENTER] Connect [F5] Rescan Networks [ESC] Back", 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);
|
|
}
|
|
}
|