console and mining tab visual improvements
This commit is contained in:
@@ -36,16 +36,68 @@ namespace Layout {
|
||||
// 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
|
||||
* 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);
|
||||
|
||||
@@ -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.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.
|
||||
// Font sizes come from Layout:: accessors (backed by UISchema JSON)
|
||||
|
||||
@@ -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.0–3.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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user