console and mining tab visual improvements
This commit is contained in:
93
src/app.cpp
93
src/app.cpp
@@ -104,6 +104,21 @@ bool App::init()
|
|||||||
DEBUG_LOGF("Warning: Could not load settings, using defaults\n");
|
DEBUG_LOGF("Warning: Could not load settings, using defaults\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply saved user font scale so fonts are correct on first reload
|
||||||
|
{
|
||||||
|
float fs = settings_->getFontScale();
|
||||||
|
if (fs > 1.0f) {
|
||||||
|
ui::Layout::setUserFontScale(fs);
|
||||||
|
// Fonts were loaded at default scale in Init(); rebuild now.
|
||||||
|
auto& typo = ui::material::Typography::instance();
|
||||||
|
ImGuiIO& io = ImGui::GetIO();
|
||||||
|
typo.reload(io, typo.getDpiScale());
|
||||||
|
// Consume the flag so App::update() doesn't double-reload
|
||||||
|
ui::Layout::consumeUserFontReload();
|
||||||
|
DEBUG_LOGF("App: Applied saved font scale %.1fx\n", fs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure ObsidianDragon config directory and template files exist
|
// Ensure ObsidianDragon config directory and template files exist
|
||||||
util::Platform::ensureObsidianDragonSetup();
|
util::Platform::ensureObsidianDragonSetup();
|
||||||
|
|
||||||
@@ -190,6 +205,39 @@ bool App::init()
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Pre-frame: font atlas rebuilds (must run BEFORE ImGui::NewFrame)
|
||||||
|
// ============================================================================
|
||||||
|
void App::preFrame()
|
||||||
|
{
|
||||||
|
ImGuiIO& io = ImGui::GetIO();
|
||||||
|
|
||||||
|
// Hot-reload unified UI schema
|
||||||
|
{
|
||||||
|
PERF_SCOPE("PreFrame.SchemaHotReload");
|
||||||
|
ui::schema::UISchema::instance().pollForChanges();
|
||||||
|
ui::schema::UISchema::instance().applyIfDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh balance layout config after schema reload
|
||||||
|
ui::RefreshBalanceLayoutConfig();
|
||||||
|
|
||||||
|
// If font sizes changed in the TOML, rebuild the font atlas
|
||||||
|
if (ui::schema::UISchema::instance().consumeFontsChanged()) {
|
||||||
|
auto& typo = ui::material::Typography::instance();
|
||||||
|
typo.reload(io, typo.getDpiScale());
|
||||||
|
DEBUG_LOGF("App: Font atlas rebuilt after hot-reload\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user changed font scale in Settings, rebuild the font atlas
|
||||||
|
if (ui::Layout::consumeUserFontReload()) {
|
||||||
|
auto& typo = ui::material::Typography::instance();
|
||||||
|
typo.reload(io, typo.getDpiScale());
|
||||||
|
DEBUG_LOGF("App: Font atlas rebuilt after user font-scale change (%.1fx)\n",
|
||||||
|
ui::Layout::userFontScale());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void App::update()
|
void App::update()
|
||||||
{
|
{
|
||||||
PERF_SCOPE("Update.Total");
|
PERF_SCOPE("Update.Total");
|
||||||
@@ -206,6 +254,9 @@ void App::update()
|
|||||||
if (worker_) {
|
if (worker_) {
|
||||||
worker_->drainResults();
|
worker_->drainResults();
|
||||||
}
|
}
|
||||||
|
if (fast_worker_) {
|
||||||
|
fast_worker_->drainResults();
|
||||||
|
}
|
||||||
|
|
||||||
// Auto-lock check (only when connected + encrypted + unlocked)
|
// Auto-lock check (only when connected + encrypted + unlocked)
|
||||||
if (state_.connected && state_.isUnlocked()) {
|
if (state_.connected && state_.isUnlocked()) {
|
||||||
@@ -218,23 +269,6 @@ void App::update()
|
|||||||
state_.rebuildAddressList();
|
state_.rebuildAddressList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hot-reload unified UI schema
|
|
||||||
{
|
|
||||||
PERF_SCOPE("Update.SchemaHotReload");
|
|
||||||
ui::schema::UISchema::instance().pollForChanges();
|
|
||||||
ui::schema::UISchema::instance().applyIfDirty();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh balance layout config after schema reload
|
|
||||||
ui::RefreshBalanceLayoutConfig();
|
|
||||||
|
|
||||||
// If font sizes changed in the JSON, rebuild the font atlas
|
|
||||||
if (ui::schema::UISchema::instance().consumeFontsChanged()) {
|
|
||||||
auto& typo = ui::material::Typography::instance();
|
|
||||||
typo.reload(io, typo.getDpiScale());
|
|
||||||
DEBUG_LOGF("App: Font atlas rebuilt after hot-reload\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update timers
|
// Update timers
|
||||||
refresh_timer_ += io.DeltaTime;
|
refresh_timer_ += io.DeltaTime;
|
||||||
price_timer_ += io.DeltaTime;
|
price_timer_ += io.DeltaTime;
|
||||||
@@ -775,7 +809,12 @@ void App::render()
|
|||||||
ui::RenderMarketTab(this);
|
ui::RenderMarketTab(this);
|
||||||
break;
|
break;
|
||||||
case ui::NavPage::Console:
|
case ui::NavPage::Console:
|
||||||
console_tab_.render(embedded_daemon_.get(), rpc_.get(), worker_.get(), xmrig_manager_.get());
|
// Use fast-lane worker for console commands to avoid head-of-line
|
||||||
|
// blocking behind the consolidated refreshData() batch.
|
||||||
|
console_tab_.render(embedded_daemon_.get(),
|
||||||
|
fast_rpc_ ? fast_rpc_.get() : rpc_.get(),
|
||||||
|
fast_worker_ ? fast_worker_.get() : worker_.get(),
|
||||||
|
xmrig_manager_.get());
|
||||||
break;
|
break;
|
||||||
case ui::NavPage::Settings:
|
case ui::NavPage::Settings:
|
||||||
ui::RenderSettingsPage(this);
|
ui::RenderSettingsPage(this);
|
||||||
@@ -1723,6 +1762,9 @@ void App::beginShutdown()
|
|||||||
if (worker_) {
|
if (worker_) {
|
||||||
worker_->requestStop();
|
worker_->requestStop();
|
||||||
}
|
}
|
||||||
|
if (fast_worker_) {
|
||||||
|
fast_worker_->requestStop();
|
||||||
|
}
|
||||||
|
|
||||||
// Stop xmrig pool miner before stopping the daemon
|
// Stop xmrig pool miner before stopping the daemon
|
||||||
if (xmrig_manager_ && xmrig_manager_->isRunning()) {
|
if (xmrig_manager_ && xmrig_manager_->isRunning()) {
|
||||||
@@ -2260,6 +2302,9 @@ void App::shutdown()
|
|||||||
if (worker_) {
|
if (worker_) {
|
||||||
worker_->stop();
|
worker_->stop();
|
||||||
}
|
}
|
||||||
|
if (fast_worker_) {
|
||||||
|
fast_worker_->stop();
|
||||||
|
}
|
||||||
if (settings_) {
|
if (settings_) {
|
||||||
settings_->save();
|
settings_->save();
|
||||||
}
|
}
|
||||||
@@ -2269,6 +2314,9 @@ void App::shutdown()
|
|||||||
if (rpc_) {
|
if (rpc_) {
|
||||||
rpc_->disconnect();
|
rpc_->disconnect();
|
||||||
}
|
}
|
||||||
|
if (fast_rpc_) {
|
||||||
|
fast_rpc_->disconnect();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2288,10 +2336,16 @@ void App::shutdown()
|
|||||||
if (worker_) {
|
if (worker_) {
|
||||||
worker_->stop();
|
worker_->stop();
|
||||||
}
|
}
|
||||||
|
if (fast_worker_) {
|
||||||
|
fast_worker_->stop();
|
||||||
|
}
|
||||||
// Disconnect RPC after worker is fully stopped (safe — no curl in flight)
|
// Disconnect RPC after worker is fully stopped (safe — no curl in flight)
|
||||||
if (rpc_) {
|
if (rpc_) {
|
||||||
rpc_->disconnect();
|
rpc_->disconnect();
|
||||||
}
|
}
|
||||||
|
if (fast_rpc_) {
|
||||||
|
fast_rpc_->disconnect();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
@@ -2310,7 +2364,8 @@ bool App::hasPinVault() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool App::hasPendingRPCResults() const {
|
bool App::hasPendingRPCResults() const {
|
||||||
return worker_ && worker_->hasPendingResults();
|
return (worker_ && worker_->hasPendingResults())
|
||||||
|
|| (fast_worker_ && fast_worker_->hasPendingResults());
|
||||||
}
|
}
|
||||||
void App::restartDaemon()
|
void App::restartDaemon()
|
||||||
{
|
{
|
||||||
|
|||||||
21
src/app.h
21
src/app.h
@@ -11,6 +11,7 @@
|
|||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include "data/wallet_state.h"
|
#include "data/wallet_state.h"
|
||||||
|
#include "rpc/connection.h"
|
||||||
#include "ui/sidebar.h"
|
#include "ui/sidebar.h"
|
||||||
#include "ui/windows/console_tab.h"
|
#include "ui/windows/console_tab.h"
|
||||||
#include "imgui.h"
|
#include "imgui.h"
|
||||||
@@ -20,7 +21,6 @@ namespace dragonx {
|
|||||||
namespace rpc {
|
namespace rpc {
|
||||||
class RPCClient;
|
class RPCClient;
|
||||||
class RPCWorker;
|
class RPCWorker;
|
||||||
struct ConnectionConfig;
|
|
||||||
}
|
}
|
||||||
namespace config { class Settings; }
|
namespace config { class Settings; }
|
||||||
namespace daemon { class EmbeddedDaemon; class XmrigManager; }
|
namespace daemon { class EmbeddedDaemon; class XmrigManager; }
|
||||||
@@ -79,6 +79,15 @@ public:
|
|||||||
*/
|
*/
|
||||||
void update();
|
void update();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Pre-frame tasks that must run BEFORE ImGui::NewFrame().
|
||||||
|
*
|
||||||
|
* Font atlas rebuilds (hot-reload, user font scale changes) must
|
||||||
|
* happen before NewFrame() because NewFrame() caches font pointers.
|
||||||
|
* Rebuilding mid-frame causes dangling pointer crashes.
|
||||||
|
*/
|
||||||
|
void preFrame();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Render the application UI (called every frame)
|
* @brief Render the application UI (called every frame)
|
||||||
*/
|
*/
|
||||||
@@ -313,6 +322,16 @@ private:
|
|||||||
// Subsystems
|
// Subsystems
|
||||||
std::unique_ptr<rpc::RPCClient> rpc_;
|
std::unique_ptr<rpc::RPCClient> rpc_;
|
||||||
std::unique_ptr<rpc::RPCWorker> worker_;
|
std::unique_ptr<rpc::RPCWorker> worker_;
|
||||||
|
|
||||||
|
// Fast-lane: dedicated RPC connection + worker for 1-second mining polls.
|
||||||
|
// Runs on its own thread with its own curl handle so it never blocks behind
|
||||||
|
// the main refresh batch.
|
||||||
|
std::unique_ptr<rpc::RPCClient> fast_rpc_;
|
||||||
|
std::unique_ptr<rpc::RPCWorker> fast_worker_;
|
||||||
|
|
||||||
|
// Saved connection credentials (needed to open the fast-lane connection)
|
||||||
|
rpc::ConnectionConfig saved_config_;
|
||||||
|
|
||||||
std::unique_ptr<config::Settings> settings_;
|
std::unique_ptr<config::Settings> settings_;
|
||||||
std::unique_ptr<daemon::EmbeddedDaemon> embedded_daemon_;
|
std::unique_ptr<daemon::EmbeddedDaemon> embedded_daemon_;
|
||||||
std::unique_ptr<daemon::XmrigManager> xmrig_manager_;
|
std::unique_ptr<daemon::XmrigManager> xmrig_manager_;
|
||||||
|
|||||||
@@ -84,8 +84,9 @@ void App::tryConnect()
|
|||||||
worker_->post([this, config, daemonStarting, externalDetected]() -> rpc::RPCWorker::MainCb {
|
worker_->post([this, config, daemonStarting, externalDetected]() -> rpc::RPCWorker::MainCb {
|
||||||
bool connected = rpc_->connect(config.host, config.port, config.rpcuser, config.rpcpassword);
|
bool connected = rpc_->connect(config.host, config.port, config.rpcuser, config.rpcpassword);
|
||||||
|
|
||||||
return [this, connected, daemonStarting, externalDetected]() {
|
return [this, config, connected, daemonStarting, externalDetected]() {
|
||||||
if (connected) {
|
if (connected) {
|
||||||
|
saved_config_ = config; // save for fast-lane connection
|
||||||
onConnected();
|
onConnected();
|
||||||
} else {
|
} else {
|
||||||
if (daemonStarting) {
|
if (daemonStarting) {
|
||||||
@@ -194,6 +195,29 @@ void App::onConnected()
|
|||||||
// Addresses are unknown on fresh connect — force a fetch
|
// Addresses are unknown on fresh connect — force a fetch
|
||||||
addresses_dirty_ = true;
|
addresses_dirty_ = true;
|
||||||
|
|
||||||
|
// Start the fast-lane RPC connection (dedicated to 1-second mining polls).
|
||||||
|
// Uses its own curl handle + worker thread so getlocalsolps never blocks
|
||||||
|
// behind the main refresh batch.
|
||||||
|
if (!fast_rpc_) {
|
||||||
|
fast_rpc_ = std::make_unique<rpc::RPCClient>();
|
||||||
|
}
|
||||||
|
if (!fast_worker_) {
|
||||||
|
fast_worker_ = std::make_unique<rpc::RPCWorker>();
|
||||||
|
fast_worker_->start();
|
||||||
|
}
|
||||||
|
// Connect on the fast worker's own thread (non-blocking to main)
|
||||||
|
fast_worker_->post([this]() -> rpc::RPCWorker::MainCb {
|
||||||
|
bool ok = fast_rpc_->connect(saved_config_.host, saved_config_.port,
|
||||||
|
saved_config_.rpcuser, saved_config_.rpcpassword);
|
||||||
|
return [ok]() {
|
||||||
|
if (!ok) {
|
||||||
|
DEBUG_LOGF("[FastLane] Failed to connect secondary RPC client\\n");
|
||||||
|
} else {
|
||||||
|
DEBUG_LOGF("[FastLane] Secondary RPC client connected\\n");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// Initial data refresh
|
// Initial data refresh
|
||||||
refreshData();
|
refreshData();
|
||||||
refreshMarketData();
|
refreshMarketData();
|
||||||
@@ -204,6 +228,14 @@ void App::onDisconnected(const std::string& reason)
|
|||||||
state_.connected = false;
|
state_.connected = false;
|
||||||
state_.clear();
|
state_.clear();
|
||||||
connection_status_ = reason;
|
connection_status_ = reason;
|
||||||
|
|
||||||
|
// Tear down the fast-lane connection
|
||||||
|
if (fast_worker_) {
|
||||||
|
fast_worker_->stop();
|
||||||
|
}
|
||||||
|
if (fast_rpc_) {
|
||||||
|
fast_rpc_->disconnect();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -217,35 +249,424 @@ void App::refreshData()
|
|||||||
// Prevent overlapping refreshes — skip if one is still running
|
// Prevent overlapping refreshes — skip if one is still running
|
||||||
if (refresh_in_progress_.exchange(true)) return;
|
if (refresh_in_progress_.exchange(true)) return;
|
||||||
|
|
||||||
refreshBalance();
|
// Capture decision flags on the main thread before posting to worker.
|
||||||
|
const bool doAddresses = addresses_dirty_;
|
||||||
|
const bool doPeers = (current_page_ == ui::NavPage::Peers);
|
||||||
|
const bool doEncrypt = !encryption_state_prefetched_;
|
||||||
|
if (encryption_state_prefetched_) encryption_state_prefetched_ = false;
|
||||||
|
|
||||||
// Addresses: only re-fetch when explicitly dirtied (new address, send, etc.)
|
// P4a: Skip transactions if no new blocks since last full fetch
|
||||||
if (addresses_dirty_) {
|
const int currentBlocks = state_.sync.blocks;
|
||||||
refreshAddresses();
|
const bool doTransactions = (last_tx_block_height_ < 0
|
||||||
}
|
|| currentBlocks != last_tx_block_height_
|
||||||
|
|| state_.transactions.empty());
|
||||||
refreshTransactions();
|
|
||||||
|
|
||||||
// Mining: handled by the 1-second fast_refresh_timer_ — skip here to
|
|
||||||
// avoid queuing a redundant call every 5 seconds.
|
|
||||||
|
|
||||||
// Peers: only fetch when the Peers tab is visible
|
|
||||||
if (current_page_ == ui::NavPage::Peers) {
|
|
||||||
refreshPeerInfo();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encryption state: skip if onConnected() already prefetched it
|
|
||||||
if (encryption_state_prefetched_) {
|
|
||||||
encryption_state_prefetched_ = false;
|
|
||||||
} else {
|
|
||||||
refreshWalletEncryptionState();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear the guard after all tasks are posted (they'll execute sequentially
|
// Snapshot z-addresses for transaction fetch (needed on worker thread)
|
||||||
// on the worker thread, so the last one to finish signals completion).
|
std::vector<std::string> txZAddrs;
|
||||||
// We post a sentinel task that clears the flag after all refresh work.
|
if (doTransactions) {
|
||||||
worker_->post([this]() -> rpc::RPCWorker::MainCb {
|
for (const auto& za : state_.z_addresses) {
|
||||||
return [this]() {
|
if (!za.address.empty()) txZAddrs.push_back(za.address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// P4b: Collect txids that are fully enriched (skip re-enrichment)
|
||||||
|
std::set<std::string> fullyEnriched;
|
||||||
|
if (doTransactions) {
|
||||||
|
for (const auto& tx : state_.transactions) {
|
||||||
|
if (tx.confirmations > 6 && tx.timestamp != 0) {
|
||||||
|
fullyEnriched.insert(tx.txid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single consolidated worker task — all RPC calls happen back-to-back
|
||||||
|
// on a single thread with no inter-task queue overhead.
|
||||||
|
worker_->post([this, doAddresses, doPeers, doEncrypt, doTransactions,
|
||||||
|
currentBlocks, txZAddrs = std::move(txZAddrs),
|
||||||
|
fullyEnriched = std::move(fullyEnriched)]() -> rpc::RPCWorker::MainCb {
|
||||||
|
// ================================================================
|
||||||
|
// Phase 1: Balance + blockchain info
|
||||||
|
// ================================================================
|
||||||
|
json totalBal, blockInfo;
|
||||||
|
bool balOk = false, blockOk = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
totalBal = rpc_->call("z_gettotalbalance");
|
||||||
|
balOk = true;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
DEBUG_LOGF("Balance error: %s\n", e.what());
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
blockInfo = rpc_->call("getblockchaininfo");
|
||||||
|
blockOk = true;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
DEBUG_LOGF("BlockchainInfo error: %s\n", e.what());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// Phase 2: Addresses (only when dirtied)
|
||||||
|
// ================================================================
|
||||||
|
std::vector<AddressInfo> zAddrs, tAddrs;
|
||||||
|
bool addrOk = false;
|
||||||
|
if (doAddresses) {
|
||||||
|
addrOk = true;
|
||||||
|
// z-addresses
|
||||||
|
try {
|
||||||
|
json zList = rpc_->call("z_listaddresses");
|
||||||
|
for (const auto& addr : zList) {
|
||||||
|
AddressInfo info;
|
||||||
|
info.address = addr.get<std::string>();
|
||||||
|
info.type = "shielded";
|
||||||
|
zAddrs.push_back(info);
|
||||||
|
}
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
DEBUG_LOGF("z_listaddresses error: %s\n", e.what());
|
||||||
|
}
|
||||||
|
// z-balances via z_listunspent (single call)
|
||||||
|
try {
|
||||||
|
json unspent = rpc_->call("z_listunspent");
|
||||||
|
std::map<std::string, double> zBalances;
|
||||||
|
for (const auto& utxo : unspent) {
|
||||||
|
if (utxo.contains("address") && utxo.contains("amount")) {
|
||||||
|
zBalances[utxo["address"].get<std::string>()] += utxo["amount"].get<double>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (auto& info : zAddrs) {
|
||||||
|
auto it = zBalances.find(info.address);
|
||||||
|
if (it != zBalances.end()) info.balance = it->second;
|
||||||
|
}
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
DEBUG_LOGF("z_listunspent unavailable (%s), falling back to z_getbalance\n", e.what());
|
||||||
|
for (auto& info : zAddrs) {
|
||||||
|
try {
|
||||||
|
json bal = rpc_->call("z_getbalance", json::array({info.address}));
|
||||||
|
if (!bal.is_null()) info.balance = bal.get<double>();
|
||||||
|
} catch (...) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// t-addresses
|
||||||
|
try {
|
||||||
|
json tList = rpc_->call("getaddressesbyaccount", json::array({""}));
|
||||||
|
for (const auto& addr : tList) {
|
||||||
|
AddressInfo info;
|
||||||
|
info.address = addr.get<std::string>();
|
||||||
|
info.type = "transparent";
|
||||||
|
tAddrs.push_back(info);
|
||||||
|
}
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
DEBUG_LOGF("getaddressesbyaccount error: %s\n", e.what());
|
||||||
|
}
|
||||||
|
// t-balances via listunspent
|
||||||
|
try {
|
||||||
|
json utxos = rpc_->call("listunspent");
|
||||||
|
std::map<std::string, double> tBalances;
|
||||||
|
for (const auto& utxo : utxos) {
|
||||||
|
tBalances[utxo["address"].get<std::string>()] += utxo["amount"].get<double>();
|
||||||
|
}
|
||||||
|
for (auto& info : tAddrs) {
|
||||||
|
auto it = tBalances.find(info.address);
|
||||||
|
if (it != tBalances.end()) info.balance = it->second;
|
||||||
|
}
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
DEBUG_LOGF("listunspent error: %s\n", e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// Phase 3: Transactions (only when new blocks)
|
||||||
|
// ================================================================
|
||||||
|
std::vector<TransactionInfo> txns;
|
||||||
|
bool txOk = false;
|
||||||
|
if (doTransactions) {
|
||||||
|
txOk = true;
|
||||||
|
std::set<std::string> knownTxids;
|
||||||
|
|
||||||
|
// Phase 3a: transparent transactions
|
||||||
|
try {
|
||||||
|
json result = rpc_->call("listtransactions", json::array({"", 9999}));
|
||||||
|
for (const auto& tx : result) {
|
||||||
|
TransactionInfo info;
|
||||||
|
if (tx.contains("txid")) info.txid = tx["txid"].get<std::string>();
|
||||||
|
if (tx.contains("category")) info.type = tx["category"].get<std::string>();
|
||||||
|
if (tx.contains("amount")) info.amount = tx["amount"].get<double>();
|
||||||
|
if (tx.contains("time")) info.timestamp = tx["time"].get<int64_t>();
|
||||||
|
if (tx.contains("confirmations")) info.confirmations = tx["confirmations"].get<int>();
|
||||||
|
if (tx.contains("address")) info.address = tx["address"].get<std::string>();
|
||||||
|
knownTxids.insert(info.txid);
|
||||||
|
txns.push_back(info);
|
||||||
|
}
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
DEBUG_LOGF("listtransactions error: %s\n", e.what());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 3b: shielded receives
|
||||||
|
for (const auto& addr : txZAddrs) {
|
||||||
|
try {
|
||||||
|
json zresult = rpc_->call("z_listreceivedbyaddress", json::array({addr, 0}));
|
||||||
|
if (zresult.is_null() || !zresult.is_array()) continue;
|
||||||
|
for (const auto& note : zresult) {
|
||||||
|
std::string txid;
|
||||||
|
if (note.contains("txid")) txid = note["txid"].get<std::string>();
|
||||||
|
if (txid.empty()) continue;
|
||||||
|
if (note.contains("change") && note["change"].get<bool>()) continue;
|
||||||
|
bool dominated = false;
|
||||||
|
for (const auto& existing : txns) {
|
||||||
|
if (existing.txid == txid && existing.type == "receive") {
|
||||||
|
dominated = true; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dominated) continue;
|
||||||
|
TransactionInfo info;
|
||||||
|
info.txid = txid;
|
||||||
|
info.type = "receive";
|
||||||
|
info.address = addr;
|
||||||
|
if (note.contains("amount")) info.amount = note["amount"].get<double>();
|
||||||
|
if (note.contains("confirmations")) info.confirmations = note["confirmations"].get<int>();
|
||||||
|
if (note.contains("time")) info.timestamp = note["time"].get<int64_t>();
|
||||||
|
if (note.contains("memoStr")) info.memo = note["memoStr"].get<std::string>();
|
||||||
|
knownTxids.insert(txid);
|
||||||
|
txns.push_back(info);
|
||||||
|
}
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
DEBUG_LOGF("z_listreceivedbyaddress error for %s: %s\n",
|
||||||
|
addr.substr(0, 12).c_str(), e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 3c: detect shielded sends via z_viewtransaction
|
||||||
|
for (const std::string& txid : knownTxids) {
|
||||||
|
if (fullyEnriched.count(txid)) continue;
|
||||||
|
try {
|
||||||
|
json vtx = rpc_->call("z_viewtransaction", json::array({txid}));
|
||||||
|
if (vtx.is_null() || !vtx.is_object()) continue;
|
||||||
|
if (vtx.contains("outputs") && vtx["outputs"].is_array()) {
|
||||||
|
for (const auto& output : vtx["outputs"]) {
|
||||||
|
bool outgoing = false;
|
||||||
|
if (output.contains("outgoing"))
|
||||||
|
outgoing = output["outgoing"].get<bool>();
|
||||||
|
if (!outgoing) continue;
|
||||||
|
std::string destAddr;
|
||||||
|
if (output.contains("address"))
|
||||||
|
destAddr = output["address"].get<std::string>();
|
||||||
|
double value = 0.0;
|
||||||
|
if (output.contains("value"))
|
||||||
|
value = output["value"].get<double>();
|
||||||
|
bool alreadyTracked = false;
|
||||||
|
for (const auto& existing : txns) {
|
||||||
|
if (existing.txid == txid && existing.type == "send"
|
||||||
|
&& std::abs(existing.amount + value) < 0.00000001) {
|
||||||
|
alreadyTracked = true; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (alreadyTracked) continue;
|
||||||
|
TransactionInfo info;
|
||||||
|
info.txid = txid;
|
||||||
|
info.type = "send";
|
||||||
|
info.address = destAddr;
|
||||||
|
info.amount = -value;
|
||||||
|
if (output.contains("memoStr"))
|
||||||
|
info.memo = output["memoStr"].get<std::string>();
|
||||||
|
for (const auto& existing : txns) {
|
||||||
|
if (existing.txid == txid) {
|
||||||
|
info.confirmations = existing.confirmations;
|
||||||
|
info.timestamp = existing.timestamp;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (info.timestamp == 0) {
|
||||||
|
try {
|
||||||
|
json rawtx = rpc_->call("gettransaction", json::array({txid}));
|
||||||
|
if (!rawtx.is_null() && rawtx.contains("time"))
|
||||||
|
info.timestamp = rawtx["time"].get<int64_t>();
|
||||||
|
if (!rawtx.is_null() && rawtx.contains("confirmations"))
|
||||||
|
info.confirmations = rawtx["confirmations"].get<int>();
|
||||||
|
} catch (...) {}
|
||||||
|
}
|
||||||
|
if (vtx.contains("spends") && vtx["spends"].is_array()) {
|
||||||
|
for (const auto& spend : vtx["spends"]) {
|
||||||
|
if (spend.contains("address")) {
|
||||||
|
info.from_address = spend["address"].get<std::string>();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
txns.push_back(info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
(void)e; // z_viewtransaction may not be available for all txids
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::sort(txns.begin(), txns.end(),
|
||||||
|
[](const TransactionInfo& a, const TransactionInfo& b) {
|
||||||
|
return a.timestamp > b.timestamp;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// Phase 4: Peers (only when tab is active)
|
||||||
|
// ================================================================
|
||||||
|
std::vector<PeerInfo> peers;
|
||||||
|
std::vector<BannedPeer> bannedPeers;
|
||||||
|
bool peerOk = false;
|
||||||
|
if (doPeers) {
|
||||||
|
peerOk = true;
|
||||||
|
try {
|
||||||
|
json result = rpc_->call("getpeerinfo");
|
||||||
|
for (const auto& peer : result) {
|
||||||
|
PeerInfo info;
|
||||||
|
if (peer.contains("id")) info.id = peer["id"].get<int>();
|
||||||
|
if (peer.contains("addr")) info.addr = peer["addr"].get<std::string>();
|
||||||
|
if (peer.contains("subver")) info.subver = peer["subver"].get<std::string>();
|
||||||
|
if (peer.contains("services")) info.services = peer["services"].get<std::string>();
|
||||||
|
if (peer.contains("version")) info.version = peer["version"].get<int>();
|
||||||
|
if (peer.contains("conntime")) info.conntime = peer["conntime"].get<int64_t>();
|
||||||
|
if (peer.contains("banscore")) info.banscore = peer["banscore"].get<int>();
|
||||||
|
if (peer.contains("pingtime")) info.pingtime = peer["pingtime"].get<double>();
|
||||||
|
if (peer.contains("bytessent")) info.bytessent = peer["bytessent"].get<int64_t>();
|
||||||
|
if (peer.contains("bytesrecv")) info.bytesrecv = peer["bytesrecv"].get<int64_t>();
|
||||||
|
if (peer.contains("startingheight")) info.startingheight = peer["startingheight"].get<int>();
|
||||||
|
if (peer.contains("synced_headers")) info.synced_headers = peer["synced_headers"].get<int>();
|
||||||
|
if (peer.contains("synced_blocks")) info.synced_blocks = peer["synced_blocks"].get<int>();
|
||||||
|
if (peer.contains("inbound")) info.inbound = peer["inbound"].get<bool>();
|
||||||
|
if (peer.contains("tls_cipher")) info.tls_cipher = peer["tls_cipher"].get<std::string>();
|
||||||
|
if (peer.contains("tls_verified")) info.tls_verified = peer["tls_verified"].get<bool>();
|
||||||
|
peers.push_back(info);
|
||||||
|
}
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
DEBUG_LOGF("getPeerInfo error: %s\n", e.what());
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
json result = rpc_->call("listbanned");
|
||||||
|
for (const auto& ban : result) {
|
||||||
|
BannedPeer info;
|
||||||
|
if (ban.contains("address")) info.address = ban["address"].get<std::string>();
|
||||||
|
if (ban.contains("banned_until")) info.banned_until = ban["banned_until"].get<int64_t>();
|
||||||
|
bannedPeers.push_back(info);
|
||||||
|
}
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
DEBUG_LOGF("listBanned error: %s\n", e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// Phase 5: Wallet encryption state
|
||||||
|
// ================================================================
|
||||||
|
json walletInfo;
|
||||||
|
bool encryptOk = false;
|
||||||
|
if (doEncrypt) {
|
||||||
|
try {
|
||||||
|
walletInfo = rpc_->call("getwalletinfo");
|
||||||
|
encryptOk = true;
|
||||||
|
} catch (...) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// Single main-thread callback — apply ALL results at once
|
||||||
|
// ================================================================
|
||||||
|
return [this, totalBal, blockInfo, balOk, blockOk,
|
||||||
|
zAddrs = std::move(zAddrs), tAddrs = std::move(tAddrs), addrOk,
|
||||||
|
txns = std::move(txns), txOk, currentBlocks,
|
||||||
|
peers = std::move(peers), bannedPeers = std::move(bannedPeers), peerOk,
|
||||||
|
walletInfo, encryptOk]() {
|
||||||
|
// --- Balance ---
|
||||||
|
try {
|
||||||
|
if (balOk) {
|
||||||
|
if (totalBal.contains("private"))
|
||||||
|
state_.shielded_balance = std::stod(totalBal["private"].get<std::string>());
|
||||||
|
if (totalBal.contains("transparent"))
|
||||||
|
state_.transparent_balance = std::stod(totalBal["transparent"].get<std::string>());
|
||||||
|
if (totalBal.contains("total"))
|
||||||
|
state_.total_balance = std::stod(totalBal["total"].get<std::string>());
|
||||||
|
state_.last_balance_update = std::time(nullptr);
|
||||||
|
}
|
||||||
|
if (blockOk) {
|
||||||
|
if (blockInfo.contains("blocks"))
|
||||||
|
state_.sync.blocks = blockInfo["blocks"].get<int>();
|
||||||
|
if (blockInfo.contains("headers"))
|
||||||
|
state_.sync.headers = blockInfo["headers"].get<int>();
|
||||||
|
if (blockInfo.contains("verificationprogress"))
|
||||||
|
state_.sync.verification_progress = blockInfo["verificationprogress"].get<double>();
|
||||||
|
state_.sync.syncing = (state_.sync.blocks < state_.sync.headers - 2);
|
||||||
|
if (blockInfo.contains("longestchain"))
|
||||||
|
state_.longestchain = blockInfo["longestchain"].get<int>();
|
||||||
|
if (blockInfo.contains("notarized"))
|
||||||
|
state_.notarized = blockInfo["notarized"].get<int>();
|
||||||
|
}
|
||||||
|
// Auto-shield transparent funds if enabled
|
||||||
|
if (balOk && settings_ && settings_->getAutoShield() &&
|
||||||
|
state_.transparent_balance > 0.0001 && !state_.sync.syncing &&
|
||||||
|
!auto_shield_pending_.exchange(true)) {
|
||||||
|
std::string targetZAddr;
|
||||||
|
for (const auto& addr : state_.addresses) {
|
||||||
|
if (addr.isShielded()) {
|
||||||
|
targetZAddr = addr.address;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!targetZAddr.empty() && rpc_) {
|
||||||
|
DEBUG_LOGF("[AutoShield] Shielding %.8f DRGX to %s\n",
|
||||||
|
state_.transparent_balance, targetZAddr.c_str());
|
||||||
|
rpc_->z_shieldCoinbase("*", targetZAddr, 0.0001, 50,
|
||||||
|
[this](const json& result) {
|
||||||
|
if (result.contains("opid")) {
|
||||||
|
DEBUG_LOGF("[AutoShield] Started: %s\n",
|
||||||
|
result["opid"].get<std::string>().c_str());
|
||||||
|
}
|
||||||
|
auto_shield_pending_ = false;
|
||||||
|
},
|
||||||
|
[this](const std::string& err) {
|
||||||
|
DEBUG_LOGF("[AutoShield] Error: %s\n", err.c_str());
|
||||||
|
auto_shield_pending_ = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
auto_shield_pending_ = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
DEBUG_LOGF("[refreshData] balance callback error: %s\n", e.what());
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Addresses ---
|
||||||
|
if (addrOk) {
|
||||||
|
state_.z_addresses = std::move(zAddrs);
|
||||||
|
state_.t_addresses = std::move(tAddrs);
|
||||||
|
address_list_dirty_ = true;
|
||||||
|
addresses_dirty_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Transactions ---
|
||||||
|
if (txOk) {
|
||||||
|
state_.transactions = std::move(txns);
|
||||||
|
state_.last_tx_update = std::time(nullptr);
|
||||||
|
last_tx_block_height_ = currentBlocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Peers ---
|
||||||
|
if (peerOk) {
|
||||||
|
state_.peers = std::move(peers);
|
||||||
|
state_.bannedPeers = std::move(bannedPeers);
|
||||||
|
state_.last_peer_update = std::time(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Encryption state ---
|
||||||
|
if (encryptOk) {
|
||||||
|
try {
|
||||||
|
if (walletInfo.contains("unlocked_until")) {
|
||||||
|
state_.encrypted = true;
|
||||||
|
int64_t until = walletInfo["unlocked_until"].get<int64_t>();
|
||||||
|
state_.unlocked_until = until;
|
||||||
|
state_.locked = (until == 0);
|
||||||
|
} else {
|
||||||
|
state_.encrypted = false;
|
||||||
|
state_.locked = false;
|
||||||
|
state_.unlocked_until = 0;
|
||||||
|
}
|
||||||
|
state_.encryption_state_known = true;
|
||||||
|
} catch (...) {}
|
||||||
|
}
|
||||||
|
|
||||||
refresh_in_progress_.store(false, std::memory_order_release);
|
refresh_in_progress_.store(false, std::memory_order_release);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -605,7 +1026,12 @@ void App::refreshTransactions()
|
|||||||
|
|
||||||
void App::refreshMiningInfo()
|
void App::refreshMiningInfo()
|
||||||
{
|
{
|
||||||
if (!worker_ || !rpc_) return;
|
// Use the dedicated fast-lane worker + connection so mining polls
|
||||||
|
// never block behind the main refresh batch. Falls back to the main
|
||||||
|
// worker if the fast lane isn't ready yet (e.g. during initial connect).
|
||||||
|
auto* w = (fast_worker_ && fast_worker_->isRunning()) ? fast_worker_.get() : worker_.get();
|
||||||
|
auto* rpc = (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get();
|
||||||
|
if (!w || !rpc) return;
|
||||||
|
|
||||||
// Prevent worker queue pileup — skip if previous refresh hasn't finished
|
// Prevent worker queue pileup — skip if previous refresh hasn't finished
|
||||||
if (mining_refresh_in_progress_.exchange(true)) return;
|
if (mining_refresh_in_progress_.exchange(true)) return;
|
||||||
@@ -623,13 +1049,13 @@ void App::refreshMiningInfo()
|
|||||||
// p2p_port are static for the lifetime of a connection (set in onConnected).
|
// p2p_port are static for the lifetime of a connection (set in onConnected).
|
||||||
bool doSlowRefresh = (mining_slow_counter_++ % 5 == 0);
|
bool doSlowRefresh = (mining_slow_counter_++ % 5 == 0);
|
||||||
|
|
||||||
worker_->post([this, daemonMemMb, doSlowRefresh]() -> rpc::RPCWorker::MainCb {
|
w->post([this, rpc, daemonMemMb, doSlowRefresh]() -> rpc::RPCWorker::MainCb {
|
||||||
json miningInfo, localHashrateJson;
|
json miningInfo, localHashrateJson;
|
||||||
bool miningOk = false, hashrateOk = false;
|
bool miningOk = false, hashrateOk = false;
|
||||||
|
|
||||||
// Fast path: only getlocalsolps (single RPC call, ~1ms) — returns H/s (RandomX)
|
// Fast path: only getlocalsolps (single RPC call, ~1ms) — returns H/s (RandomX)
|
||||||
try {
|
try {
|
||||||
localHashrateJson = rpc_->call("getlocalsolps");
|
localHashrateJson = rpc->call("getlocalsolps");
|
||||||
hashrateOk = true;
|
hashrateOk = true;
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
DEBUG_LOGF("getLocalHashrate error: %s\n", e.what());
|
DEBUG_LOGF("getLocalHashrate error: %s\n", e.what());
|
||||||
@@ -638,7 +1064,7 @@ void App::refreshMiningInfo()
|
|||||||
// Slow path: getmininginfo every ~5s
|
// Slow path: getmininginfo every ~5s
|
||||||
if (doSlowRefresh) {
|
if (doSlowRefresh) {
|
||||||
try {
|
try {
|
||||||
miningInfo = rpc_->call("getmininginfo");
|
miningInfo = rpc->call("getmininginfo");
|
||||||
miningOk = true;
|
miningOk = true;
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
DEBUG_LOGF("getMiningInfo error: %s\n", e.what());
|
DEBUG_LOGF("getMiningInfo error: %s\n", e.what());
|
||||||
|
|||||||
@@ -212,11 +212,14 @@ void App::unlockWallet(const std::string& passphrase, int timeout) {
|
|||||||
if (!rpc_ || !rpc_->isConnected() || !worker_) return;
|
if (!rpc_ || !rpc_->isConnected() || !worker_) return;
|
||||||
lock_unlock_in_progress_ = true;
|
lock_unlock_in_progress_ = true;
|
||||||
|
|
||||||
worker_->post([this, passphrase, timeout]() -> rpc::RPCWorker::MainCb {
|
// Use fast-lane worker to bypass head-of-line blocking behind refreshData.
|
||||||
|
auto* w = (fast_worker_ && fast_worker_->isRunning()) ? fast_worker_.get() : worker_.get();
|
||||||
|
auto* r = (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get();
|
||||||
|
w->post([this, r, passphrase, timeout]() -> rpc::RPCWorker::MainCb {
|
||||||
bool ok = false;
|
bool ok = false;
|
||||||
std::string err_msg;
|
std::string err_msg;
|
||||||
try {
|
try {
|
||||||
rpc_->call("walletpassphrase", {passphrase, timeout});
|
r->call("walletpassphrase", {passphrase, timeout});
|
||||||
ok = true;
|
ok = true;
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
err_msg = e.what();
|
err_msg = e.what();
|
||||||
@@ -256,10 +259,13 @@ void App::lockWallet() {
|
|||||||
if (lock_unlock_in_progress_) return; // Prevent duplicate async calls
|
if (lock_unlock_in_progress_) return; // Prevent duplicate async calls
|
||||||
lock_unlock_in_progress_ = true;
|
lock_unlock_in_progress_ = true;
|
||||||
|
|
||||||
worker_->post([this]() -> rpc::RPCWorker::MainCb {
|
// Use fast-lane worker to avoid blocking behind refreshData.
|
||||||
|
auto* w = (fast_worker_ && fast_worker_->isRunning()) ? fast_worker_.get() : worker_.get();
|
||||||
|
auto* r = (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get();
|
||||||
|
w->post([this, r]() -> rpc::RPCWorker::MainCb {
|
||||||
bool ok = false;
|
bool ok = false;
|
||||||
try {
|
try {
|
||||||
rpc_->call("walletlock");
|
r->call("walletlock");
|
||||||
ok = true;
|
ok = true;
|
||||||
} catch (...) {}
|
} catch (...) {}
|
||||||
|
|
||||||
@@ -279,11 +285,13 @@ void App::changePassphrase(const std::string& oldPass, const std::string& newPas
|
|||||||
encrypt_in_progress_ = true;
|
encrypt_in_progress_ = true;
|
||||||
encrypt_status_ = "Changing passphrase...";
|
encrypt_status_ = "Changing passphrase...";
|
||||||
|
|
||||||
worker_->post([this, oldPass, newPass]() -> rpc::RPCWorker::MainCb {
|
auto* w = (fast_worker_ && fast_worker_->isRunning()) ? fast_worker_.get() : worker_.get();
|
||||||
|
auto* r = (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get();
|
||||||
|
w->post([this, r, oldPass, newPass]() -> rpc::RPCWorker::MainCb {
|
||||||
bool ok = false;
|
bool ok = false;
|
||||||
std::string err_msg;
|
std::string err_msg;
|
||||||
try {
|
try {
|
||||||
rpc_->call("walletpassphrasechange", {oldPass, newPass});
|
r->call("walletpassphrasechange", {oldPass, newPass});
|
||||||
ok = true;
|
ok = true;
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
err_msg = e.what();
|
err_msg = e.what();
|
||||||
@@ -609,8 +617,11 @@ void App::renderLockScreen() {
|
|||||||
memset(lock_pin_buf_, 0, sizeof(lock_pin_buf_));
|
memset(lock_pin_buf_, 0, sizeof(lock_pin_buf_));
|
||||||
lock_unlock_in_progress_ = true;
|
lock_unlock_in_progress_ = true;
|
||||||
|
|
||||||
if (worker_) {
|
// Use fast-lane worker for priority unlock.
|
||||||
worker_->post([this, pin, timeout]() -> rpc::RPCWorker::MainCb {
|
auto* w = (fast_worker_ && fast_worker_->isRunning()) ? fast_worker_.get() : worker_.get();
|
||||||
|
auto* r = (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get();
|
||||||
|
if (w) {
|
||||||
|
w->post([this, r, pin, timeout]() -> rpc::RPCWorker::MainCb {
|
||||||
// Heavy Argon2id derivation runs here (worker thread)
|
// Heavy Argon2id derivation runs here (worker thread)
|
||||||
std::string passphrase;
|
std::string passphrase;
|
||||||
bool vaultOk = vault_ && vault_->retrieve(pin, passphrase);
|
bool vaultOk = vault_ && vault_->retrieve(pin, passphrase);
|
||||||
@@ -634,8 +645,8 @@ void App::renderLockScreen() {
|
|||||||
bool rpcOk = false;
|
bool rpcOk = false;
|
||||||
std::string rpcErr;
|
std::string rpcErr;
|
||||||
try {
|
try {
|
||||||
if (rpc_ && rpc_->isConnected()) {
|
if (r && r->isConnected()) {
|
||||||
rpc_->call("walletpassphrase", {passphrase, timeout});
|
r->call("walletpassphrase", {passphrase, timeout});
|
||||||
rpcOk = true;
|
rpcOk = true;
|
||||||
} else {
|
} else {
|
||||||
rpcErr = "Not connected to daemon";
|
rpcErr = "Not connected to daemon";
|
||||||
|
|||||||
@@ -145,6 +145,8 @@ bool Settings::load(const std::string& path)
|
|||||||
if (j.contains("pool_tls")) pool_tls_ = j["pool_tls"].get<bool>();
|
if (j.contains("pool_tls")) pool_tls_ = j["pool_tls"].get<bool>();
|
||||||
if (j.contains("pool_hugepages")) pool_hugepages_ = j["pool_hugepages"].get<bool>();
|
if (j.contains("pool_hugepages")) pool_hugepages_ = j["pool_hugepages"].get<bool>();
|
||||||
if (j.contains("pool_mode")) pool_mode_ = j["pool_mode"].get<bool>();
|
if (j.contains("pool_mode")) pool_mode_ = j["pool_mode"].get<bool>();
|
||||||
|
if (j.contains("font_scale") && j["font_scale"].is_number())
|
||||||
|
font_scale_ = std::max(1.0f, std::min(1.5f, j["font_scale"].get<float>()));
|
||||||
if (j.contains("window_width") && j["window_width"].is_number_integer())
|
if (j.contains("window_width") && j["window_width"].is_number_integer())
|
||||||
window_width_ = j["window_width"].get<int>();
|
window_width_ = j["window_width"].get<int>();
|
||||||
if (j.contains("window_height") && j["window_height"].is_number_integer())
|
if (j.contains("window_height") && j["window_height"].is_number_integer())
|
||||||
@@ -215,6 +217,7 @@ bool Settings::save(const std::string& path)
|
|||||||
j["pool_tls"] = pool_tls_;
|
j["pool_tls"] = pool_tls_;
|
||||||
j["pool_hugepages"] = pool_hugepages_;
|
j["pool_hugepages"] = pool_hugepages_;
|
||||||
j["pool_mode"] = pool_mode_;
|
j["pool_mode"] = pool_mode_;
|
||||||
|
j["font_scale"] = font_scale_;
|
||||||
if (window_width_ > 0 && window_height_ > 0) {
|
if (window_width_ > 0 && window_height_ > 0) {
|
||||||
j["window_width"] = window_width_;
|
j["window_width"] = window_width_;
|
||||||
j["window_height"] = window_height_;
|
j["window_height"] = window_height_;
|
||||||
|
|||||||
@@ -202,6 +202,10 @@ public:
|
|||||||
bool getPoolMode() const { return pool_mode_; }
|
bool getPoolMode() const { return pool_mode_; }
|
||||||
void setPoolMode(bool v) { pool_mode_ = v; }
|
void setPoolMode(bool v) { pool_mode_ = v; }
|
||||||
|
|
||||||
|
// Font scale (user accessibility setting, 1.0–1.5)
|
||||||
|
float getFontScale() const { return font_scale_; }
|
||||||
|
void setFontScale(float v) { font_scale_ = std::max(1.0f, std::min(1.5f, v)); }
|
||||||
|
|
||||||
// Window size persistence (logical pixels at 1x scale)
|
// Window size persistence (logical pixels at 1x scale)
|
||||||
int getWindowWidth() const { return window_width_; }
|
int getWindowWidth() const { return window_width_; }
|
||||||
int getWindowHeight() const { return window_height_; }
|
int getWindowHeight() const { return window_height_; }
|
||||||
@@ -254,6 +258,9 @@ private:
|
|||||||
bool pool_hugepages_ = true;
|
bool pool_hugepages_ = true;
|
||||||
bool pool_mode_ = false; // false=solo, true=pool
|
bool pool_mode_ = false; // false=solo, true=pool
|
||||||
|
|
||||||
|
// Font scale (user accessibility, 1.0–3.0; 1.0 = default)
|
||||||
|
float font_scale_ = 1.0f;
|
||||||
|
|
||||||
// Window size (logical pixels at 1x scale; 0 = use default 1200×775)
|
// Window size (logical pixels at 1x scale; 0 = use default 1200×775)
|
||||||
int window_width_ = 0;
|
int window_width_ = 0;
|
||||||
int window_height_ = 0;
|
int window_height_ = 0;
|
||||||
|
|||||||
71
src/main.cpp
71
src/main.cpp
@@ -778,6 +778,37 @@ int main(int argc, char* argv[])
|
|||||||
dragonx::util::PerfLog::instance().init(perfPath);
|
dragonx::util::PerfLog::instance().init(perfPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the user had a font scale > 1.0 saved, app.init() rebuilt fonts
|
||||||
|
// at that scale. Resize the window now so the larger UI fits.
|
||||||
|
{
|
||||||
|
float fs = dragonx::ui::Layout::userFontScale();
|
||||||
|
if (fs > 1.01f) {
|
||||||
|
int curW = 0, curH = 0;
|
||||||
|
SDL_GetWindowSize(window, &curW, &curH);
|
||||||
|
int newW = (int)lroundf(curW * fs);
|
||||||
|
int newH = (int)lroundf(curH * fs);
|
||||||
|
|
||||||
|
// Clamp to display work area
|
||||||
|
SDL_DisplayID did = SDL_GetDisplayForWindow(window);
|
||||||
|
if (did) {
|
||||||
|
SDL_Rect usable;
|
||||||
|
if (SDL_GetDisplayUsableBounds(did, &usable)) {
|
||||||
|
newW = std::min(newW, usable.w);
|
||||||
|
newH = std::min(newH, usable.h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float hwDpi = dragonx::ui::Layout::rawDpiScale();
|
||||||
|
SDL_SetWindowSize(window, newW, newH);
|
||||||
|
SDL_SetWindowMinimumSize(window,
|
||||||
|
(int)(1024 * hwDpi * fs),
|
||||||
|
(int)(720 * hwDpi * fs));
|
||||||
|
SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
|
||||||
|
DEBUG_LOGF("Font-scale startup: window %dx%d -> %dx%d (fontScale %.1f)\n",
|
||||||
|
curW, curH, newW, newH, fs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle pending payment URI from command line
|
// Handle pending payment URI from command line
|
||||||
if (!pendingURI.empty()) {
|
if (!pendingURI.empty()) {
|
||||||
DEBUG_LOGF("Processing payment URI: %s\n", pendingURI.c_str());
|
DEBUG_LOGF("Processing payment URI: %s\n", pendingURI.c_str());
|
||||||
@@ -825,6 +856,9 @@ int main(int argc, char* argv[])
|
|||||||
if (in_resize_render) return true;
|
if (in_resize_render) return true;
|
||||||
in_resize_render = true;
|
in_resize_render = true;
|
||||||
|
|
||||||
|
// Pre-frame: font atlas rebuilds before NewFrame()
|
||||||
|
ctx->app->preFrame();
|
||||||
|
|
||||||
#ifdef DRAGONX_USE_DX11
|
#ifdef DRAGONX_USE_DX11
|
||||||
ctx->dx->resize(event->window.data1, event->window.data2);
|
ctx->dx->resize(event->window.data1, event->window.data2);
|
||||||
ImGui_ImplDX11_NewFrame();
|
ImGui_ImplDX11_NewFrame();
|
||||||
@@ -1259,6 +1293,43 @@ int main(int argc, char* argv[])
|
|||||||
// --- PerfLog: begin frame ---
|
// --- PerfLog: begin frame ---
|
||||||
dragonx::util::PerfLog::instance().beginFrame();
|
dragonx::util::PerfLog::instance().beginFrame();
|
||||||
|
|
||||||
|
// Pre-frame: font atlas rebuilds and schema hot-reload must
|
||||||
|
// happen BEFORE NewFrame() because NewFrame() caches font ptrs.
|
||||||
|
float prevFontScale = dragonx::ui::Layout::userFontScale();
|
||||||
|
app.preFrame();
|
||||||
|
|
||||||
|
// If font scale changed (user dragged the slider), resize window
|
||||||
|
{
|
||||||
|
float curFS = dragonx::ui::Layout::userFontScale();
|
||||||
|
if (std::fabs(curFS - prevFontScale) > 0.001f) {
|
||||||
|
int curW = 0, curH = 0;
|
||||||
|
SDL_GetWindowSize(window, &curW, &curH);
|
||||||
|
float ratio = curFS / prevFontScale;
|
||||||
|
int newW = (int)lroundf(curW * ratio);
|
||||||
|
int newH = (int)lroundf(curH * ratio);
|
||||||
|
|
||||||
|
// Clamp to display work area
|
||||||
|
SDL_DisplayID did = SDL_GetDisplayForWindow(window);
|
||||||
|
if (did) {
|
||||||
|
SDL_Rect usable;
|
||||||
|
if (SDL_GetDisplayUsableBounds(did, &usable)) {
|
||||||
|
newW = std::min(newW, usable.w);
|
||||||
|
newH = std::min(newH, usable.h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float hwDpi = dragonx::ui::Layout::rawDpiScale();
|
||||||
|
SDL_SetWindowSize(window, newW, newH);
|
||||||
|
SDL_SetWindowMinimumSize(window,
|
||||||
|
(int)(1024 * hwDpi * curFS),
|
||||||
|
(int)(720 * hwDpi * curFS));
|
||||||
|
lastKnownW = newW;
|
||||||
|
lastKnownH = newH;
|
||||||
|
DEBUG_LOGF("Font-scale resize: %dx%d -> %dx%d (%.1fx -> %.1fx)\n",
|
||||||
|
curW, curH, newW, newH, prevFontScale, curFS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Start the Dear ImGui frame
|
// Start the Dear ImGui frame
|
||||||
PERF_BEGIN(_perfNewFrame);
|
PERF_BEGIN(_perfNewFrame);
|
||||||
#ifdef DRAGONX_USE_DX11
|
#ifdef DRAGONX_USE_DX11
|
||||||
|
|||||||
@@ -36,16 +36,68 @@ namespace Layout {
|
|||||||
// DPI Scaling (must be first — other accessors multiply by dpiScale())
|
// DPI Scaling (must be first — other accessors multiply by dpiScale())
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// User Font Scale (accessibility, 1.0–3.0, persisted in Settings)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
namespace detail {
|
||||||
|
inline float& userFontScaleRef() { static float s = 1.0f; return s; }
|
||||||
|
inline bool& fontReloadNeededRef() { static bool s = false; return s; }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get the current display DPI scale factor.
|
* @brief Get the user's font scale preference (1.0–3.0).
|
||||||
|
* Multiplied into font loading so glyphs render at the chosen size.
|
||||||
|
*/
|
||||||
|
inline float userFontScale() { return detail::userFontScaleRef(); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set the user's font scale and flag a font reload.
|
||||||
|
* Called from the settings UI; the main loop detects the flag and
|
||||||
|
* calls Typography::reload().
|
||||||
|
*/
|
||||||
|
inline void setUserFontScale(float v) {
|
||||||
|
v = std::max(1.0f, std::min(1.5f, v));
|
||||||
|
if (v != detail::userFontScaleRef()) {
|
||||||
|
detail::userFontScaleRef() = v;
|
||||||
|
detail::fontReloadNeededRef() = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Consume the pending font-reload flag (returns true once).
|
||||||
|
*/
|
||||||
|
inline bool consumeUserFontReload() {
|
||||||
|
bool v = detail::fontReloadNeededRef();
|
||||||
|
detail::fontReloadNeededRef() = false;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// DPI Scaling (must be after userFontScale — dpiScale includes it)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the raw hardware DPI scale factor (no user font scale).
|
||||||
*
|
*
|
||||||
* Returns the DPI scale set during typography initialization (e.g. 2.0 for
|
* Returns the DPI scale set during typography initialization (e.g. 2.0 for
|
||||||
* 200 % Windows scaling). All pixel constants from TOML are in *logical*
|
* 200 % Windows scaling). Use this when you need the pure hardware DPI
|
||||||
* pixels and must be multiplied by this factor before being used as ImGui
|
* without the user's accessibility font scale applied.
|
||||||
* coordinates (which are physical pixels on Windows Per-Monitor DPI v2).
|
*/
|
||||||
|
inline float rawDpiScale() {
|
||||||
|
return dragonx::ui::material::Typography::instance().getDpiScale();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the effective DPI scale factor including user font scale.
|
||||||
|
*
|
||||||
|
* Returns rawDpiScale() * userFontScale(). At userFontScale() == 1.0
|
||||||
|
* this is identical to the hardware DPI. All pixel constants from TOML
|
||||||
|
* are in *logical* pixels and should be multiplied by this factor so that
|
||||||
|
* containers grow proportionally when the user increases font scale.
|
||||||
*/
|
*/
|
||||||
inline float dpiScale() {
|
inline float dpiScale() {
|
||||||
return dragonx::ui::material::Typography::instance().getDpiScale();
|
return rawDpiScale() * userFontScale();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -165,11 +217,12 @@ inline LayoutTier currentTier(float availW, float availH) {
|
|||||||
*/
|
*/
|
||||||
inline float hScale(float availWidth) {
|
inline float hScale(float availWidth) {
|
||||||
const auto& S = schema::UI();
|
const auto& S = schema::UI();
|
||||||
float dp = dpiScale();
|
float rawDp = rawDpiScale(); // reference uses hardware DPI only
|
||||||
float rw = S.drawElement("responsive", "ref-width").sizeOr(1200.0f) * dp;
|
float dp = dpiScale(); // output includes user font scale
|
||||||
|
float rw = S.drawElement("responsive", "ref-width").sizeOr(1200.0f) * rawDp;
|
||||||
float minH = S.drawElement("responsive", "min-h-scale").sizeOr(0.5f);
|
float minH = S.drawElement("responsive", "min-h-scale").sizeOr(0.5f);
|
||||||
float maxH = S.drawElement("responsive", "max-h-scale").sizeOr(1.5f);
|
float maxH = S.drawElement("responsive", "max-h-scale").sizeOr(1.5f);
|
||||||
// Clamp the logical (DPI-neutral) portion, then apply DPI.
|
// Clamp the logical (DPI-neutral) portion, then apply effective DPI.
|
||||||
float logical = std::clamp(availWidth / rw, minH, maxH);
|
float logical = std::clamp(availWidth / rw, minH, maxH);
|
||||||
return logical * dp;
|
return logical * dp;
|
||||||
}
|
}
|
||||||
@@ -185,8 +238,9 @@ inline float hScale() {
|
|||||||
*/
|
*/
|
||||||
inline float vScale(float availHeight) {
|
inline float vScale(float availHeight) {
|
||||||
const auto& S = schema::UI();
|
const auto& S = schema::UI();
|
||||||
float dp = dpiScale();
|
float rawDp = rawDpiScale(); // reference uses hardware DPI only
|
||||||
float rh = S.drawElement("responsive", "ref-height").sizeOr(700.0f) * dp;
|
float dp = dpiScale(); // output includes user font scale
|
||||||
|
float rh = S.drawElement("responsive", "ref-height").sizeOr(700.0f) * rawDp;
|
||||||
float minV = S.drawElement("responsive", "min-v-scale").sizeOr(0.5f);
|
float minV = S.drawElement("responsive", "min-v-scale").sizeOr(0.5f);
|
||||||
float maxV = S.drawElement("responsive", "max-v-scale").sizeOr(1.4f);
|
float maxV = S.drawElement("responsive", "max-v-scale").sizeOr(1.4f);
|
||||||
float logical = std::clamp(availHeight / rh, minV, maxV);
|
float logical = std::clamp(availHeight / rh, minV, maxV);
|
||||||
@@ -205,8 +259,9 @@ inline float vScale() {
|
|||||||
*/
|
*/
|
||||||
inline float densityScale(float availHeight) {
|
inline float densityScale(float availHeight) {
|
||||||
const auto& S = schema::UI();
|
const auto& S = schema::UI();
|
||||||
float dp = dpiScale();
|
float rawDp = rawDpiScale(); // reference uses hardware DPI only
|
||||||
float rh = S.drawElement("responsive", "ref-height").sizeOr(700.0f) * dp;
|
float dp = dpiScale(); // output includes user font scale
|
||||||
|
float rh = S.drawElement("responsive", "ref-height").sizeOr(700.0f) * rawDp;
|
||||||
float minDen = S.drawElement("responsive", "min-density").sizeOr(0.6f);
|
float minDen = S.drawElement("responsive", "min-density").sizeOr(0.6f);
|
||||||
float maxDen = S.drawElement("responsive", "max-density").sizeOr(1.2f);
|
float maxDen = S.drawElement("responsive", "max-density").sizeOr(1.2f);
|
||||||
float logical = std::clamp(availHeight / rh, minDen, maxDen);
|
float logical = std::clamp(availHeight / rh, minDen, maxDen);
|
||||||
|
|||||||
@@ -118,8 +118,12 @@ bool Typography::load(ImGuiIO& io, float dpiScale)
|
|||||||
// and DisplayFramebufferScale is 1.0 (no automatic upscaling).
|
// and DisplayFramebufferScale is 1.0 (no automatic upscaling).
|
||||||
// The window is resized by dpiScale in main.cpp so that fonts at
|
// The window is resized by dpiScale in main.cpp so that fonts at
|
||||||
// size*dpiScale fit proportionally (no overflow).
|
// size*dpiScale fit proportionally (no overflow).
|
||||||
float scale = dpiScale * Layout::kFontScale();
|
// Layout::userFontScale() is the user-chosen accessibility multiplier
|
||||||
DEBUG_LOGF("Typography: Loading Material Design type scale (DPI: %.2f, fontScale: %.2f, combined: %.2f)\n", dpiScale, Layout::kFontScale(), scale);
|
// (1.0–3.0) persisted in Settings; it makes glyphs physically larger
|
||||||
|
// without any bitmap up-scaling (sharp at every size).
|
||||||
|
float scale = dpiScale * Layout::kFontScale() * Layout::userFontScale();
|
||||||
|
DEBUG_LOGF("Typography: Loading Material Design type scale (DPI: %.2f, fontScale: %.2f, userFontScale: %.2f, combined: %.2f)\n",
|
||||||
|
dpiScale, Layout::kFontScale(), Layout::userFontScale(), scale);
|
||||||
|
|
||||||
// For ImGui, we need to load fonts at specific pixel sizes.
|
// For ImGui, we need to load fonts at specific pixel sizes.
|
||||||
// Font sizes come from Layout:: accessors (backed by UISchema JSON)
|
// Font sizes come from Layout:: accessors (backed by UISchema JSON)
|
||||||
|
|||||||
@@ -84,6 +84,9 @@ static bool sp_gradient_background = false;
|
|||||||
// Low-spec mode
|
// Low-spec mode
|
||||||
static bool sp_low_spec_mode = false;
|
static bool sp_low_spec_mode = false;
|
||||||
|
|
||||||
|
// Font scale (user accessibility, 1.0–3.0)
|
||||||
|
static float sp_font_scale = 1.0f;
|
||||||
|
|
||||||
// Snapshot of effect settings saved when low-spec is toggled ON,
|
// Snapshot of effect settings saved when low-spec is toggled ON,
|
||||||
// restored when toggled OFF so user state isn't lost.
|
// restored when toggled OFF so user state isn't lost.
|
||||||
struct LowSpecSnapshot {
|
struct LowSpecSnapshot {
|
||||||
@@ -155,6 +158,8 @@ static void loadSettingsPageState(config::Settings* settings) {
|
|||||||
sp_theme_effects_enabled = settings->getThemeEffectsEnabled();
|
sp_theme_effects_enabled = settings->getThemeEffectsEnabled();
|
||||||
sp_low_spec_mode = settings->getLowSpecMode();
|
sp_low_spec_mode = settings->getLowSpecMode();
|
||||||
effects::setLowSpecMode(sp_low_spec_mode);
|
effects::setLowSpecMode(sp_low_spec_mode);
|
||||||
|
sp_font_scale = settings->getFontScale();
|
||||||
|
Layout::setUserFontScale(sp_font_scale); // sync with Layout on load
|
||||||
sp_keep_daemon_running = settings->getKeepDaemonRunning();
|
sp_keep_daemon_running = settings->getKeepDaemonRunning();
|
||||||
sp_stop_external_daemon = settings->getStopExternalDaemon();
|
sp_stop_external_daemon = settings->getStopExternalDaemon();
|
||||||
sp_debug_categories = settings->getDebugCategories();
|
sp_debug_categories = settings->getDebugCategories();
|
||||||
@@ -200,6 +205,7 @@ static void saveSettingsPageState(config::Settings* settings) {
|
|||||||
settings->setScanlineEnabled(sp_scanline_enabled);
|
settings->setScanlineEnabled(sp_scanline_enabled);
|
||||||
settings->setThemeEffectsEnabled(sp_theme_effects_enabled);
|
settings->setThemeEffectsEnabled(sp_theme_effects_enabled);
|
||||||
settings->setLowSpecMode(sp_low_spec_mode);
|
settings->setLowSpecMode(sp_low_spec_mode);
|
||||||
|
settings->setFontScale(sp_font_scale);
|
||||||
settings->setKeepDaemonRunning(sp_keep_daemon_running);
|
settings->setKeepDaemonRunning(sp_keep_daemon_running);
|
||||||
settings->setStopExternalDaemon(sp_stop_external_daemon);
|
settings->setStopExternalDaemon(sp_stop_external_daemon);
|
||||||
settings->setDebugCategories(sp_debug_categories);
|
settings->setDebugCategories(sp_debug_categories);
|
||||||
@@ -899,6 +905,34 @@ void RenderSettingsPage(App* app) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Font Scale slider (always enabled, not affected by low-spec)
|
||||||
|
// ============================================================
|
||||||
|
{
|
||||||
|
ImGui::PushFont(body2);
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::TextUnformatted("Font Scale");
|
||||||
|
float fontSliderW = std::min(availWidth - pad * 2, 260.0f * dp);
|
||||||
|
ImGui::SetNextItemWidth(fontSliderW);
|
||||||
|
float prev_font_scale = sp_font_scale;
|
||||||
|
{
|
||||||
|
char fs_fmt[16];
|
||||||
|
snprintf(fs_fmt, sizeof(fs_fmt), "%.1fx", sp_font_scale);
|
||||||
|
ImGui::SliderFloat("##FontScale", &sp_font_scale, 1.0f, 1.5f, fs_fmt,
|
||||||
|
ImGuiSliderFlags_AlwaysClamp);
|
||||||
|
}
|
||||||
|
// Snap to nearest 0.1 and apply live as the user drags.
|
||||||
|
// Font atlas rebuild is deferred to preFrame() (before NewFrame),
|
||||||
|
// so updating every tick is safe — no dangling font pointers.
|
||||||
|
sp_font_scale = std::round(sp_font_scale * 10.0f) / 10.0f;
|
||||||
|
if (sp_font_scale != prev_font_scale) {
|
||||||
|
Layout::setUserFontScale(sp_font_scale);
|
||||||
|
saveSettingsPageState(app->settings());
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Scale all text and UI (1.0x = default, up to 1.5x).");
|
||||||
|
ImGui::PopFont();
|
||||||
|
}
|
||||||
|
|
||||||
// Bottom padding
|
// Bottom padding
|
||||||
ImGui::Dummy(ImVec2(0, bottomPad));
|
ImGui::Dummy(ImVec2(0, bottomPad));
|
||||||
ImGui::Unindent(pad);
|
ImGui::Unindent(pad);
|
||||||
|
|||||||
@@ -231,11 +231,29 @@ void ConsoleTab::render(daemon::EmbeddedDaemon* daemon, rpc::RPCClient* rpc, rpc
|
|||||||
ImGui::PopFont();
|
ImGui::PopFont();
|
||||||
ImGui::EndChild();
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
// Auto-toggle auto-scroll based on scroll position:
|
||||||
|
// At the bottom → re-enable; scrolled up → already disabled by wheel handler.
|
||||||
|
// After wheel-up, wait for the cooldown so smooth-scroll can animate
|
||||||
|
// away from the bottom before we check position again.
|
||||||
|
if (scroll_up_cooldown_ > 0.0f)
|
||||||
|
scroll_up_cooldown_ -= ImGui::GetIO().DeltaTime;
|
||||||
|
if (!auto_scroll_ && scroll_up_cooldown_ <= 0.0f && consoleScrollMaxY > 0.0f) {
|
||||||
|
float tolerance = Type().caption()->LegacySize * 1.5f;
|
||||||
|
if (consoleScrollY >= consoleScrollMaxY - tolerance) {
|
||||||
|
auto_scroll_ = true;
|
||||||
|
new_lines_since_scroll_ = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// CSS-style clipping mask
|
// CSS-style clipping mask
|
||||||
|
// When auto-scroll is off, force bottom fade to always show by
|
||||||
|
// inflating scrollMax so the mask thinks there's content below.
|
||||||
{
|
{
|
||||||
float fadeZone = std::min(Type().caption()->LegacySize * 3.0f, outputH * 0.18f);
|
float fadeZone = std::min(Type().caption()->LegacySize * 3.0f, outputH * 0.18f);
|
||||||
|
float effectiveScrollMax = auto_scroll_ ? consoleScrollMaxY
|
||||||
|
: std::max(consoleScrollMaxY, consoleScrollY + 10.0f);
|
||||||
ApplyScrollEdgeMask(dlOut, consoleParentVtx, consoleChildDL, consoleChildVtx,
|
ApplyScrollEdgeMask(dlOut, consoleParentVtx, consoleChildDL, consoleChildVtx,
|
||||||
outPanelMin.y, outPanelMax.y, fadeZone, consoleScrollY, consoleScrollMaxY);
|
outPanelMin.y, outPanelMax.y, fadeZone, consoleScrollY, effectiveScrollMax);
|
||||||
}
|
}
|
||||||
|
|
||||||
// CRT scanline effect over output area — aligned to text lines
|
// CRT scanline effect over output area — aligned to text lines
|
||||||
@@ -552,7 +570,12 @@ void ConsoleTab::renderOutput()
|
|||||||
auto& S = schema::UI();
|
auto& S = schema::UI();
|
||||||
std::lock_guard<std::mutex> lock(lines_mutex_);
|
std::lock_guard<std::mutex> lock(lines_mutex_);
|
||||||
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, S.drawElement("tabs.console", "output").size));
|
// Zero item spacing so Dummy items advance the cursor by exactly their
|
||||||
|
// height. The inter-line gap is added explicitly to wrapped_heights_
|
||||||
|
// so that cumulative_y_offsets_ stays perfectly in sync with actual
|
||||||
|
// cursor positions (avoiding selection-offset drift).
|
||||||
|
float interLineGap = S.drawElement("tabs.console", "output").getFloat("line-spacing", 0.0f);
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
|
||||||
|
|
||||||
// Inner padding for glass panel
|
// Inner padding for glass panel
|
||||||
float padX = Layout::spacingMd();
|
float padX = Layout::spacingMd();
|
||||||
@@ -560,7 +583,7 @@ void ConsoleTab::renderOutput()
|
|||||||
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + padY);
|
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + padY);
|
||||||
ImGui::Indent(padX);
|
ImGui::Indent(padX);
|
||||||
|
|
||||||
float line_height = ImGui::GetTextLineHeightWithSpacing();
|
float line_height = ImGui::GetTextLineHeight();
|
||||||
output_line_height_ = line_height; // store for scanline alignment
|
output_line_height_ = line_height; // store for scanline alignment
|
||||||
output_origin_ = ImGui::GetCursorScreenPos();
|
output_origin_ = ImGui::GetCursorScreenPos();
|
||||||
output_scroll_y_ = ImGui::GetScrollY();
|
output_scroll_y_ = ImGui::GetScrollY();
|
||||||
@@ -598,33 +621,69 @@ void ConsoleTab::renderOutput()
|
|||||||
}
|
}
|
||||||
int visible_count = static_cast<int>(visible_indices_.size());
|
int visible_count = static_cast<int>(visible_indices_.size());
|
||||||
|
|
||||||
// Calculate wrapped heights for each visible line
|
// Calculate wrapped heights AND build sub-row segments for each visible line.
|
||||||
// This is needed because TextWrapped creates variable-height content
|
// Each segment records which bytes of the source text appear on that visual
|
||||||
|
// row, so hit-testing and selection highlight can map screen positions to
|
||||||
|
// exact character offsets.
|
||||||
float wrap_width = ImGui::GetContentRegionAvail().x - padX * 2;
|
float wrap_width = ImGui::GetContentRegionAvail().x - padX * 2;
|
||||||
if (wrap_width < 50.0f) wrap_width = 50.0f; // Minimum wrap width
|
if (wrap_width < 50.0f) wrap_width = 50.0f;
|
||||||
|
|
||||||
|
ImFont* font = ImGui::GetFont();
|
||||||
|
float fontSize = ImGui::GetFontSize();
|
||||||
|
|
||||||
wrapped_heights_.resize(visible_count);
|
wrapped_heights_.resize(visible_count);
|
||||||
cumulative_y_offsets_.resize(visible_count);
|
cumulative_y_offsets_.resize(visible_count);
|
||||||
|
visible_wrap_segments_.resize(visible_count);
|
||||||
total_wrapped_height_ = 0.0f;
|
total_wrapped_height_ = 0.0f;
|
||||||
cached_wrap_width_ = wrap_width;
|
cached_wrap_width_ = wrap_width;
|
||||||
|
|
||||||
for (int vi = 0; vi < visible_count; vi++) {
|
for (int vi = 0; vi < visible_count; vi++) {
|
||||||
int i = visible_indices_[vi];
|
int i = visible_indices_[vi];
|
||||||
const std::string& text = lines_[i].text;
|
const std::string& text = lines_[i].text;
|
||||||
|
auto& segs = visible_wrap_segments_[vi];
|
||||||
|
segs.clear();
|
||||||
|
|
||||||
// Calculate wrapped text size - CalcTextSize with wrap_width > 0
|
|
||||||
ImVec2 sz;
|
|
||||||
if (text.empty()) {
|
if (text.empty()) {
|
||||||
sz = ImVec2(0.0f, line_height);
|
segs.push_back({0, 0, 0.0f, line_height});
|
||||||
} else {
|
cumulative_y_offsets_[vi] = total_wrapped_height_;
|
||||||
sz = ImGui::CalcTextSize(text.c_str(), nullptr, false, wrap_width);
|
wrapped_heights_[vi] = line_height + interLineGap;
|
||||||
// Add a small margin for item spacing
|
total_wrapped_height_ += wrapped_heights_[vi];
|
||||||
sz.y = std::max(sz.y, line_height);
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk the text using ImFont::CalcWordWrapPositionA to find
|
||||||
|
// exactly where ImGui would break each visual row.
|
||||||
|
const char* textStart = text.c_str();
|
||||||
|
const char* textEnd = textStart + text.size();
|
||||||
|
const char* cur = textStart;
|
||||||
|
float segY = 0.0f;
|
||||||
|
|
||||||
|
while (cur < textEnd) {
|
||||||
|
const char* wrapPos = font->CalcWordWrapPositionA(
|
||||||
|
fontSize / font->LegacySize, cur, textEnd, wrap_width);
|
||||||
|
// Ensure forward progress (at least one character)
|
||||||
|
if (wrapPos <= cur) wrapPos = cur + 1;
|
||||||
|
// Skip a leading newline character that ends the previous segment
|
||||||
|
if (*cur == '\n') { cur++; continue; }
|
||||||
|
|
||||||
|
int byteStart = static_cast<int>(cur - textStart);
|
||||||
|
int byteEnd = static_cast<int>(wrapPos - textStart);
|
||||||
|
// Trim trailing newline from this segment
|
||||||
|
if (byteEnd > byteStart && text[byteEnd - 1] == '\n') byteEnd--;
|
||||||
|
|
||||||
|
segs.push_back({byteStart, byteEnd, segY, line_height});
|
||||||
|
segY += line_height;
|
||||||
|
cur = wrapPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segs.empty()) {
|
||||||
|
segs.push_back({0, 0, 0.0f, line_height});
|
||||||
|
segY = line_height;
|
||||||
}
|
}
|
||||||
|
|
||||||
cumulative_y_offsets_[vi] = total_wrapped_height_;
|
cumulative_y_offsets_[vi] = total_wrapped_height_;
|
||||||
wrapped_heights_[vi] = sz.y;
|
wrapped_heights_[vi] = segY + interLineGap;
|
||||||
total_wrapped_height_ += sz.y;
|
total_wrapped_height_ += wrapped_heights_[vi];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use raw IO for mouse handling to bypass child window event consumption
|
// Use raw IO for mouse handling to bypass child window event consumption
|
||||||
@@ -642,7 +701,12 @@ void ConsoleTab::renderOutput()
|
|||||||
// Disable auto-scroll when user scrolls up (wheel scroll)
|
// Disable auto-scroll when user scrolls up (wheel scroll)
|
||||||
if (mouse_in_output && io.MouseWheel > 0.0f) {
|
if (mouse_in_output && io.MouseWheel > 0.0f) {
|
||||||
auto_scroll_ = false;
|
auto_scroll_ = false;
|
||||||
|
scroll_up_cooldown_ = 0.5f; // give smooth-scroll time to animate away
|
||||||
}
|
}
|
||||||
|
// Scrolling down to the very bottom re-enables auto-scroll.
|
||||||
|
// Actual position check happens after EndChild() using captured
|
||||||
|
// scroll values, but is skipped on the frame where wheel-up
|
||||||
|
// was detected (scroll position hasn't caught up yet).
|
||||||
|
|
||||||
// Set cursor to text selection when hovering
|
// Set cursor to text selection when hovering
|
||||||
if (mouse_in_output) {
|
if (mouse_in_output) {
|
||||||
@@ -696,8 +760,9 @@ void ConsoleTab::renderOutput()
|
|||||||
TextPos sel_start_pos = selectionStart();
|
TextPos sel_start_pos = selectionStart();
|
||||||
TextPos sel_end_pos = selectionEnd();
|
TextPos sel_end_pos = selectionEnd();
|
||||||
|
|
||||||
// Render lines with selection highlighting
|
// Render lines with selection highlighting.
|
||||||
// Use manual rendering instead of ImGuiListClipper to support variable-height wrapped lines
|
// Each line is split into pre-computed wrap segments rendered individually
|
||||||
|
// via AddText so that hit-testing and highlights map 1:1 to visual positions.
|
||||||
float scroll_y = ImGui::GetScrollY();
|
float scroll_y = ImGui::GetScrollY();
|
||||||
float window_height = ImGui::GetWindowHeight();
|
float window_height = ImGui::GetWindowHeight();
|
||||||
float visible_top = scroll_y;
|
float visible_top = scroll_y;
|
||||||
@@ -724,10 +789,12 @@ void ConsoleTab::renderOutput()
|
|||||||
ImGui::Dummy(ImVec2(0, cumulative_y_offsets_[first_visible]));
|
ImGui::Dummy(ImVec2(0, cumulative_y_offsets_[first_visible]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||||
|
ImU32 selColor = WithAlpha(Secondary(), 80);
|
||||||
|
|
||||||
// Render visible lines
|
// Render visible lines
|
||||||
int last_rendered_vi = first_visible - 1; // Track actual last rendered line
|
int last_rendered_vi = first_visible - 1;
|
||||||
for (int vi = first_visible; vi < visible_count; vi++) {
|
for (int vi = first_visible; vi < visible_count; vi++) {
|
||||||
// Early exit if we're past the visible region
|
|
||||||
if (vi < static_cast<int>(cumulative_y_offsets_.size()) &&
|
if (vi < static_cast<int>(cumulative_y_offsets_.size()) &&
|
||||||
cumulative_y_offsets_[vi] > visible_bottom) {
|
cumulative_y_offsets_[vi] > visible_bottom) {
|
||||||
break;
|
break;
|
||||||
@@ -736,60 +803,59 @@ void ConsoleTab::renderOutput()
|
|||||||
|
|
||||||
int i = visible_indices_[vi];
|
int i = visible_indices_[vi];
|
||||||
const auto& line = lines_[i];
|
const auto& line = lines_[i];
|
||||||
ImVec2 text_pos = ImGui::GetCursorScreenPos();
|
const auto& segs = visible_wrap_segments_[vi];
|
||||||
float this_line_height = (vi < static_cast<int>(wrapped_heights_.size()))
|
ImVec2 lineOrigin = ImGui::GetCursorScreenPos();
|
||||||
? wrapped_heights_[vi] : line_height;
|
float totalH = wrapped_heights_[vi];
|
||||||
|
|
||||||
// Draw selection highlight for this line
|
// Determine byte-level selection range for this line
|
||||||
|
int selByteStart = 0, selByteEnd = 0;
|
||||||
|
bool lineSelected = false;
|
||||||
if (has_selection_ && i >= sel_start_pos.line && i <= sel_end_pos.line) {
|
if (has_selection_ && i >= sel_start_pos.line && i <= sel_end_pos.line) {
|
||||||
int sel_col_start = 0;
|
lineSelected = true;
|
||||||
int sel_col_end = static_cast<int>(line.text.size());
|
selByteStart = (i == sel_start_pos.line) ? sel_start_pos.col : 0;
|
||||||
|
selByteEnd = (i == sel_end_pos.line) ? sel_end_pos.col
|
||||||
|
: static_cast<int>(line.text.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& seg : segs) {
|
||||||
|
float rowY = lineOrigin.y + seg.yOffset;
|
||||||
|
const char* segStart = line.text.c_str() + seg.byteStart;
|
||||||
|
const char* segEnd = line.text.c_str() + seg.byteEnd;
|
||||||
|
|
||||||
if (i == sel_start_pos.line) {
|
// Selection highlight for this sub-row
|
||||||
sel_col_start = sel_start_pos.col;
|
if (lineSelected && selByteStart < seg.byteEnd && selByteEnd > seg.byteStart) {
|
||||||
}
|
int hlStart = std::max(selByteStart, seg.byteStart) - seg.byteStart;
|
||||||
if (i == sel_end_pos.line) {
|
int hlEnd = std::min(selByteEnd, seg.byteEnd) - seg.byteStart;
|
||||||
sel_col_end = sel_end_pos.col;
|
int segLen = seg.byteEnd - seg.byteStart;
|
||||||
|
|
||||||
|
float xStart = 0.0f;
|
||||||
|
if (hlStart > 0) {
|
||||||
|
xStart = font->CalcTextSizeA(fontSize, FLT_MAX, 0,
|
||||||
|
segStart, segStart + hlStart).x;
|
||||||
|
}
|
||||||
|
float xEnd = font->CalcTextSizeA(fontSize, FLT_MAX, 0,
|
||||||
|
segStart, segStart + hlEnd).x;
|
||||||
|
// Extend to window edge when selection reaches end of segment
|
||||||
|
if (hlEnd >= segLen && selByteEnd >= static_cast<int>(line.text.size())) {
|
||||||
|
xEnd = std::max(xEnd + 8.0f, ImGui::GetWindowWidth());
|
||||||
|
}
|
||||||
|
|
||||||
|
dl->AddRectFilled(
|
||||||
|
ImVec2(lineOrigin.x + xStart, rowY),
|
||||||
|
ImVec2(lineOrigin.x + xEnd, rowY + seg.height),
|
||||||
|
selColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sel_col_start < sel_col_end) {
|
// Render text segment
|
||||||
// Calculate pixel positions for highlight
|
if (seg.byteStart < seg.byteEnd) {
|
||||||
float x_start = 0;
|
dl->AddText(font, fontSize,
|
||||||
float x_end = 0;
|
ImVec2(lineOrigin.x, rowY),
|
||||||
|
line.color, segStart, segEnd);
|
||||||
if (sel_col_start > 0 && sel_col_start <= static_cast<int>(line.text.size())) {
|
|
||||||
ImVec2 sz = ImGui::CalcTextSize(line.text.c_str(),
|
|
||||||
line.text.c_str() + sel_col_start);
|
|
||||||
x_start = sz.x;
|
|
||||||
}
|
|
||||||
if (sel_col_end <= static_cast<int>(line.text.size())) {
|
|
||||||
ImVec2 sz = ImGui::CalcTextSize(line.text.c_str(),
|
|
||||||
line.text.c_str() + sel_col_end);
|
|
||||||
x_end = sz.x;
|
|
||||||
} else {
|
|
||||||
x_end = ImGui::CalcTextSize(line.text.c_str()).x;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If full line selected, extend highlight to window edge
|
|
||||||
if (sel_col_end >= static_cast<int>(line.text.size())) {
|
|
||||||
x_end = std::max(x_end + S.drawElement("tabs.console", "selection-extension").size, ImGui::GetWindowWidth());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use actual wrapped height for selection highlight
|
|
||||||
ImVec2 rect_min(text_pos.x + x_start, text_pos.y);
|
|
||||||
ImVec2 rect_max(text_pos.x + x_end, text_pos.y + this_line_height);
|
|
||||||
ImGui::GetWindowDrawList()->AddRectFilled(
|
|
||||||
rect_min, rect_max,
|
|
||||||
WithAlpha(Secondary(), 80) // Selection highlight
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImColor(line.color).Value);
|
// Advance ImGui cursor by the total wrapped height of this line
|
||||||
ImGui::PushTextWrapPos(ImGui::GetContentRegionAvail().x - padX);
|
ImGui::Dummy(ImVec2(0, totalH));
|
||||||
ImGui::TextWrapped("%s", line.text.c_str());
|
|
||||||
ImGui::PopTextWrapPos();
|
|
||||||
ImGui::PopStyleColor();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add spacer for lines after last visible (to maintain correct content height)
|
// Add spacer for lines after last visible (to maintain correct content height)
|
||||||
@@ -806,9 +872,10 @@ void ConsoleTab::renderOutput()
|
|||||||
ImGui::Unindent(padX);
|
ImGui::Unindent(padX);
|
||||||
ImGui::PopStyleVar();
|
ImGui::PopStyleVar();
|
||||||
|
|
||||||
// Add bottom padding so the last line sits above the fade-out zone.
|
// Bottom padding keeps the last line above the fade-out zone.
|
||||||
// Only the fade zone height is used — no extra padding beyond that,
|
// Always present so that scrollMaxY stays stable when auto-scroll
|
||||||
// so text can still overflow into the fade and scrolling stays snappy.
|
// toggles — otherwise the geometry shift clamps the user back to
|
||||||
|
// bottom and a single scroll-up tick can't escape.
|
||||||
{
|
{
|
||||||
float fadeZone = std::min(Type().caption()->LegacySize * 3.0f,
|
float fadeZone = std::min(Type().caption()->LegacySize * 3.0f,
|
||||||
ImGui::GetWindowHeight() * 0.18f);
|
ImGui::GetWindowHeight() * 0.18f);
|
||||||
@@ -911,14 +978,11 @@ ConsoleTab::TextPos ConsoleTab::screenToTextPos(ImVec2 screen_pos, float line_he
|
|||||||
return {0, 0};
|
return {0, 0};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate which VISIBLE line based on Y position relative to output origin
|
|
||||||
// Use cumulative_y_offsets_ for accurate wrapped text positioning
|
|
||||||
float relative_y = screen_pos.y - output_origin_.y;
|
float relative_y = screen_pos.y - output_origin_.y;
|
||||||
|
|
||||||
// Find the visible line using cumulative Y offsets (binary search)
|
// Binary search for the visible line that contains this Y position
|
||||||
int visible_line = 0;
|
int visible_line = 0;
|
||||||
if (!cumulative_y_offsets_.empty()) {
|
if (!cumulative_y_offsets_.empty()) {
|
||||||
// Binary search for the line that contains this Y position
|
|
||||||
int lo = 0, hi = static_cast<int>(cumulative_y_offsets_.size()) - 1;
|
int lo = 0, hi = static_cast<int>(cumulative_y_offsets_.size()) - 1;
|
||||||
while (lo < hi) {
|
while (lo < hi) {
|
||||||
int mid = (lo + hi + 1) / 2;
|
int mid = (lo + hi + 1) / 2;
|
||||||
@@ -929,12 +993,9 @@ ConsoleTab::TextPos ConsoleTab::screenToTextPos(ImVec2 screen_pos, float line_he
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
visible_line = lo;
|
visible_line = lo;
|
||||||
} else {
|
|
||||||
// Fallback to fixed line height if offsets not calculated
|
|
||||||
visible_line = static_cast<int>(relative_y / line_height);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clamp visible line to valid range
|
// Clamp visible line
|
||||||
if (visible_line < 0) visible_line = 0;
|
if (visible_line < 0) visible_line = 0;
|
||||||
if (visible_line >= static_cast<int>(visible_indices_.size())) {
|
if (visible_line >= static_cast<int>(visible_indices_.size())) {
|
||||||
visible_line = static_cast<int>(visible_indices_.size()) - 1;
|
visible_line = static_cast<int>(visible_indices_.size()) - 1;
|
||||||
@@ -943,34 +1004,49 @@ ConsoleTab::TextPos ConsoleTab::screenToTextPos(ImVec2 screen_pos, float line_he
|
|||||||
return pos;
|
return pos;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map visible line index to actual line index
|
|
||||||
pos.line = visible_indices_[visible_line];
|
pos.line = visible_indices_[visible_line];
|
||||||
|
|
||||||
// Calculate column from X position
|
|
||||||
const std::string& text = lines_[pos.line].text;
|
const std::string& text = lines_[pos.line].text;
|
||||||
float relative_x = screen_pos.x - output_origin_.x;
|
|
||||||
|
|
||||||
if (relative_x <= 0 || text.empty()) {
|
if (text.empty()) {
|
||||||
pos.col = 0;
|
pos.col = 0;
|
||||||
return pos;
|
return pos;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Binary search for the character position
|
// Find which sub-row (wrap segment) the mouse Y falls into
|
||||||
// Walk character by character for accuracy
|
const auto& segs = visible_wrap_segments_[visible_line];
|
||||||
pos.col = 0;
|
float lineRelY = relative_y - cumulative_y_offsets_[visible_line];
|
||||||
for (int c = 0; c < static_cast<int>(text.size()); c++) {
|
int segIdx = 0;
|
||||||
ImVec2 sz = ImGui::CalcTextSize(text.c_str(), text.c_str() + c + 1);
|
for (int s = 0; s < static_cast<int>(segs.size()); s++) {
|
||||||
float char_mid = (c > 0)
|
if (lineRelY >= segs[s].yOffset)
|
||||||
? (ImGui::CalcTextSize(text.c_str(), text.c_str() + c).x + sz.x) * 0.5f
|
segIdx = s;
|
||||||
: sz.x * 0.5f;
|
}
|
||||||
if (relative_x < char_mid) {
|
const auto& seg = segs[segIdx];
|
||||||
pos.col = c;
|
|
||||||
return pos;
|
// Calculate column within this segment from X position
|
||||||
}
|
float relative_x = screen_pos.x - output_origin_.x;
|
||||||
pos.col = c + 1;
|
if (relative_x <= 0.0f) {
|
||||||
|
pos.col = seg.byteStart;
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImFont* font = ImGui::GetFont();
|
||||||
|
float fontSize = ImGui::GetFontSize();
|
||||||
|
const char* segStart = text.c_str() + seg.byteStart;
|
||||||
|
const char* segEnd = text.c_str() + seg.byteEnd;
|
||||||
|
int segLen = seg.byteEnd - seg.byteStart;
|
||||||
|
|
||||||
|
// Walk characters within this segment for accurate positioning
|
||||||
|
pos.col = seg.byteEnd; // default: past end of segment
|
||||||
|
for (int c = 0; c < segLen; c++) {
|
||||||
|
float wCur = font->CalcTextSizeA(fontSize, FLT_MAX, 0, segStart, segStart + c + 1).x;
|
||||||
|
float wPrev = (c > 0) ? font->CalcTextSizeA(fontSize, FLT_MAX, 0, segStart, segStart + c).x : 0.0f;
|
||||||
|
float charMid = (wPrev + wCur) * 0.5f;
|
||||||
|
if (relative_x < charMid) {
|
||||||
|
pos.col = seg.byteStart + c;
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pos.col = static_cast<int>(text.size());
|
|
||||||
return pos;
|
return pos;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ private:
|
|||||||
char input_buffer_[4096] = {0};
|
char input_buffer_[4096] = {0};
|
||||||
bool auto_scroll_ = true;
|
bool auto_scroll_ = true;
|
||||||
bool scroll_to_bottom_ = false;
|
bool scroll_to_bottom_ = false;
|
||||||
|
float scroll_up_cooldown_ = 0.0f; // seconds to wait before re-enabling auto-scroll
|
||||||
int new_lines_since_scroll_ = 0; // new lines while scrolled up (for indicator)
|
int new_lines_since_scroll_ = 0; // new lines while scrolled up (for indicator)
|
||||||
size_t last_daemon_output_size_ = 0;
|
size_t last_daemon_output_size_ = 0;
|
||||||
size_t last_xmrig_output_size_ = 0;
|
size_t last_xmrig_output_size_ = 0;
|
||||||
@@ -140,6 +141,17 @@ private:
|
|||||||
mutable float total_wrapped_height_ = 0.0f; // Total height of all visible lines
|
mutable float total_wrapped_height_ = 0.0f; // Total height of all visible lines
|
||||||
mutable float cached_wrap_width_ = 0.0f; // Wrap width used for cached heights
|
mutable float cached_wrap_width_ = 0.0f; // Wrap width used for cached heights
|
||||||
|
|
||||||
|
// Sub-row layout: each visible line is split into wrap segments so
|
||||||
|
// selection and hit-testing know the exact screen position of every
|
||||||
|
// character.
|
||||||
|
struct WrapSegment {
|
||||||
|
int byteStart; // byte offset into ConsoleLine::text
|
||||||
|
int byteEnd; // byte offset past last char in this segment
|
||||||
|
float yOffset; // Y offset of this segment relative to the line's top
|
||||||
|
float height; // visual height of this segment
|
||||||
|
};
|
||||||
|
mutable std::vector<std::vector<WrapSegment>> visible_wrap_segments_; // [vi] -> segments
|
||||||
|
|
||||||
// Commands popup
|
// Commands popup
|
||||||
bool show_commands_popup_ = false;
|
bool show_commands_popup_ = false;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1021,43 +1021,49 @@ void RenderMiningTab(App* app)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Catmull-Rom spline interpolation for smooth curve
|
// Catmull-Rom spline interpolation for smooth curve
|
||||||
|
// Subdivisions are adaptive: more when points are far apart,
|
||||||
|
// none when points are already sub-2px apart.
|
||||||
std::vector<ImVec2> points;
|
std::vector<ImVec2> points;
|
||||||
if (n <= 2) {
|
if (n <= 2) {
|
||||||
points = rawPts;
|
points = rawPts;
|
||||||
} else {
|
} else {
|
||||||
const int subdivs = 8; // segments between each pair of data points
|
points.reserve(n * 4); // conservative estimate
|
||||||
points.reserve((n - 1) * subdivs + 1);
|
|
||||||
for (size_t i = 0; i + 1 < n; i++) {
|
for (size_t i = 0; i + 1 < n; i++) {
|
||||||
// Four control points: p0, p1, p2, p3
|
|
||||||
ImVec2 p0 = rawPts[i > 0 ? i - 1 : 0];
|
ImVec2 p0 = rawPts[i > 0 ? i - 1 : 0];
|
||||||
ImVec2 p1 = rawPts[i];
|
ImVec2 p1 = rawPts[i];
|
||||||
ImVec2 p2 = rawPts[i + 1];
|
ImVec2 p2 = rawPts[i + 1];
|
||||||
ImVec2 p3 = rawPts[i + 2 < n ? i + 2 : n - 1];
|
ImVec2 p3 = rawPts[i + 2 < n ? i + 2 : n - 1];
|
||||||
|
|
||||||
|
// Adaptive subdivision: ~1 segment per 3px of distance
|
||||||
|
float dx = p2.x - p1.x, dy = p2.y - p1.y;
|
||||||
|
float dist = sqrtf(dx * dx + dy * dy);
|
||||||
|
int subdivs = std::clamp((int)(dist / 3.0f), 1, 16);
|
||||||
|
|
||||||
for (int s = 0; s < subdivs; s++) {
|
for (int s = 0; s < subdivs; s++) {
|
||||||
float t = (float)s / (float)subdivs;
|
float t = (float)s / (float)subdivs;
|
||||||
float t2 = t * t;
|
float t2 = t * t;
|
||||||
float t3 = t2 * t;
|
float t3 = t2 * t;
|
||||||
// Catmull-Rom basis
|
|
||||||
float q0 = -t3 + 2.0f * t2 - t;
|
float q0 = -t3 + 2.0f * t2 - t;
|
||||||
float q1 = 3.0f * t3 - 5.0f * t2 + 2.0f;
|
float q1 = 3.0f * t3 - 5.0f * t2 + 2.0f;
|
||||||
float q2 = -3.0f * t3 + 4.0f * t2 + t;
|
float q2 = -3.0f * t3 + 4.0f * t2 + t;
|
||||||
float q3 = t3 - t2;
|
float q3 = t3 - t2;
|
||||||
float sx = 0.5f * (p0.x * q0 + p1.x * q1 + p2.x * q2 + p3.x * q3);
|
float sx = 0.5f * (p0.x * q0 + p1.x * q1 + p2.x * q2 + p3.x * q3);
|
||||||
float sy = 0.5f * (p0.y * q0 + p1.y * q1 + p2.y * q2 + p3.y * q3);
|
float sy = 0.5f * (p0.y * q0 + p1.y * q1 + p2.y * q2 + p3.y * q3);
|
||||||
|
// Clamp Y to plot bounds to prevent Catmull-Rom overshoot
|
||||||
|
sy = std::clamp(sy, plotTop, plotBottom);
|
||||||
points.push_back(ImVec2(sx, sy));
|
points.push_back(ImVec2(sx, sy));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
points.push_back(rawPts[n - 1]); // final point
|
points.push_back(rawPts[n - 1]); // final point
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fill under curve
|
// Fill under curve (single concave polygon to avoid AA seam shimmer)
|
||||||
for (size_t i = 0; i + 1 < points.size(); i++) {
|
if (points.size() >= 2) {
|
||||||
ImVec2 quad[4] = {
|
for (size_t i = 0; i < points.size(); i++)
|
||||||
points[i], points[i + 1],
|
dl->PathLineTo(points[i]);
|
||||||
ImVec2(points[i + 1].x, plotBottom),
|
dl->PathLineTo(ImVec2(points.back().x, plotBottom));
|
||||||
ImVec2(points[i].x, plotBottom)
|
dl->PathLineTo(ImVec2(points.front().x, plotBottom));
|
||||||
};
|
dl->PathFillConcave(WithAlpha(Success(), 25));
|
||||||
dl->AddConvexPolyFilled(quad, 4, WithAlpha(Success(), 25));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Green line
|
// Green line
|
||||||
|
|||||||
Reference in New Issue
Block a user