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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user