refactor(console): phase 1 — extract pure ConsoleTextLayout (wrap/hit-test/selection)

Lift the console's word-wrap layout, screen->text hit-testing, and selection extraction
out of ConsoleTab into a pure, ImGui-free module (console_text_layout.{h,cpp}). Text
measurement is injected via a ConsoleTextMeasure interface — production uses an
ImFont-backed impl (identical CalcWordWrapPositionA/CalcTextSizeA calls as before), while
tests drive a fixed-width stub.

- BuildConsoleLayout: per-line wrap segments + heights + cumulative Y (was inline in
  renderOutput).
- HitTestConsoleLayout: point -> (visibleRow, byte col) (was screenToTextPos).
- ExtractConsoleSelection: selected text across visible lines (was getSelectedText).
- ConsoleTab now holds a single `ConsoleLayout layout_` (replacing 4 mutable members;
  drops the dead cached_wrap_width_) and calls the pure functions.
- New unit test testConsoleTextLayout covers wrapping, multi-row hit-testing, bottom
  clamp, and filtered/unfiltered selection — the biggest previously-untested surface.

No behavior change (algorithm preserved verbatim). Full-node + lite build clean, ctest
green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-01 17:58:29 -05:00
parent f12153cdd0
commit 19e83eb12a
6 changed files with 348 additions and 198 deletions

View File

@@ -15,6 +15,7 @@
#include "ui/windows/console_input_model.h"
#include "ui/windows/console_output_model.h"
#include "ui/windows/console_tab_helpers.h"
#include "ui/windows/console_text_layout.h"
#include "ui/windows/mining_benchmark.h"
#include "ui/windows/mining_pool_panel.h"
#include "ui/windows/mining_tab_helpers.h"
@@ -2282,6 +2283,59 @@ void testDaemonLifecycleAdapters()
fs::remove_all(dataDir);
}
// Pure console wrap layout / hit-test / selection (console_text_layout.h), driven by a
// fixed-width measurement stub so the byte-offset selection math is unit-testable.
void testConsoleTextLayout()
{
using namespace dragonx::ui;
struct StubMeasure : ConsoleTextMeasure {
float charW = 10.0f;
float width(const char* b, const char* e) const override {
return static_cast<float>(e - b) * charW;
}
const char* wrapPosition(const char* b, const char* e, float w) const override {
int maxChars = static_cast<int>(w / charW);
if (maxChars < 1) maxChars = 1;
return (e - b) <= maxChars ? e : b + maxChars;
}
} stub;
std::vector<std::string> lines = { "", "abc", "abcdefghij" }; // 0, 3, 10 chars
auto get = [&lines](int i) -> const std::string& { return lines[i]; };
const float lineH = 12.0f, gap = 2.0f;
// wrapWidth 50 (charW 10 -> 5 chars/row)
ConsoleLayout L = BuildConsoleLayout(static_cast<int>(lines.size()), get, 50.0f, lineH, gap, stub);
EXPECT_EQ(L.segments.size(), static_cast<size_t>(3));
EXPECT_EQ(L.segments[0].size(), static_cast<size_t>(1)); // empty -> one empty seg
EXPECT_EQ(L.segments[0][0].byteEnd, 0);
EXPECT_NEAR(L.heights[0], lineH + gap, 0.01);
EXPECT_EQ(L.segments[1].size(), static_cast<size_t>(1)); // "abc" fits
EXPECT_EQ(L.segments[1][0].byteEnd, 3);
EXPECT_EQ(L.segments[2].size(), static_cast<size_t>(2)); // 10 chars, 5/row -> 2 rows
EXPECT_EQ(L.segments[2][0].byteEnd, 5);
EXPECT_EQ(L.segments[2][1].byteStart, 5);
EXPECT_EQ(L.segments[2][1].byteEnd, 10);
EXPECT_NEAR(L.heights[2], 2 * lineH + gap, 0.01);
EXPECT_NEAR(L.cumulativeY[2], 2 * (lineH + gap), 0.01);
EXPECT_NEAR(L.totalHeight, 3 * (lineH + gap) + lineH, 0.01);
// Hit-test: second wrap row of line 2, column 7.
ConsoleHit hit = HitTestConsoleLayout(L, 3, get, /*relX*/ 20.0f, /*relY*/ 45.0f, stub);
EXPECT_EQ(hit.visibleRow, 2);
EXPECT_EQ(hit.col, 7);
// Past the bottom clamps to the last line's end.
ConsoleHit last = HitTestConsoleLayout(L, 3, get, 1000.0f, 9999.0f, stub);
EXPECT_EQ(last.visibleRow, 2);
EXPECT_EQ(last.col, 10);
// Selection extraction (multi-line, no filter, then filtered to line 0 only).
std::vector<std::string> lines2 = { "hello", "world" };
auto get2 = [&lines2](int i) -> const std::string& { return lines2[i]; };
EXPECT_EQ(ExtractConsoleSelection(2, get2, {}, {0, 1}, {1, 3}), std::string("ello\nwor"));
EXPECT_EQ(ExtractConsoleSelection(2, get2, std::vector<int>{0}, {0, 1}, {1, 3}), std::string("ello"));
}
void testRendererHelpers()
{
EXPECT_NEAR(dragonx::ui::ComputeConsoleInputHeight(20.0f, 4.0f, 8.0f, 2.0f, 1.0f),
@@ -4959,6 +5013,7 @@ int main()
testDaemonShutdownPolicy();
testDaemonLifecycleExecution();
testDaemonLifecycleAdapters();
testConsoleTextLayout();
testRendererHelpers();
testConsoleInputModel();
testMiningBenchmarkModel();