console and mining tab visual improvements

This commit is contained in:
2026-02-27 13:30:06 -06:00
parent 48ce983966
commit eebfd5947e
13 changed files with 962 additions and 183 deletions

View File

@@ -36,16 +36,68 @@ namespace Layout {
// DPI Scaling (must be first — other accessors multiply by dpiScale())
// ============================================================================
// ============================================================================
// User Font Scale (accessibility, 1.03.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.03.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
* 200 % Windows scaling). All pixel constants from TOML are in *logical*
* pixels and must be multiplied by this factor before being used as ImGui
* coordinates (which are physical pixels on Windows Per-Monitor DPI v2).
* 200 % Windows scaling). Use this when you need the pure hardware DPI
* without the user's accessibility font scale applied.
*/
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() {
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) {
const auto& S = schema::UI();
float dp = dpiScale();
float rw = S.drawElement("responsive", "ref-width").sizeOr(1200.0f) * dp;
float rawDp = rawDpiScale(); // reference uses hardware DPI only
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 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);
return logical * dp;
}
@@ -185,8 +238,9 @@ inline float hScale() {
*/
inline float vScale(float availHeight) {
const auto& S = schema::UI();
float dp = dpiScale();
float rh = S.drawElement("responsive", "ref-height").sizeOr(700.0f) * dp;
float rawDp = rawDpiScale(); // reference uses hardware DPI only
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 maxV = S.drawElement("responsive", "max-v-scale").sizeOr(1.4f);
float logical = std::clamp(availHeight / rh, minV, maxV);
@@ -205,8 +259,9 @@ inline float vScale() {
*/
inline float densityScale(float availHeight) {
const auto& S = schema::UI();
float dp = dpiScale();
float rh = S.drawElement("responsive", "ref-height").sizeOr(700.0f) * dp;
float rawDp = rawDpiScale(); // reference uses hardware DPI only
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 maxDen = S.drawElement("responsive", "max-density").sizeOr(1.2f);
float logical = std::clamp(availHeight / rh, minDen, maxDen);

View File

@@ -118,8 +118,12 @@ bool Typography::load(ImGuiIO& io, float dpiScale)
// and DisplayFramebufferScale is 1.0 (no automatic upscaling).
// The window is resized by dpiScale in main.cpp so that fonts at
// size*dpiScale fit proportionally (no overflow).
float scale = dpiScale * Layout::kFontScale();
DEBUG_LOGF("Typography: Loading Material Design type scale (DPI: %.2f, fontScale: %.2f, combined: %.2f)\n", dpiScale, Layout::kFontScale(), scale);
// Layout::userFontScale() is the user-chosen accessibility multiplier
// (1.03.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.
// Font sizes come from Layout:: accessors (backed by UISchema JSON)

View File

@@ -84,6 +84,9 @@ static bool sp_gradient_background = false;
// Low-spec mode
static bool sp_low_spec_mode = false;
// Font scale (user accessibility, 1.03.0)
static float sp_font_scale = 1.0f;
// Snapshot of effect settings saved when low-spec is toggled ON,
// restored when toggled OFF so user state isn't lost.
struct LowSpecSnapshot {
@@ -155,6 +158,8 @@ static void loadSettingsPageState(config::Settings* settings) {
sp_theme_effects_enabled = settings->getThemeEffectsEnabled();
sp_low_spec_mode = settings->getLowSpecMode();
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_stop_external_daemon = settings->getStopExternalDaemon();
sp_debug_categories = settings->getDebugCategories();
@@ -200,6 +205,7 @@ static void saveSettingsPageState(config::Settings* settings) {
settings->setScanlineEnabled(sp_scanline_enabled);
settings->setThemeEffectsEnabled(sp_theme_effects_enabled);
settings->setLowSpecMode(sp_low_spec_mode);
settings->setFontScale(sp_font_scale);
settings->setKeepDaemonRunning(sp_keep_daemon_running);
settings->setStopExternalDaemon(sp_stop_external_daemon);
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
ImGui::Dummy(ImVec2(0, bottomPad));
ImGui::Unindent(pad);

View File

@@ -231,11 +231,29 @@ void ConsoleTab::render(daemon::EmbeddedDaemon* daemon, rpc::RPCClient* rpc, rpc
ImGui::PopFont();
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
// 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 effectiveScrollMax = auto_scroll_ ? consoleScrollMaxY
: std::max(consoleScrollMaxY, consoleScrollY + 10.0f);
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
@@ -552,7 +570,12 @@ void ConsoleTab::renderOutput()
auto& S = schema::UI();
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
float padX = Layout::spacingMd();
@@ -560,7 +583,7 @@ void ConsoleTab::renderOutput()
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + padY);
ImGui::Indent(padX);
float line_height = ImGui::GetTextLineHeightWithSpacing();
float line_height = ImGui::GetTextLineHeight();
output_line_height_ = line_height; // store for scanline alignment
output_origin_ = ImGui::GetCursorScreenPos();
output_scroll_y_ = ImGui::GetScrollY();
@@ -598,33 +621,69 @@ void ConsoleTab::renderOutput()
}
int visible_count = static_cast<int>(visible_indices_.size());
// Calculate wrapped heights for each visible line
// This is needed because TextWrapped creates variable-height content
// Calculate wrapped heights AND build sub-row segments for each visible line.
// 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;
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);
cumulative_y_offsets_.resize(visible_count);
visible_wrap_segments_.resize(visible_count);
total_wrapped_height_ = 0.0f;
cached_wrap_width_ = wrap_width;
for (int vi = 0; vi < visible_count; vi++) {
int i = visible_indices_[vi];
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()) {
sz = ImVec2(0.0f, line_height);
} else {
sz = ImGui::CalcTextSize(text.c_str(), nullptr, false, wrap_width);
// Add a small margin for item spacing
sz.y = std::max(sz.y, line_height);
segs.push_back({0, 0, 0.0f, line_height});
cumulative_y_offsets_[vi] = total_wrapped_height_;
wrapped_heights_[vi] = line_height + interLineGap;
total_wrapped_height_ += wrapped_heights_[vi];
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_;
wrapped_heights_[vi] = sz.y;
total_wrapped_height_ += sz.y;
wrapped_heights_[vi] = segY + interLineGap;
total_wrapped_height_ += wrapped_heights_[vi];
}
// 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)
if (mouse_in_output && io.MouseWheel > 0.0f) {
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
if (mouse_in_output) {
@@ -696,8 +760,9 @@ void ConsoleTab::renderOutput()
TextPos sel_start_pos = selectionStart();
TextPos sel_end_pos = selectionEnd();
// Render lines with selection highlighting
// Use manual rendering instead of ImGuiListClipper to support variable-height wrapped lines
// Render lines with selection highlighting.
// 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 window_height = ImGui::GetWindowHeight();
float visible_top = scroll_y;
@@ -724,10 +789,12 @@ void ConsoleTab::renderOutput()
ImGui::Dummy(ImVec2(0, cumulative_y_offsets_[first_visible]));
}
ImDrawList* dl = ImGui::GetWindowDrawList();
ImU32 selColor = WithAlpha(Secondary(), 80);
// 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++) {
// Early exit if we're past the visible region
if (vi < static_cast<int>(cumulative_y_offsets_.size()) &&
cumulative_y_offsets_[vi] > visible_bottom) {
break;
@@ -736,60 +803,59 @@ void ConsoleTab::renderOutput()
int i = visible_indices_[vi];
const auto& line = lines_[i];
ImVec2 text_pos = ImGui::GetCursorScreenPos();
float this_line_height = (vi < static_cast<int>(wrapped_heights_.size()))
? wrapped_heights_[vi] : line_height;
const auto& segs = visible_wrap_segments_[vi];
ImVec2 lineOrigin = ImGui::GetCursorScreenPos();
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) {
int sel_col_start = 0;
int sel_col_end = static_cast<int>(line.text.size());
lineSelected = true;
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) {
sel_col_start = sel_start_pos.col;
}
if (i == sel_end_pos.line) {
sel_col_end = sel_end_pos.col;
// Selection highlight for this sub-row
if (lineSelected && selByteStart < seg.byteEnd && selByteEnd > seg.byteStart) {
int hlStart = std::max(selByteStart, seg.byteStart) - seg.byteStart;
int hlEnd = std::min(selByteEnd, seg.byteEnd) - seg.byteStart;
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) {
// Calculate pixel positions for highlight
float x_start = 0;
float x_end = 0;
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
);
// Render text segment
if (seg.byteStart < seg.byteEnd) {
dl->AddText(font, fontSize,
ImVec2(lineOrigin.x, rowY),
line.color, segStart, segEnd);
}
}
ImGui::PushStyleColor(ImGuiCol_Text, ImColor(line.color).Value);
ImGui::PushTextWrapPos(ImGui::GetContentRegionAvail().x - padX);
ImGui::TextWrapped("%s", line.text.c_str());
ImGui::PopTextWrapPos();
ImGui::PopStyleColor();
// Advance ImGui cursor by the total wrapped height of this line
ImGui::Dummy(ImVec2(0, totalH));
}
// Add spacer for lines after last visible (to maintain correct content height)
@@ -806,9 +872,10 @@ void ConsoleTab::renderOutput()
ImGui::Unindent(padX);
ImGui::PopStyleVar();
// Add bottom padding so the last line sits above the fade-out zone.
// Only the fade zone height is used — no extra padding beyond that,
// so text can still overflow into the fade and scrolling stays snappy.
// Bottom padding keeps the last line above the fade-out zone.
// Always present so that scrollMaxY stays stable when auto-scroll
// 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,
ImGui::GetWindowHeight() * 0.18f);
@@ -911,14 +978,11 @@ ConsoleTab::TextPos ConsoleTab::screenToTextPos(ImVec2 screen_pos, float line_he
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;
// 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;
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;
while (lo < hi) {
int mid = (lo + hi + 1) / 2;
@@ -929,12 +993,9 @@ ConsoleTab::TextPos ConsoleTab::screenToTextPos(ImVec2 screen_pos, float line_he
}
}
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 >= static_cast<int>(visible_indices_.size())) {
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;
}
// Map visible line index to actual line index
pos.line = visible_indices_[visible_line];
// Calculate column from X position
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;
return pos;
}
// Binary search for the character position
// Walk character by character for accuracy
pos.col = 0;
for (int c = 0; c < static_cast<int>(text.size()); c++) {
ImVec2 sz = ImGui::CalcTextSize(text.c_str(), text.c_str() + c + 1);
float char_mid = (c > 0)
? (ImGui::CalcTextSize(text.c_str(), text.c_str() + c).x + sz.x) * 0.5f
: sz.x * 0.5f;
if (relative_x < char_mid) {
pos.col = c;
return pos;
}
pos.col = c + 1;
// Find which sub-row (wrap segment) the mouse Y falls into
const auto& segs = visible_wrap_segments_[visible_line];
float lineRelY = relative_y - cumulative_y_offsets_[visible_line];
int segIdx = 0;
for (int s = 0; s < static_cast<int>(segs.size()); s++) {
if (lineRelY >= segs[s].yOffset)
segIdx = s;
}
const auto& seg = segs[segIdx];
// Calculate column within this segment from X position
float relative_x = screen_pos.x - output_origin_.x;
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;
}

View File

@@ -113,6 +113,7 @@ private:
char input_buffer_[4096] = {0};
bool auto_scroll_ = true;
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)
size_t last_daemon_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 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
bool show_commands_popup_ = false;
};

View File

@@ -1021,43 +1021,49 @@ void RenderMiningTab(App* app)
}
// 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;
if (n <= 2) {
points = rawPts;
} else {
const int subdivs = 8; // segments between each pair of data points
points.reserve((n - 1) * subdivs + 1);
points.reserve(n * 4); // conservative estimate
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 p1 = rawPts[i];
ImVec2 p2 = rawPts[i + 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++) {
float t = (float)s / (float)subdivs;
float t2 = t * t;
float t3 = t2 * t;
// Catmull-Rom basis
float q0 = -t3 + 2.0f * t2 - t;
float q1 = 3.0f * t3 - 5.0f * t2 + 2.0f;
float q2 = -3.0f * t3 + 4.0f * t2 + t;
float q3 = t3 - t2;
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);
// 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(rawPts[n - 1]); // final point
}
// Fill under curve
for (size_t i = 0; i + 1 < points.size(); i++) {
ImVec2 quad[4] = {
points[i], points[i + 1],
ImVec2(points[i + 1].x, plotBottom),
ImVec2(points[i].x, plotBottom)
};
dl->AddConvexPolyFilled(quad, 4, WithAlpha(Success(), 25));
// Fill under curve (single concave polygon to avoid AA seam shimmer)
if (points.size() >= 2) {
for (size_t i = 0; i < points.size(); i++)
dl->PathLineTo(points[i]);
dl->PathLineTo(ImVec2(points.back().x, plotBottom));
dl->PathLineTo(ImVec2(points.front().x, plotBottom));
dl->PathFillConcave(WithAlpha(Success(), 25));
}
// Green line