// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 // // app_wizard.cpp — First-run setup wizard // Split from app.cpp for maintainability. #include "app.h" #include "rpc/rpc_client.h" #include "rpc/rpc_worker.h" #include "rpc/connection.h" #include "config/settings.h" #include "daemon/embedded_daemon.h" #include "ui/notifications.h" #include "ui/material/color_theme.h" #include "ui/material/type.h" #include "ui/material/typography.h" #include "ui/material/draw_helpers.h" #include "ui/schema/ui_schema.h" #include "ui/schema/skin_manager.h" #include "ui/effects/low_spec.h" #include "ui/windows/balance_tab.h" #include "util/platform.h" #include "util/bootstrap.h" #include "util/secure_vault.h" #include "util/i18n.h" #include "util/perf_log.h" #include "embedded/IconsMaterialDesign.h" #include "resources/embedded_resources.h" #include "imgui.h" #include #include #include #include #include namespace dragonx { using json = nlohmann::json; void App::restartWizard() { DEBUG_LOGF("[App] Restarting setup wizard — stopping daemon...\n"); // Reset crash counter for fresh wizard attempt if (embedded_daemon_) { embedded_daemon_->resetCrashCount(); } // Disconnect RPC if (rpc_ && rpc_->isConnected()) { rpc_->disconnect(); } onDisconnected("Wizard restart"); // Stop the embedded daemon in a background thread to avoid // blocking the UI for up to 32 seconds (RPC stop + process wait). if (embedded_daemon_ && isEmbeddedDaemonRunning()) { std::thread([this]() { stopEmbeddedDaemon(); }).detach(); } // Enter wizard — the wizard completion handler already calls // startEmbeddedDaemon() + tryConnect(), so no extra logic needed. wizard_phase_ = WizardPhase::Appearance; } // =========================================================================== // First-Run Wizard Rendering // =========================================================================== void App::renderFirstRunWizard() { ImGuiViewport* viewport = ImGui::GetMainViewport(); ImGui::SetNextWindowPos(viewport->WorkPos); ImGui::SetNextWindowSize(viewport->WorkSize); ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse; ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); ImGui::Begin("##FirstRunWizard", nullptr, flags); ImGui::PopStyleVar(); ImDrawList* dl = ImGui::GetWindowDrawList(); ImVec2 winPos = ImGui::GetWindowPos(); ImVec2 winSize = ImGui::GetWindowSize(); // Background fill ImU32 bgCol = ui::material::Surface(); dl->AddRectFilled(winPos, ImVec2(winPos.x + winSize.x, winPos.y + winSize.y), bgCol); // --- Determine which of the 3 masonry sections is focused --- // 0 = Appearance, 1 = Bootstrap, 2 = Encrypt + PIN int focusIdx = 0; switch (wizard_phase_) { case WizardPhase::Appearance: focusIdx = 0; break; case WizardPhase::BootstrapOffer: case WizardPhase::BootstrapInProgress: case WizardPhase::BootstrapFailed: focusIdx = 1; break; case WizardPhase::EncryptOffer: case WizardPhase::EncryptInProgress: case WizardPhase::PinSetup: focusIdx = 2; break; default: focusIdx = 0; break; } // Card visual state: 0 = not-reached, 1 = focused, 2 = completed auto cardState = [&](int idx) -> int { if (idx < focusIdx) return 2; if (idx == focusIdx) return 1; return 0; }; // --- Fonts & Colors --- const auto& S = ui::schema::UI(); ImFont* titleFont = S.resolveFont(S.label("screens.first-run", "title").font); if (!titleFont) titleFont = ui::material::Type().h5(); ImFont* bodyFont = S.resolveFont(S.label("screens.first-run", "subtitle").font); if (!bodyFont) bodyFont = ui::material::Type().body1(); ImFont* captionFont = S.resolveFont(S.label("screens.first-run", "trust-warning").font); if (!captionFont) captionFont = ui::material::Type().caption(); ImU32 textCol = ui::material::OnSurface(); ImU32 dimCol = (textCol & 0x00FFFFFF) | (IM_COL32_A_MASK & IM_COL32(0,0,0,180)); ImFont* iconFont = ui::material::Type().iconSmall(); if (!iconFont) iconFont = captionFont; // DPI scale factor — multiply all pixel constants by dp const float dp = ui::Layout::dpiScale(); // --- Header: Logo + Welcome --- float headerCy = winPos.y + 20.0f * dp; float logoSize = S.drawElement("screens.first-run", "logo").sizeOr(56.0f); if (logo_tex_ != 0) { float aspect = (logo_h_ > 0) ? (float)logo_w_ / (float)logo_h_ : 1.0f; float logoW = logoSize * aspect; float logoX = winPos.x + (winSize.x - logoW) * 0.5f; dl->AddImage(logo_tex_, ImVec2(logoX, headerCy), ImVec2(logoX + logoW, headerCy + logoSize)); } headerCy += logoSize + 8.0f * dp; { const char* welcomeTitle = "Welcome to ObsidianDragon!"; ImVec2 wts = titleFont->CalcTextSizeA(titleFont->LegacySize, FLT_MAX, 0, welcomeTitle); dl->AddText(titleFont, titleFont->LegacySize, ImVec2(winPos.x + (winSize.x - wts.x) * 0.5f, headerCy), textCol, welcomeTitle); headerCy += wts.y + 16.0f * dp; } // --- Masonry: 2 columns --- // Left column: Card 0 (Appearance) on top, Card 2 (Encrypt+PIN) below // Right column: Card 1 (Bootstrap) float totalW = std::min(920.0f * dp, winSize.x - 40.0f * dp); float gap = 16.0f * dp; float colW = (totalW - gap) * 0.5f; float areaX = winPos.x + (winSize.x - totalW) * 0.5f; float leftX = areaX; float rightX = areaX + colW + gap; float cardPad = 24.0f * dp; float cardRound = 12.0f * dp; float topY = headerCy; // Step icon helper auto stepIcon = [](int state) -> const char* { return (state == 2) ? ICON_MD_CHECK_CIRCLE : (state == 1) ? ICON_MD_RADIO_BUTTON_CHECKED : ICON_MD_RADIO_BUTTON_UNCHECKED; }; // Split draw list: 0 = backgrounds, 1 = content, 2 = overlays/borders dl->ChannelsSplit(3); dl->ChannelsSetCurrent(1); // Helper: finalize card — draw background, accent border or dim overlay auto finalizeCard = [&](float cardX_, float cardW_, float ytop, float ybot, int state) { ImVec2 cMin(cardX_, ytop); ImVec2 cMax(cardX_ + cardW_, ybot); // Background (channel 0) dl->ChannelsSetCurrent(0); if (state == 1) { // Focused card: subtle drop shadow float shadowOff = 3.0f * dp; dl->AddRectFilled( ImVec2(cMin.x + shadowOff, cMin.y + shadowOff), ImVec2(cMax.x + shadowOff, cMax.y + shadowOff), IM_COL32(0, 0, 0, 35), cardRound); } // Use DrawGlassPanel for proper acrylic/opacity/noise/theme effects ui::material::GlassPanelSpec glass; glass.rounding = cardRound; ui::material::DrawGlassPanel(dl, cMin, cMax, glass); // Overlays & borders (channel 2) dl->ChannelsSetCurrent(2); if (state == 1) { // Focused: accent border dl->AddRect(cMin, cMax, ui::material::Primary(), cardRound, 0, 2.0f * dp); } else if (state == 2) { // Completed: dim overlay (preserves color) dl->AddRectFilled(cMin, cMax, (bgCol & 0x00FFFFFF) | IM_COL32(0, 0, 0, 110), cardRound); } else { // Not reached: heavy overlay (creates greyscale look) dl->AddRectFilled(cMin, cMax, (bgCol & 0x00FFFFFF) | IM_COL32(0, 0, 0, 165), cardRound); } dl->ChannelsSetCurrent(1); }; // ======================= CARD 0: Appearance ======================= float card0Top = topY; float card0Bot; { int state = cardState(0); bool isFocused = (state == 1); float cx = leftX + cardPad; float cy = card0Top + cardPad; float contentW = colW - 2 * cardPad; // Step indicator { float iconW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, stepIcon(state)).x; dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), dimCol, stepIcon(state)); dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iconW + 4.0f * dp, cy), dimCol, "Step 1"); cy += captionFont->LegacySize + 6.0f * dp; } // Title { const char* t = "Appearance"; dl->AddText(titleFont, titleFont->LegacySize, ImVec2(cx, cy), textCol, t); cy += titleFont->LegacySize + 10.0f * dp; } // Separator dl->AddLine(ImVec2(cx, cy), ImVec2(cx + contentW, cy), (textCol & 0x00FFFFFF) | IM_COL32(0,0,0,40), 1.0f * dp); cy += 14.0f * dp; // Statics for appearance settings static float wiz_blur_amount = 1.5f; static bool wiz_theme_effects = true; static float wiz_ui_opacity = 1.0f; static bool wiz_low_spec = false; static bool wiz_scanline = true; static std::string wiz_balance_layout = "classic"; static int wiz_language_index = 0; static bool wiz_appearance_init = false; if (!wiz_appearance_init) { wiz_blur_amount = settings_->getBlurMultiplier(); wiz_theme_effects = settings_->getThemeEffectsEnabled(); wiz_ui_opacity = settings_->getUIOpacity(); wiz_low_spec = settings_->getLowSpecMode(); wiz_scanline = settings_->getScanlineEnabled(); wiz_balance_layout = settings_->getBalanceLayout(); // Find current language index const auto& wiz_languages = util::I18n::instance().getAvailableLanguages(); std::string wiz_cur_lang = settings_->getLanguage(); if (wiz_cur_lang.empty()) wiz_cur_lang = "en"; int idx = 0; for (const auto& lang : wiz_languages) { if (lang.first == wiz_cur_lang) { wiz_language_index = idx; break; } idx++; } // Apply loaded settings to runtime so visuals match slider values ui::effects::setLowSpecMode(wiz_low_spec); ui::effects::ImGuiAcrylic::ApplyBlurAmount(wiz_blur_amount); ui::effects::ImGuiAcrylic::SetUIOpacity(wiz_ui_opacity); ui::effects::ThemeEffects::instance().setEnabled(wiz_theme_effects); ui::effects::ThemeEffects::instance().setReducedTransparency(!wiz_theme_effects); ui::ConsoleTab::s_scanline_enabled = wiz_scanline; wiz_appearance_init = true; } // Render controls always so content is visible under the dim // overlay when not focused; disable interaction when not active. ImGui::BeginDisabled(!isFocused); // --- Theme combo --- { auto& skinMgr = ui::schema::SkinManager::instance(); const auto& skins = skinMgr.available(); std::string activePreview = "DragonX"; for (const auto& skin : skins) { if (skin.id == skinMgr.activeSkinId()) { activePreview = skin.name; break; } } dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy + 4.0f * dp), textCol, "Theme"); float comboX = cx + 110.0f * dp; float comboW = contentW - 110.0f * dp; ImGui::SetCursorScreenPos(ImVec2(comboX, cy)); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f * dp); ImGui::SetNextItemWidth(comboW); if (ImGui::BeginCombo("##wiz_theme", activePreview.c_str())) { ImGui::TextDisabled("Built-in"); ImGui::Separator(); for (const auto& skin : skins) { if (!skin.bundled) continue; bool sel = (skin.id == skinMgr.activeSkinId()); if (ImGui::Selectable(skin.name.c_str(), sel)) { skinMgr.setActiveSkin(skin.id); settings_->setSkinId(skin.id); settings_->save(); } if (sel) ImGui::SetItemDefaultFocus(); } bool hasCustom = false; for (const auto& skin : skins) { if (!skin.bundled) { hasCustom = true; break; } } if (hasCustom) { ImGui::Spacing(); ImGui::TextDisabled("Custom"); ImGui::Separator(); for (const auto& skin : skins) { if (skin.bundled) continue; bool sel = (skin.id == skinMgr.activeSkinId()); if (!skin.valid) { ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1,0.3f,0.3f,1)); ImGui::BeginDisabled(true); ImGui::Selectable((skin.name + " (invalid)").c_str(), false); ImGui::EndDisabled(); ImGui::PopStyleColor(); } else { std::string lbl = skin.name; if (!skin.author.empty()) lbl += " (" + skin.author + ")"; if (ImGui::Selectable(lbl.c_str(), sel)) { skinMgr.setActiveSkin(skin.id); settings_->setSkinId(skin.id); settings_->save(); } if (sel) ImGui::SetItemDefaultFocus(); } } } ImGui::EndCombo(); } ImGui::PopStyleVar(); cy += bodyFont->LegacySize + 16.0f * dp; } // --- Balance Layout combo --- { const auto& layouts = ui::GetBalanceLayouts(); std::string balPreview = wiz_balance_layout; for (const auto& l : layouts) { if (l.id == wiz_balance_layout) { balPreview = l.name; break; } } dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy + 4.0f * dp), textCol, "Balance Layout"); float comboX = cx + 110.0f * dp; float comboW = contentW - 110.0f * dp; ImGui::SetCursorScreenPos(ImVec2(comboX, cy)); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f * dp); ImGui::SetNextItemWidth(comboW); if (ImGui::BeginCombo("##wiz_layout", balPreview.c_str())) { for (const auto& l : layouts) { if (!l.enabled) continue; bool sel = (l.id == wiz_balance_layout); if (ImGui::Selectable(l.name.c_str(), sel)) { wiz_balance_layout = l.id; settings_->setBalanceLayout(wiz_balance_layout); settings_->save(); } if (sel) ImGui::SetItemDefaultFocus(); } ImGui::EndCombo(); } ImGui::PopStyleVar(); cy += bodyFont->LegacySize + 16.0f * dp; } // --- Language combo --- { auto& i18n = util::I18n::instance(); const auto& languages = i18n.getAvailableLanguages(); std::vector langNames; langNames.reserve(languages.size()); for (const auto& lang : languages) langNames.push_back(lang.second.c_str()); dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy + 4.0f * dp), textCol, "Language"); float comboX = cx + 110.0f * dp; float comboW = contentW - 110.0f * dp; ImGui::SetCursorScreenPos(ImVec2(comboX, cy)); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f * dp); ImGui::SetNextItemWidth(comboW); if (ImGui::Combo("##wiz_lang", &wiz_language_index, langNames.data(), static_cast(langNames.size()))) { auto it = languages.begin(); std::advance(it, wiz_language_index); i18n.loadLanguage(it->first); } ImGui::PopStyleVar(); cy += bodyFont->LegacySize + 20.0f * dp; } // --- Low-spec mode checkbox --- // Snapshot for restoring settings when low-spec is turned off static struct { bool valid; float blur; float uiOp; bool fx; bool scanline; } wiz_lsSnap = {}; ImGui::SetCursorScreenPos(ImVec2(cx, cy)); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f * dp); if (ImGui::Checkbox("##wiz_lowspec", &wiz_low_spec)) { ui::effects::setLowSpecMode(wiz_low_spec); if (wiz_low_spec) { // Save current effect settings before zeroing wiz_lsSnap.valid = true; wiz_lsSnap.blur = wiz_blur_amount; wiz_lsSnap.uiOp = wiz_ui_opacity; wiz_lsSnap.fx = wiz_theme_effects; wiz_lsSnap.scanline = wiz_scanline; // Disable all heavy effects wiz_blur_amount = 0.0f; wiz_ui_opacity = 1.0f; wiz_theme_effects = false; wiz_scanline = false; ui::effects::ImGuiAcrylic::ApplyBlurAmount(0.0f); ui::effects::ImGuiAcrylic::SetUIOpacity(1.0f); settings_->setWindowOpacity(1.0f); ui::effects::ThemeEffects::instance().setEnabled(false); ui::effects::ThemeEffects::instance().setReducedTransparency(true); ui::ConsoleTab::s_scanline_enabled = false; } else if (wiz_lsSnap.valid) { // Restore previous effect settings wiz_blur_amount = wiz_lsSnap.blur; wiz_ui_opacity = wiz_lsSnap.uiOp; wiz_theme_effects = wiz_lsSnap.fx; wiz_scanline = wiz_lsSnap.scanline; ui::effects::ImGuiAcrylic::ApplyBlurAmount(wiz_blur_amount); ui::effects::ImGuiAcrylic::SetUIOpacity(wiz_ui_opacity); ui::effects::ThemeEffects::instance().setEnabled(wiz_theme_effects); ui::effects::ThemeEffects::instance().setReducedTransparency(false); ui::ConsoleTab::s_scanline_enabled = wiz_scanline; wiz_lsSnap.valid = false; } // Persist immediately so effects read correct values settings_->setAcrylicEnabled(wiz_blur_amount > 0.001f); settings_->setAcrylicQuality(wiz_blur_amount > 0.001f ? static_cast(ui::effects::AcrylicQuality::Low) : static_cast(ui::effects::AcrylicQuality::Off)); settings_->setBlurMultiplier(wiz_blur_amount); settings_->setUIOpacity(wiz_ui_opacity); settings_->setThemeEffectsEnabled(wiz_theme_effects); settings_->setScanlineEnabled(wiz_scanline); settings_->setLowSpecMode(wiz_low_spec); settings_->save(); } ImGui::PopStyleVar(); ImGui::SameLine(); dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(ImGui::GetCursorScreenPos().x, cy + 2.0f * dp), textCol, "Low-spec mode"); cy += bodyFont->LegacySize + 6.0f * dp; dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + 28.0f * dp, cy), dimCol, "Disable all heavy visual effects"); cy += captionFont->LegacySize + 16.0f * dp; ImGui::BeginDisabled(wiz_low_spec); // Acrylic blur slider dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx, cy + 2.0f * dp), textCol, "Acrylic glass effects"); cy += bodyFont->LegacySize + 4.0f * dp; dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, "Translucent blur on panels (Off disables)"); cy += captionFont->LegacySize + 10.0f * dp; { dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + 4.0f * dp, cy), textCol, "Level:"); ImGui::SetCursorScreenPos(ImVec2(cx + 72.0f * dp, cy - 2.0f * dp)); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f * dp); float sliderW = contentW - 72.0f * dp; ImGui::SetNextItemWidth(std::max(80.0f * dp, sliderW)); { char blur_fmt[16]; if (wiz_blur_amount < 0.01f) snprintf(blur_fmt, sizeof(blur_fmt), "Off"); else snprintf(blur_fmt, sizeof(blur_fmt), "%.0f%%%%", wiz_blur_amount * 25.0f); if (ImGui::SliderFloat("##wiz_blur", &wiz_blur_amount, 0.0f, 4.0f, blur_fmt, ImGuiSliderFlags_AlwaysClamp)) { if (wiz_blur_amount > 0.0f && wiz_blur_amount < 0.15f) wiz_blur_amount = 0.0f; ui::effects::ImGuiAcrylic::ApplyBlurAmount(wiz_blur_amount); } } if (ImGui::IsItemDeactivatedAfterEdit()) { settings_->setAcrylicEnabled(wiz_blur_amount > 0.001f); settings_->setAcrylicQuality(wiz_blur_amount > 0.001f ? static_cast(ui::effects::AcrylicQuality::Low) : static_cast(ui::effects::AcrylicQuality::Off)); settings_->setBlurMultiplier(wiz_blur_amount); settings_->save(); } ImGui::PopStyleVar(); cy += bodyFont->LegacySize + 16.0f * dp; } // Theme effects checkbox ImGui::SetCursorScreenPos(ImVec2(cx, cy)); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f * dp); if (ImGui::Checkbox("##wiz_fx", &wiz_theme_effects)) { ui::effects::ThemeEffects::instance().setEnabled(wiz_theme_effects); ui::effects::ThemeEffects::instance().setReducedTransparency(!wiz_theme_effects); settings_->setThemeEffectsEnabled(wiz_theme_effects); settings_->save(); } ImGui::PopStyleVar(); ImGui::SameLine(); dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(ImGui::GetCursorScreenPos().x, cy + 2.0f * dp), textCol, "Theme visual effects"); cy += bodyFont->LegacySize + 6.0f * dp; dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + 28.0f * dp, cy), dimCol, "Animated borders, color wash"); cy += captionFont->LegacySize + 16.0f * dp; // UI Opacity slider dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx, cy + 2.0f * dp), textCol, "UI Opacity"); cy += bodyFont->LegacySize + 4.0f * dp; dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, "Card & sidebar transparency (1.0 = solid)"); cy += captionFont->LegacySize + 10.0f * dp; { ImGui::SetCursorScreenPos(ImVec2(cx, cy - 2.0f * dp)); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f * dp); ImGui::SetNextItemWidth(std::max(80.0f * dp, contentW)); if (ImGui::SliderFloat("##wiz_ui_opacity", &wiz_ui_opacity, 0.3f, 1.0f, "%.2f")) { ui::effects::ImGuiAcrylic::SetUIOpacity(wiz_ui_opacity); } if (ImGui::IsItemDeactivatedAfterEdit()) { settings_->setUIOpacity(wiz_ui_opacity); settings_->save(); } ImGui::PopStyleVar(); cy += bodyFont->LegacySize + 16.0f * dp; } // Console scanline checkbox ImGui::SetCursorScreenPos(ImVec2(cx, cy)); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f * dp); if (ImGui::Checkbox("##wiz_scanline", &wiz_scanline)) { ui::ConsoleTab::s_scanline_enabled = wiz_scanline; settings_->setScanlineEnabled(wiz_scanline); settings_->save(); } ImGui::PopStyleVar(); ImGui::SameLine(); dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(ImGui::GetCursorScreenPos().x, cy + 2.0f * dp), textCol, "Console scanline"); cy += bodyFont->LegacySize + 6.0f * dp; dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + 28.0f * dp, cy), dimCol, "CRT scanline effect in console"); cy += captionFont->LegacySize + 24.0f * dp; ImGui::EndDisabled(); // low-spec ImGui::EndDisabled(); // !isFocused // Continue button (only when focused) if (isFocused) { float btnW = 140.0f * dp; float btnH = 40.0f * dp; float btnX = leftX + (colW - btnW) * 0.5f; ImGui::SetCursorScreenPos(ImVec2(btnX, cy)); ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(ui::material::Primary())); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant())); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary())); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp); if (ImGui::Button("Continue##app", ImVec2(btnW, btnH))) { // Save appearance choices, advance to Bootstrap settings_->setAcrylicEnabled(wiz_blur_amount > 0.001f); settings_->setAcrylicQuality(wiz_blur_amount > 0.001f ? static_cast(ui::effects::AcrylicQuality::Low) : static_cast(ui::effects::AcrylicQuality::Off)); settings_->setBlurMultiplier(wiz_blur_amount); settings_->setThemeEffectsEnabled(wiz_theme_effects); settings_->setUIOpacity(wiz_ui_opacity); settings_->setLowSpecMode(wiz_low_spec); settings_->setScanlineEnabled(wiz_scanline); settings_->setBalanceLayout(wiz_balance_layout); settings_->save(); wizard_phase_ = WizardPhase::BootstrapOffer; } ImGui::PopStyleVar(); ImGui::PopStyleColor(3); cy += btnH; } cy += cardPad; // Lock card height to the tallest content ever seen static float card0MaxH = 0.0f; card0MaxH = std::max(card0MaxH, cy - card0Top); card0Bot = card0Top + card0MaxH; // Card 0 finalization deferred until after cards 1+2 are sized } // ======================= CARD 1: Bootstrap ======================= float card1Top = topY; float card1Bot; { int state = cardState(1); bool isFocused = (state == 1); bool isCollapsed = (state == 2 && cardState(2) == 1); // Minimize when step 3 active float cx = rightX + cardPad; float cy = card1Top + cardPad; float contentW = colW - 2 * cardPad; // Step indicator + title (inline when collapsed) if (isCollapsed) { // Compact single-line: icon + "Step 2" + "Bootstrap" + checkmark float iconW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, stepIcon(state)).x; dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), dimCol, stepIcon(state)); float labelX = cx + iconW + 4.0f * dp; dl->AddText(captionFont, captionFont->LegacySize, ImVec2(labelX, cy), dimCol, "Step 2"); float step2W = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, "Step 2").x; float titleX = labelX + step2W + 12.0f * dp; dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(titleX, cy), dimCol, "Bootstrap"); cy += captionFont->LegacySize + 4.0f * dp; } else { // Step indicator { float iconW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, stepIcon(state)).x; dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), dimCol, stepIcon(state)); dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iconW + 4.0f * dp, cy), dimCol, "Step 2"); cy += captionFont->LegacySize + 4.0f * dp; } // Title { const char* t = "Bootstrap"; dl->AddText(titleFont, titleFont->LegacySize, ImVec2(cx, cy), textCol, t); cy += titleFont->LegacySize + 6.0f * dp; } // Separator dl->AddLine(ImVec2(cx, cy), ImVec2(cx + contentW, cy), (textCol & 0x00FFFFFF) | IM_COL32(0,0,0,40), 1.0f * dp); cy += 10.0f * dp; } // --- Content varies by sub-state (only when focused, skip when collapsed) --- if (isCollapsed) { // No content — card is minimized } else if (isFocused && wizard_phase_ == WizardPhase::BootstrapInProgress) { // ---- Bootstrap download in progress ---- if (!bootstrap_) { wizard_phase_ = WizardPhase::EncryptOffer; } else { auto prog = bootstrap_->getProgress(); const char* statusTitle; if (prog.state == util::Bootstrap::State::Downloading) statusTitle = "Downloading bootstrap..."; else if (prog.state == util::Bootstrap::State::Verifying) statusTitle = "Verifying checksums..."; else statusTitle = "Extracting blockchain data..."; dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx, cy), textCol, statusTitle); cy += bodyFont->LegacySize + 12.0f * dp; // Progress bar float barH = 8.0f * dp, barR = 4.0f * dp; dl->AddRectFilled(ImVec2(cx, cy), ImVec2(cx + contentW, cy + barH), IM_COL32(255,255,255,30), barR); float fillW = contentW * (prog.percent / 100.0f); if (fillW > 0) { dl->AddRectFilled(ImVec2(cx, cy), ImVec2(cx + fillW, cy + barH), ui::material::Primary(), barR); } cy += barH + 8.0f * dp; // Status text + percent { char pctText[64]; snprintf(pctText, sizeof(pctText), "%.1f%%", prog.percent); ImVec2 pts = bodyFont->CalcTextSizeA(bodyFont->LegacySize, FLT_MAX, 0, pctText); dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx + contentW - pts.x, cy), textCol, pctText); } dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, prog.status_text.c_str()); cy += bodyFont->LegacySize + 6.0f * dp; if (prog.state == util::Bootstrap::State::Extracting) { dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, "(wallet.dat is protected)"); cy += captionFont->LegacySize + 6.0f * dp; } cy += 12.0f * dp; // Cancel button float cancelW = 100.0f * dp; float cancelH = 36.0f * dp; float cancelBX = rightX + (colW - cancelW) * 0.5f; ImGui::SetCursorScreenPos(ImVec2(cancelBX, cy)); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp); if (ImGui::Button("Cancel##bs", ImVec2(cancelW, cancelH))) { bootstrap_->cancel(); } ImGui::PopStyleVar(); cy += cancelH; // Check completion if (bootstrap_->isDone()) { auto finalProg = bootstrap_->getProgress(); if (finalProg.state == util::Bootstrap::State::Completed) { bootstrap_.reset(); wizard_phase_ = WizardPhase::EncryptOffer; } else { wizard_phase_ = WizardPhase::BootstrapFailed; } } } } else if (isFocused && wizard_phase_ == WizardPhase::BootstrapFailed) { // ---- Bootstrap failed ---- std::string errMsg; if (bootstrap_) { errMsg = bootstrap_->getProgress().error; bootstrap_.reset(); } if (errMsg.empty()) errMsg = "Bootstrap failed"; dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx, cy), ui::material::Error(), "Download Failed"); cy += bodyFont->LegacySize + 8.0f * dp; dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), textCol, errMsg.c_str()); cy += captionFont->LegacySize + 16.0f * dp; // Retry / Skip float btnW2 = 120.0f * dp; float btnH2 = 40.0f * dp; float totalBW = btnW2 * 2 + 12.0f * dp; float bx = rightX + (colW - totalBW) * 0.5f; ImGui::SetCursorScreenPos(ImVec2(bx, cy)); ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(ui::material::Primary())); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant())); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary())); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp); if (ImGui::Button("Retry##bs", ImVec2(btnW2, btnH2))) { // Stop embedded daemon before bootstrap to avoid chain data corruption if (isEmbeddedDaemonRunning()) { DEBUG_LOGF("[Wizard] Stopping embedded daemon before bootstrap retry...\n"); if (rpc_ && rpc_->isConnected()) { try { rpc_->call("stop"); } catch (...) {} rpc_->disconnect(); } onDisconnected("Bootstrap retry"); } bootstrap_ = std::make_unique(); std::string dataDir = util::Platform::getDragonXDataDir(); bootstrap_->start(dataDir); wizard_phase_ = WizardPhase::BootstrapInProgress; } ImGui::PopStyleVar(); ImGui::PopStyleColor(3); ImGui::SetCursorScreenPos(ImVec2(bx + btnW2 + 12.0f * dp, cy)); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp); if (ImGui::Button("Skip##bsfail", ImVec2(btnW2, btnH2))) { wizard_phase_ = WizardPhase::EncryptOffer; } ImGui::PopStyleVar(); cy += btnH2; } else { // ---- Bootstrap offer (default view, also for non-focused) ---- // External daemon check (async — avoids blocking UI thread). // On Windows isRpcPortInUse() creates a TCP socket + connect() // which can block for seconds when the port is not listening. bool externalRunning = false; if (isFocused) { static std::atomic s_extCached{false}; static std::atomic s_checkInFlight{false}; static double s_extLastCheck = -10.0; double now = ImGui::GetTime(); if (now - s_extLastCheck >= 2.0 && !s_checkInFlight.load()) { s_extLastCheck = now; bool embeddedRunning = isEmbeddedDaemonRunning(); s_checkInFlight.store(true); std::thread([embeddedRunning]() { bool inUse = daemon::EmbeddedDaemon::isRpcPortInUse(); s_extCached.store(inUse && !embeddedRunning); s_checkInFlight.store(false); }).detach(); } externalRunning = s_extCached.load(); } if (isFocused && (externalRunning || wizard_stopping_external_)) { // --- External daemon warning --- ImU32 warnCol = ui::material::Warning(); { float iw = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, ICON_MD_WARNING).x; dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), warnCol, ICON_MD_WARNING); dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx + iw + 4.0f * dp, cy), warnCol, "External daemon running"); } cy += bodyFont->LegacySize + 4.0f * dp; { const char* warnBody = "It must be stopped before downloading a bootstrap, otherwise chain data could be corrupted."; ImVec2 ws = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, contentW, warnBody); dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), textCol, warnBody, nullptr, contentW); cy += ws.y + 12.0f * dp; } if (wizard_stopping_external_) { dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, wizard_stop_status_.c_str()); cy += captionFont->LegacySize + 8.0f * dp; } else { float stopW = 150.0f * dp; float skipW2 = 100.0f * dp; float btnH2 = 40.0f * dp; float totalBW = stopW + 12.0f * dp + skipW2; float bx = rightX + (colW - totalBW) * 0.5f; ImGui::SetCursorScreenPos(ImVec2(bx, cy)); ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(ui::material::Error())); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4( IM_COL32(220, 60, 60, 255))); ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 255, 255, 255)); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp); if (ImGui::Button("Stop Daemon##wiz", ImVec2(stopW, btnH2))) { wizard_stopping_external_ = true; wizard_stop_status_ = "Sending stop command..."; if (wizard_stop_thread_.joinable()) wizard_stop_thread_.join(); wizard_stop_thread_ = std::thread([this]() { auto config = rpc::Connection::autoDetectConfig(); if (!config.rpcuser.empty() && !config.rpcpassword.empty()) { auto tmp_rpc = std::make_unique(); if (tmp_rpc->connect(config.host, config.port, config.rpcuser, config.rpcpassword)) { try { tmp_rpc->call("stop"); } catch (...) {} tmp_rpc->disconnect(); } } wizard_stop_status_ = "Waiting for daemon to shut down..."; for (int i = 0; i < 60; i++) { std::this_thread::sleep_for(std::chrono::seconds(1)); if (!daemon::EmbeddedDaemon::isRpcPortInUse()) { wizard_stop_status_ = "Daemon stopped."; wizard_stopping_external_ = false; return; } } wizard_stop_status_ = "Daemon did not stop — try manually."; wizard_stopping_external_ = false; }); } ImGui::PopStyleVar(); ImGui::PopStyleColor(3); ImGui::SetCursorScreenPos(ImVec2(bx + stopW + 12.0f * dp, cy)); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp); if (ImGui::Button("Skip##extd", ImVec2(skipW2, btnH2))) { wizard_phase_ = WizardPhase::EncryptOffer; } ImGui::PopStyleVar(); cy += btnH2; } } else { // --- Normal bootstrap offer --- { const char* bsText = "Download a blockchain bootstrap to dramatically speed up initial sync.\n\nYour existing wallet.dat will NOT be modified or replaced."; ImVec2 bsSize = bodyFont->CalcTextSizeA(bodyFont->LegacySize, FLT_MAX, contentW, bsText); dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx, cy), textCol, bsText, nullptr, contentW); cy += bsSize.y + 8.0f * dp; } // Trust warning { float warnOpacity = S.drawElement("screens.first-run", "trust-warning").opacity; if (warnOpacity <= 0) warnOpacity = 0.7f; ImU32 warnCol = (textCol & 0x00FFFFFF) | ((ImU32)(255 * warnOpacity) << 24); float iw = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, ICON_MD_WARNING).x; dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), warnCol, ICON_MD_WARNING); const char* twText = "Only use bootstrap.dragonx.is or bootstrap2.dragonx.is. Using files from untrusted sources could compromise your node."; float twWrap = contentW - iw - 4.0f * dp; ImVec2 twSize = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, twWrap, twText); dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iw + 4.0f * dp, cy), warnCol, twText, nullptr, twWrap); cy += twSize.y + 12.0f * dp; } // Buttons (only when focused) if (isFocused) { float dlBtnW = 150.0f * dp; float mirrorW = 150.0f * dp; float skipW2 = 80.0f * dp; float btnH2 = 40.0f * dp; float totalBW = dlBtnW + 8.0f * dp + mirrorW + 8.0f * dp + skipW2; float bx = rightX + (colW - totalBW) * 0.5f; // --- Download button (main / Cloudflare) --- ImGui::SetCursorScreenPos(ImVec2(bx, cy)); ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(ui::material::Primary())); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant())); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary())); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp); if (ImGui::Button("Download##bs", ImVec2(dlBtnW, btnH2))) { // Stop embedded daemon before bootstrap to avoid chain data corruption if (isEmbeddedDaemonRunning()) { DEBUG_LOGF("[Wizard] Stopping embedded daemon before bootstrap...\n"); if (rpc_ && rpc_->isConnected()) { try { rpc_->call("stop"); } catch (...) {} rpc_->disconnect(); } onDisconnected("Bootstrap"); } bootstrap_ = std::make_unique(); std::string dataDir = util::Platform::getDragonXDataDir(); bootstrap_->start(dataDir); wizard_phase_ = WizardPhase::BootstrapInProgress; } ImGui::PopStyleVar(); ImGui::PopStyleColor(3); // --- Mirror Download button --- ImGui::SetCursorScreenPos(ImVec2(bx + dlBtnW + 8.0f * dp, cy)); ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(ui::material::Surface())); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant())); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnSurface())); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp); if (ImGui::Button("Mirror##bs_mirror", ImVec2(mirrorW, btnH2))) { if (isEmbeddedDaemonRunning()) { DEBUG_LOGF("[Wizard] Stopping embedded daemon before bootstrap (mirror)...\n"); if (rpc_ && rpc_->isConnected()) { try { rpc_->call("stop"); } catch (...) {} rpc_->disconnect(); } onDisconnected("Bootstrap"); } bootstrap_ = std::make_unique(); std::string dataDir = util::Platform::getDragonXDataDir(); std::string mirrorUrl = std::string(util::Bootstrap::kMirrorUrl) + "/" + util::Bootstrap::kZipName; bootstrap_->start(dataDir, mirrorUrl); wizard_phase_ = WizardPhase::BootstrapInProgress; } if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Download from mirror (bootstrap2.dragonx.is).\nUse this if the main download is slow or failing."); } ImGui::PopStyleVar(); ImGui::PopStyleColor(3); // --- Skip button --- ImGui::SetCursorScreenPos(ImVec2(bx + dlBtnW + 8.0f * dp + mirrorW + 8.0f * dp, cy)); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp); if (ImGui::Button("Skip##bs", ImVec2(skipW2, btnH2))) { wizard_phase_ = WizardPhase::EncryptOffer; } ImGui::PopStyleVar(); cy += btnH2; } } } cy += cardPad; // Lock card height to the tallest content ever seen (but not when collapsed) static float card1MaxH = 0.0f; if (isCollapsed) { card1Bot = card1Top + (cy - card1Top); } else { card1MaxH = std::max(card1MaxH, cy - card1Top); card1Bot = card1Top + card1MaxH; } finalizeCard(rightX, colW, card1Top, card1Bot, state); } // ======================= CARD 2: Encrypt + PIN ======================= float card2Top = card1Bot + gap; float card2Bot; { int state = cardState(2); bool isFocused = (state == 1); float cx = rightX + cardPad; float cy = card2Top + cardPad; float contentW = colW - 2 * cardPad; // Pre-start daemon when encrypt card becomes focused so it's ready // by the time the user finishes typing their passphrase if (isFocused) { static bool wiz_daemon_prestarted = false; if (!wiz_daemon_prestarted) { wiz_daemon_prestarted = true; if (!state_.connected && isUsingEmbeddedDaemon() && !isEmbeddedDaemonRunning()) { startEmbeddedDaemon(); } if (!state_.connected && !connection_in_progress_) { tryConnect(); } } } // Step indicator { float iconW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, stepIcon(state)).x; dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), dimCol, stepIcon(state)); dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iconW + 4.0f * dp, cy), dimCol, "Step 3"); cy += captionFont->LegacySize + 4.0f * dp; } // Title (changes for PinSetup sub-state) { const char* t = (isFocused && wizard_phase_ == WizardPhase::PinSetup) ? "Quick-Unlock PIN" : "Encryption"; dl->AddText(titleFont, titleFont->LegacySize, ImVec2(cx, cy), textCol, t); cy += titleFont->LegacySize + 6.0f * dp; } // Separator dl->AddLine(ImVec2(cx, cy), ImVec2(cx + contentW, cy), (textCol & 0x00FFFFFF) | IM_COL32(0,0,0,40), 1.0f * dp); cy += 10.0f * dp; // --- Content varies by sub-state --- if (isFocused && state_.isEncrypted()) { // ---- Wallet already encrypted ---- { ImU32 okCol = ui::material::Secondary(); float iw = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, ICON_MD_VERIFIED_USER).x; dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), okCol, ICON_MD_VERIFIED_USER); dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx + iw + 6.0f * dp, cy), okCol, "Wallet is already encrypted"); cy += bodyFont->LegacySize + 12.0f * dp; } { const char* desc = "Your wallet is protected with a passphrase. No further action is needed."; ImVec2 ds = bodyFont->CalcTextSizeA(bodyFont->LegacySize, FLT_MAX, contentW, desc); dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx, cy), textCol, desc, nullptr, contentW); cy += ds.y + 20.0f * dp; } // Continue button — skip to Done float btnW2 = 140.0f * dp; float btnH2 = 40.0f * dp; float bx = rightX + (colW - btnW2) * 0.5f; ImGui::SetCursorScreenPos(ImVec2(bx, cy)); ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(ui::material::Primary())); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant())); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary())); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp); if (ImGui::Button("Continue##encok", ImVec2(btnW2, btnH2))) { wizard_phase_ = WizardPhase::Done; settings_->setWizardCompleted(true); settings_->save(); } ImGui::PopStyleVar(); ImGui::PopStyleColor(3); cy += btnH2; } else if (isFocused) { // ---- Encryption offer + optional PIN (combined) ---- { const char* encDesc = "Encrypt your wallet to protect private keys with a passphrase."; ImVec2 edSize = bodyFont->CalcTextSizeA(bodyFont->LegacySize, FLT_MAX, contentW, encDesc); dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx, cy), textCol, encDesc, nullptr, contentW); cy += edSize.y + 6.0f * dp; } { ImU32 warnCol2 = ui::material::Warning(); float iw = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, ICON_MD_WARNING).x; dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), warnCol2, ICON_MD_WARNING); const char* warnLoss = "If you lose your passphrase, you lose access to your funds."; float wlWrap = contentW - iw - 4.0f * dp; ImVec2 wlSize = bodyFont->CalcTextSizeA(bodyFont->LegacySize, FLT_MAX, wlWrap, warnLoss); dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx + iw + 4.0f * dp, cy), warnCol2, warnLoss, nullptr, wlWrap); cy += wlSize.y + 8.0f * dp; } // Passphrase input dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, "Passphrase:"); cy += captionFont->LegacySize + 4.0f * dp; ImGui::SetCursorScreenPos(ImVec2(cx, cy)); ImGui::PushItemWidth(contentW); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f * dp); ImGui::InputText("##wiz_pass", encrypt_pass_buf_, sizeof(encrypt_pass_buf_), ImGuiInputTextFlags_Password); ImGui::PopStyleVar(); ImGui::PopItemWidth(); cy += 36.0f * dp + 6.0f * dp; dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, "Confirm:"); cy += captionFont->LegacySize + 4.0f * dp; ImGui::SetCursorScreenPos(ImVec2(cx, cy)); ImGui::PushItemWidth(contentW); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f * dp); ImGui::InputText("##wiz_confirm", encrypt_confirm_buf_, sizeof(encrypt_confirm_buf_), ImGuiInputTextFlags_Password); ImGui::PopStyleVar(); ImGui::PopItemWidth(); cy += 36.0f * dp + 6.0f * dp; // Strength meter { size_t len = strlen(encrypt_pass_buf_); const char* strengthLabel = "Weak"; ImU32 strengthCol = ui::material::Error(); float strengthPct = 0.25f; if (len >= 16) { strengthLabel = "Strong"; strengthCol = ui::material::Secondary(); strengthPct = 1.0f; } else if (len >= 12) { strengthLabel = "Good"; strengthCol = ui::material::Secondary(); strengthPct = 0.75f; } else if (len >= 8) { strengthLabel = "Fair"; strengthCol = ui::material::Warning(); strengthPct = 0.5f; } float sBarH = 4.0f * dp, sBarR = 2.0f * dp; dl->AddRectFilled(ImVec2(cx, cy), ImVec2(cx + contentW, cy + sBarH), IM_COL32(255,255,255,30), sBarR); if (len > 0) { dl->AddRectFilled(ImVec2(cx, cy), ImVec2(cx + contentW * strengthPct, cy + sBarH), strengthCol, sBarR); } cy += sBarH + 4.0f * dp; char slabel[64]; snprintf(slabel, sizeof(slabel), "Strength: %s", strengthLabel); dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, slabel); cy += captionFont->LegacySize + 10.0f * dp; } // Feedback on why Encrypt is disabled { size_t pLen = strlen(encrypt_pass_buf_); if (pLen > 0 && pLen < 8) { char fb[80]; snprintf(fb, sizeof(fb), "Passphrase must be at least 8 characters (%zu/8)", pLen); dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), ui::material::Error(), fb); cy += captionFont->LegacySize + 6.0f * dp; } else if (pLen >= 8 && strlen(encrypt_confirm_buf_) > 0 && strcmp(encrypt_pass_buf_, encrypt_confirm_buf_) != 0) { dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), ui::material::Error(), "Passphrases do not match"); cy += captionFont->LegacySize + 6.0f * dp; } } // ---- Optional PIN section ---- cy += 4.0f * dp; dl->AddLine(ImVec2(cx, cy), ImVec2(cx + contentW, cy), (textCol & 0x00FFFFFF) | IM_COL32(0,0,0,40), 1.0f * dp); cy += 8.0f * dp; { const char* pinTitle = "Quick-Unlock PIN (optional)"; dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), textCol, pinTitle); cy += captionFont->LegacySize + 4.0f * dp; } dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, "PIN (4-8 digits):"); cy += captionFont->LegacySize + 4.0f * dp; ImGui::SetCursorScreenPos(ImVec2(cx, cy)); ImGui::PushItemWidth(contentW); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f * dp); ImGui::InputText("##wiz_pin", wizard_pin_buf_, sizeof(wizard_pin_buf_), ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal); ImGui::PopStyleVar(); ImGui::PopItemWidth(); cy += 36.0f * dp + 6.0f * dp; dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, "Confirm PIN:"); cy += captionFont->LegacySize + 4.0f * dp; ImGui::SetCursorScreenPos(ImVec2(cx, cy)); ImGui::PushItemWidth(contentW); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f * dp); ImGui::InputText("##wiz_pin_confirm", wizard_pin_confirm_buf_, sizeof(wizard_pin_confirm_buf_), ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal); ImGui::PopStyleVar(); ImGui::PopItemWidth(); cy += 36.0f * dp + 6.0f * dp; // PIN validation feedback { std::string pinStr(wizard_pin_buf_); if (!pinStr.empty() && !util::SecureVault::isValidPin(pinStr)) { dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), ui::material::Error(), "PIN must be 4-8 digits"); cy += captionFont->LegacySize + 6.0f * dp; } else if (!pinStr.empty() && strlen(wizard_pin_confirm_buf_) > 0 && pinStr != std::string(wizard_pin_confirm_buf_)) { dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), ui::material::Error(), "PINs do not match"); cy += captionFont->LegacySize + 6.0f * dp; } } // Status if (!encrypt_status_.empty()) { dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), ui::material::Warning(), encrypt_status_.c_str()); cy += captionFont->LegacySize + 6.0f * dp; } // Buttons { bool passValid = strlen(encrypt_pass_buf_) >= 8 && strcmp(encrypt_pass_buf_, encrypt_confirm_buf_) == 0; // PIN is optional: if entered, must be valid + confirmed std::string pinStr(wizard_pin_buf_); bool pinEntered = !pinStr.empty(); bool pinOk = !pinEntered || (util::SecureVault::isValidPin(pinStr) && pinStr == std::string(wizard_pin_confirm_buf_)); bool canEncrypt = passValid && pinOk && !encrypt_in_progress_; float encBtnW = 180.0f * dp; float skipW2 = 80.0f * dp; float btnH2 = 40.0f * dp; float totalBW = encBtnW + 12.0f * dp + skipW2; float bx = rightX + (colW - totalBW) * 0.5f; ImGui::SetCursorScreenPos(ImVec2(bx, cy)); ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4( canEncrypt ? ui::material::Primary() : IM_COL32(128,128,128,128))); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant())); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary())); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp); ImGui::BeginDisabled(!canEncrypt); if (ImGui::Button("Encrypt & Continue##wiz", ImVec2(encBtnW, btnH2))) { // Save passphrase + optional PIN for background processing deferred_encrypt_passphrase_ = std::string(encrypt_pass_buf_); if (pinEntered && pinOk) deferred_encrypt_pin_ = pinStr; deferred_encrypt_pending_ = true; // Clear sensitive buffers memset(encrypt_pass_buf_, 0, sizeof(encrypt_pass_buf_)); memset(encrypt_confirm_buf_, 0, sizeof(encrypt_confirm_buf_)); memset(wizard_pin_buf_, 0, sizeof(wizard_pin_buf_)); memset(wizard_pin_confirm_buf_, 0, sizeof(wizard_pin_confirm_buf_)); // Start daemon + finish wizard immediately if (!isEmbeddedDaemonRunning() && isUsingEmbeddedDaemon()) { startEmbeddedDaemon(); } tryConnect(); wizard_phase_ = WizardPhase::Done; settings_->setWizardCompleted(true); settings_->save(); ui::Notifications::instance().info("Encryption will complete in the background"); } ImGui::EndDisabled(); ImGui::PopStyleVar(); ImGui::PopStyleColor(3); ImGui::SetCursorScreenPos(ImVec2(bx + encBtnW + 12.0f * dp, cy)); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp); if (ImGui::Button("Skip##enc", ImVec2(skipW2, btnH2))) { wizard_phase_ = WizardPhase::Done; settings_->setWizardCompleted(true); settings_->save(); if (!isEmbeddedDaemonRunning() && isUsingEmbeddedDaemon()) { startEmbeddedDaemon(); } tryConnect(); } ImGui::PopStyleVar(); cy += btnH2; } } else { // ---- Not focused: show static description ---- const char* encDesc = "Encrypt your wallet to protect private keys with a passphrase."; ImVec2 edSize = bodyFont->CalcTextSizeA(bodyFont->LegacySize, FLT_MAX, contentW, encDesc); dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx, cy), dimCol, encDesc, nullptr, contentW); cy += edSize.y + 6.0f * dp; } cy += cardPad; { card2Bot = card2Top + (cy - card2Top); // Stretch card 2 so its bottom sits flush with card 0 (left column) if (card0Bot > card2Bot) card2Bot = card0Bot; } finalizeCard(rightX, colW, card2Top, card2Bot, state); } // --- Deferred Card 0 finalization: match right column total height --- { float rightColBot = card2Bot; if (rightColBot > card0Bot) card0Bot = rightColBot; finalizeCard(leftX, colW, card0Top, card0Bot, cardState(0)); } // Merge channels: backgrounds → content → overlays dl->ChannelsMerge(); ImGui::End(); } } // namespace dragonx