add generic pre-runtime acquisition wizard overlay
This commit is contained in:
parent
05049e0769
commit
eed2883e74
3 changed files with 272 additions and 0 deletions
89
include/rex/ui/overlay/acquire_wizard_overlay.h
Normal file
89
include/rex/ui/overlay/acquire_wizard_overlay.h
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
/**
|
||||||
|
* @file rex/ui/overlay/acquire_wizard_overlay.h
|
||||||
|
*
|
||||||
|
* @brief Generic pre-runtime acquisition dialog: install a payload either
|
||||||
|
* by fetching it automatically (e.g. a download) or from a
|
||||||
|
* user-selected source file.
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#include <rex/ui/imgui_dialog.h>
|
||||||
|
|
||||||
|
namespace rex::ui {
|
||||||
|
|
||||||
|
class AcquireWizardDialog final : public ImGuiDialog {
|
||||||
|
public:
|
||||||
|
struct Options {
|
||||||
|
std::string title;
|
||||||
|
std::string intro;
|
||||||
|
std::string target_directory;
|
||||||
|
std::string initial_status;
|
||||||
|
// Leave a label empty to hide the corresponding button.
|
||||||
|
std::string fetch_button_label;
|
||||||
|
std::string pick_button_label;
|
||||||
|
// Shown while a fetch is in progress but no bytes have arrived yet (e.g.
|
||||||
|
// waiting on the server's first byte). Falls back to fetch_working_status
|
||||||
|
// if empty.
|
||||||
|
std::string fetch_connecting_status;
|
||||||
|
std::string fetch_working_status;
|
||||||
|
std::string install_working_status;
|
||||||
|
std::string done_status;
|
||||||
|
std::string done_button_label;
|
||||||
|
};
|
||||||
|
|
||||||
|
using PickSourceCallback = std::function<std::filesystem::path()>;
|
||||||
|
using InstallCallback = std::function<bool(const std::filesystem::path& source,
|
||||||
|
std::atomic<uint64_t>& copied_bytes,
|
||||||
|
std::atomic<uint64_t>& total_bytes,
|
||||||
|
std::string& error)>;
|
||||||
|
using FetchCallback = std::function<bool(std::atomic<uint64_t>& copied_bytes,
|
||||||
|
std::atomic<uint64_t>& total_bytes,
|
||||||
|
std::string& error)>;
|
||||||
|
using CompleteCallback = std::function<void()>;
|
||||||
|
|
||||||
|
AcquireWizardDialog(ImGuiDrawer* drawer, Options options, FetchCallback fetch,
|
||||||
|
PickSourceCallback pick_source, InstallCallback install,
|
||||||
|
CompleteCallback complete);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void OnClose() override;
|
||||||
|
void OnDraw(ImGuiIO& io) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum class State {
|
||||||
|
kWaitingForChoice,
|
||||||
|
kWorking,
|
||||||
|
kDone,
|
||||||
|
kFailed,
|
||||||
|
};
|
||||||
|
|
||||||
|
void StartWork(std::function<bool(std::string&)> work, std::string busy_status);
|
||||||
|
void StartFetch();
|
||||||
|
void PickSourceAndInstall();
|
||||||
|
void FinishWorkIfNeeded();
|
||||||
|
const std::string& WorkingStatus() const;
|
||||||
|
|
||||||
|
Options options_;
|
||||||
|
FetchCallback fetch_;
|
||||||
|
PickSourceCallback pick_source_;
|
||||||
|
InstallCallback install_;
|
||||||
|
CompleteCallback complete_;
|
||||||
|
std::thread work_thread_;
|
||||||
|
std::atomic<bool> work_done_{false};
|
||||||
|
std::atomic<bool> work_ok_{false};
|
||||||
|
std::atomic<uint64_t> copied_bytes_{0};
|
||||||
|
std::atomic<uint64_t> total_bytes_{0};
|
||||||
|
State state_ = State::kWaitingForChoice;
|
||||||
|
bool working_is_fetch_ = false;
|
||||||
|
std::filesystem::path source_path_;
|
||||||
|
std::string status_;
|
||||||
|
std::string error_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace rex::ui
|
||||||
|
|
@ -20,6 +20,7 @@ set(REXUI_CORE_SOURCES
|
||||||
set(REXUI_OVERLAY_SOURCES
|
set(REXUI_OVERLAY_SOURCES
|
||||||
overlay/debug_overlay.cpp
|
overlay/debug_overlay.cpp
|
||||||
overlay/console_overlay.cpp
|
overlay/console_overlay.cpp
|
||||||
|
overlay/acquire_wizard_overlay.cpp
|
||||||
overlay/install_wizard_overlay.cpp
|
overlay/install_wizard_overlay.cpp
|
||||||
overlay/simple_settings_overlay.cpp
|
overlay/simple_settings_overlay.cpp
|
||||||
overlay/settings_overlay.cpp
|
overlay/settings_overlay.cpp
|
||||||
|
|
|
||||||
182
src/ui/overlay/acquire_wizard_overlay.cpp
Normal file
182
src/ui/overlay/acquire_wizard_overlay.cpp
Normal file
|
|
@ -0,0 +1,182 @@
|
||||||
|
/**
|
||||||
|
* @file ui/overlay/acquire_wizard_overlay.cpp
|
||||||
|
*
|
||||||
|
* @brief Generic pre-runtime acquisition dialog.
|
||||||
|
*/
|
||||||
|
#include <rex/ui/overlay/acquire_wizard_overlay.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
namespace rex::ui {
|
||||||
|
|
||||||
|
AcquireWizardDialog::AcquireWizardDialog(ImGuiDrawer* drawer, Options options, FetchCallback fetch,
|
||||||
|
PickSourceCallback pick_source, InstallCallback install,
|
||||||
|
CompleteCallback complete)
|
||||||
|
: ImGuiDialog(drawer),
|
||||||
|
options_(std::move(options)),
|
||||||
|
fetch_(std::move(fetch)),
|
||||||
|
pick_source_(std::move(pick_source)),
|
||||||
|
install_(std::move(install)),
|
||||||
|
complete_(std::move(complete)),
|
||||||
|
status_(options_.initial_status) {}
|
||||||
|
|
||||||
|
void AcquireWizardDialog::OnClose() {
|
||||||
|
if (work_thread_.joinable()) {
|
||||||
|
work_thread_.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AcquireWizardDialog::StartWork(std::function<bool(std::string&)> work,
|
||||||
|
std::string busy_status) {
|
||||||
|
if (work_thread_.joinable()) {
|
||||||
|
work_thread_.join();
|
||||||
|
}
|
||||||
|
copied_bytes_ = 0;
|
||||||
|
total_bytes_ = 0;
|
||||||
|
work_done_ = false;
|
||||||
|
work_ok_ = false;
|
||||||
|
error_.clear();
|
||||||
|
state_ = State::kWorking;
|
||||||
|
status_ = std::move(busy_status);
|
||||||
|
work_thread_ = std::thread([this, work = std::move(work)]() {
|
||||||
|
std::string error;
|
||||||
|
const bool ok = work(error);
|
||||||
|
error_ = std::move(error);
|
||||||
|
work_ok_ = ok;
|
||||||
|
work_done_ = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void AcquireWizardDialog::StartFetch() {
|
||||||
|
if (!fetch_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
source_path_.clear();
|
||||||
|
working_is_fetch_ = true;
|
||||||
|
StartWork([this](std::string& error) { return fetch_(copied_bytes_, total_bytes_, error); },
|
||||||
|
options_.fetch_working_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AcquireWizardDialog::PickSourceAndInstall() {
|
||||||
|
if (!pick_source_ || !install_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto source_path = pick_source_();
|
||||||
|
if (source_path.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
source_path_ = std::move(source_path);
|
||||||
|
working_is_fetch_ = false;
|
||||||
|
StartWork(
|
||||||
|
[this](std::string& error) {
|
||||||
|
return install_(source_path_, copied_bytes_, total_bytes_, error);
|
||||||
|
},
|
||||||
|
options_.install_working_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& AcquireWizardDialog::WorkingStatus() const {
|
||||||
|
// Before any bytes arrive during a fetch, the connection may be waiting on
|
||||||
|
// the server's first byte; surface that instead of an idle "downloading".
|
||||||
|
if (working_is_fetch_ && !options_.fetch_connecting_status.empty() &&
|
||||||
|
copied_bytes_.load(std::memory_order_relaxed) == 0) {
|
||||||
|
return options_.fetch_connecting_status;
|
||||||
|
}
|
||||||
|
return status_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AcquireWizardDialog::FinishWorkIfNeeded() {
|
||||||
|
if (state_ != State::kWorking || !work_done_.load(std::memory_order_acquire)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (work_thread_.joinable()) {
|
||||||
|
work_thread_.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (work_ok_.load(std::memory_order_acquire)) {
|
||||||
|
state_ = State::kDone;
|
||||||
|
status_ = options_.done_status;
|
||||||
|
} else {
|
||||||
|
state_ = State::kFailed;
|
||||||
|
status_ = options_.initial_status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AcquireWizardDialog::OnDraw(ImGuiIO& io) {
|
||||||
|
FinishWorkIfNeeded();
|
||||||
|
|
||||||
|
const float width = std::min(760.0f, std::max(460.0f, io.DisplaySize.x - 64.0f));
|
||||||
|
ImGui::SetNextWindowPos(ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f),
|
||||||
|
ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
|
||||||
|
ImGui::SetNextWindowSize(ImVec2(width, 0.0f), ImGuiCond_Always);
|
||||||
|
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(24.0f, 22.0f));
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(14.0f, 9.0f));
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(12.0f, 12.0f));
|
||||||
|
ImGui::PushFont(nullptr, 18.0f);
|
||||||
|
|
||||||
|
if (ImGui::Begin(options_.title.c_str(), nullptr,
|
||||||
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize |
|
||||||
|
ImGuiWindowFlags_AlwaysAutoResize)) {
|
||||||
|
if (!options_.intro.empty()) {
|
||||||
|
ImGui::TextWrapped("%s", options_.intro.c_str());
|
||||||
|
ImGui::Spacing();
|
||||||
|
}
|
||||||
|
if (!options_.target_directory.empty()) {
|
||||||
|
ImGui::TextWrapped("Install directory: %s", options_.target_directory.c_str());
|
||||||
|
}
|
||||||
|
ImGui::TextWrapped("%s", WorkingStatus().c_str());
|
||||||
|
|
||||||
|
if (!source_path_.empty()) {
|
||||||
|
ImGui::TextWrapped("Source: %s", source_path_.string().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state_ == State::kWorking) {
|
||||||
|
const uint64_t total = total_bytes_.load(std::memory_order_relaxed);
|
||||||
|
const uint64_t copied = copied_bytes_.load(std::memory_order_relaxed);
|
||||||
|
const float progress =
|
||||||
|
total == 0 ? 0.0f
|
||||||
|
: std::clamp(static_cast<float>(double(copied) / double(total)), 0.0f, 1.0f);
|
||||||
|
ImGui::ProgressBar(progress, ImVec2(-1.0f, 28.0f));
|
||||||
|
} else if (state_ == State::kFailed) {
|
||||||
|
ImGui::TextColored(ImVec4(0.95f, 0.28f, 0.24f, 1.0f), "%s", error_.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::Spacing();
|
||||||
|
if (state_ == State::kWaitingForChoice || state_ == State::kFailed) {
|
||||||
|
bool first_button = true;
|
||||||
|
if (fetch_ && !options_.fetch_button_label.empty()) {
|
||||||
|
if (ImGui::Button(options_.fetch_button_label.c_str(), ImVec2(220.0f, 42.0f))) {
|
||||||
|
StartFetch();
|
||||||
|
}
|
||||||
|
first_button = false;
|
||||||
|
}
|
||||||
|
if (pick_source_ && install_ && !options_.pick_button_label.empty()) {
|
||||||
|
if (!first_button) {
|
||||||
|
ImGui::SameLine();
|
||||||
|
}
|
||||||
|
if (ImGui::Button(options_.pick_button_label.c_str(), ImVec2(180.0f, 42.0f))) {
|
||||||
|
PickSourceAndInstall();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (state_ == State::kDone) {
|
||||||
|
if (ImGui::Button(options_.done_button_label.c_str(), ImVec2(160.0f, 42.0f))) {
|
||||||
|
auto complete = std::move(complete_);
|
||||||
|
Close();
|
||||||
|
if (complete) {
|
||||||
|
complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::End();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::PopFont();
|
||||||
|
ImGui::PopStyleVar(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace rex::ui
|
||||||
Loading…
Add table
Reference in a new issue