#include "wifi_setup_page.h" #include "app_state.h" #include "provisioning.h" #include #include #include 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(networks_.size())) % static_cast(networks_.size()); } }); state.keybinds.bind(SDLK_DOWN, "wifi_down", [this]() { if (inputMode_ == InputMode::Normal && !networks_.empty()) { selectedIndex_ = (selectedIndex_ + 1) % static_cast(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(networks_.size())) % static_cast(networks_.size()); } }); state.keybinds.bind(SDLK_DOWN, "wifi_down", [this]() { if (inputMode_ == InputMode::Normal && !networks_.empty()) { selectedIndex_ = (selectedIndex_ + 1) % static_cast(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 (selectedIndex_ >= static_cast(networks_.size())) { selectedIndex_ = std::max(0, static_cast(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]() { return state_->network.connect(ssid, password); }); } 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 (selectedIndex_ >= 0 && selectedIndex_ < static_cast(networks_.size())) { std::string ssid = networks_[selectedIndex_].ssid; startConnectAsync(ssid, 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(networks_.size())) % static_cast(networks_.size()); } } else if (key == SDLK_DOWN) { if (!networks_.empty()) { selectedIndex_ = (selectedIndex_ + 1) % static_cast(networks_.size()); } } else if (key == SDLK_RETURN || key == SDLK_KP_ENTER) { if (selectedIndex_ >= 0 && selectedIndex_ < static_cast(networks_.size())) { const auto& net = networks_[selectedIndex_]; if (net.security == radiantwave::network::SecurityType::Open) { startConnectAsync(net.ssid, ""); } else { inputMode_ = InputMode::PasswordEntry; passwordInput_ = ""; statusMessage_ = "Type password and press ENTER to submit"; if (state_->window) { SDL_StartTextInput(state_->window); } } } } else if (key == SDLK_ESCAPE || key == SDLK_TAB) { 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; 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) { auto result = connectFuture_.get(); connectInProgress_ = false; if (result.state == radiantwave::network::ConnectionState::Activated) { statusMessage_ = "Connected successfully!"; inputMode_ = InputMode::Normal; } else { statusMessage_ = "Connection failed: " + (result.error_message.empty() ? "Authentication or signal error" : result.error_message); inputMode_ = InputMode::Normal; } refreshNetworkState(); } } } void WiFiSetupPage::render(SDL_Renderer* renderer) { int winW, winH; SDL_GetWindowSize(SDL_GetRenderWindow(renderer), &winW, &winH); float fw = static_cast(winW); float fh = static_cast(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(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 (selectedIndex_ >= 0 && selectedIndex_ < static_cast(networks_.size())) { std::string promptStr = "Enter Password for " + networks_[selectedIndex_].ssid + ":"; 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/TAB] 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); } }