refactor(console): phase 4 — extract ConsoleSelectionController from the god-class
Pull the text-selection concern out of ConsoleTab into a focused, ImGui-free
ConsoleSelectionController (console_selection_controller.{h,cpp}). It owns the
anchor/caret state and all the fiddly logic around it: the mouse-drag lifecycle
(beginDrag/updateDrag/endDrag), select-all, range ordering, text extraction over
the visible model, and the index shift applied when the buffer cap evicts lines
from the top.
ConsoleTab now delegates: the four scattered selection fields (is_selecting_,
has_selection_, sel_anchor_, sel_end_) and five helper methods (selectionStart/
End, isPosBeforeOrEqual, getSelectedText, clearSelection) collapse to a single
selection_ member plus a thin selectedText() wrapper. screenToTextPos (still
view-side — it needs the frame's layout) now returns the shared ConsoleTextPos,
so ConsoleTab::TextPos is retired.
The byte-offset / eviction math was previously only reachable through the live
ImGui renderer; it now has direct unit coverage (testConsoleSelectionController:
ordering, drag lifecycle, upward drag, select-all, partial/full/no-op eviction
shift, and filtered extraction). Full-node + lite build clean; ctest green;
source hygiene clean.
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_model.h"
|
||||
#include "ui/windows/console_output_model.h"
|
||||
#include "ui/windows/console_selection_controller.h"
|
||||
#include "ui/windows/console_tab_helpers.h"
|
||||
#include "ui/windows/console_text_layout.h"
|
||||
#include "ui/windows/mining_benchmark.h"
|
||||
@@ -2337,6 +2338,93 @@ void testConsoleTextLayout()
|
||||
EXPECT_EQ(ExtractConsoleSelection(2, get2, std::vector<int>{0}, {0, 1}, {1, 3}), std::string("ello"));
|
||||
}
|
||||
|
||||
// ConsoleSelectionController — anchor/caret state, ordering, select-all, eviction shift.
|
||||
void testConsoleSelectionController()
|
||||
{
|
||||
using dragonx::ui::ConsoleSelectionController;
|
||||
using dragonx::ui::ConsoleTextPos;
|
||||
|
||||
// Ordering helper.
|
||||
EXPECT_TRUE(ConsoleSelectionController::isBeforeOrEqual({1, 2}, {1, 5}));
|
||||
EXPECT_TRUE(ConsoleSelectionController::isBeforeOrEqual({1, 5}, {2, 0}));
|
||||
EXPECT_TRUE(ConsoleSelectionController::isBeforeOrEqual({3, 4}, {3, 4}));
|
||||
EXPECT_FALSE(ConsoleSelectionController::isBeforeOrEqual({2, 0}, {1, 9}));
|
||||
|
||||
// Drag lifecycle: press starts a drag but is not yet a real selection.
|
||||
{
|
||||
ConsoleSelectionController sel;
|
||||
sel.beginDrag({2, 3});
|
||||
EXPECT_TRUE(sel.dragging());
|
||||
EXPECT_FALSE(sel.active());
|
||||
sel.updateDrag({2, 3}); // no movement -> still not active
|
||||
EXPECT_FALSE(sel.active());
|
||||
sel.updateDrag({4, 1}); // moved -> real selection
|
||||
EXPECT_TRUE(sel.active());
|
||||
sel.endDrag({4, 1});
|
||||
EXPECT_FALSE(sel.dragging());
|
||||
EXPECT_TRUE(sel.active());
|
||||
// Ordered range is anchor..caret regardless of drag direction.
|
||||
EXPECT_EQ(sel.rangeStart().line, 2);
|
||||
EXPECT_EQ(sel.rangeStart().col, 3);
|
||||
EXPECT_EQ(sel.rangeEnd().line, 4);
|
||||
EXPECT_EQ(sel.rangeEnd().col, 1);
|
||||
}
|
||||
|
||||
// Upward drag: caret before anchor -> range is still ordered low..high.
|
||||
{
|
||||
ConsoleSelectionController sel;
|
||||
sel.beginDrag({5, 2});
|
||||
sel.updateDrag({1, 0});
|
||||
EXPECT_EQ(sel.rangeStart().line, 1);
|
||||
EXPECT_EQ(sel.rangeEnd().line, 5);
|
||||
}
|
||||
|
||||
// Select-all + clear.
|
||||
{
|
||||
ConsoleSelectionController sel;
|
||||
sel.selectAll(0, 0); // empty buffer -> no-op
|
||||
EXPECT_FALSE(sel.active());
|
||||
sel.selectAll(3, 7); // lines 0..2, last line 7 bytes
|
||||
EXPECT_TRUE(sel.active());
|
||||
EXPECT_EQ(sel.rangeStart().line, 0);
|
||||
EXPECT_EQ(sel.rangeStart().col, 0);
|
||||
EXPECT_EQ(sel.rangeEnd().line, 2);
|
||||
EXPECT_EQ(sel.rangeEnd().col, 7);
|
||||
sel.clear();
|
||||
EXPECT_FALSE(sel.active());
|
||||
EXPECT_FALSE(sel.dragging());
|
||||
}
|
||||
|
||||
// Eviction shift: partial (top of selection clamped), full (dropped), and no-op.
|
||||
{
|
||||
ConsoleSelectionController sel;
|
||||
sel.selectAll(10, 4); // [0,0]..[9,4]
|
||||
sel.shiftForEviction(3); // 3 lines fell off the top
|
||||
EXPECT_TRUE(sel.active());
|
||||
EXPECT_EQ(sel.rangeStart().line, 0); // was line 0 -> clamped to 0
|
||||
EXPECT_EQ(sel.rangeStart().col, 0);
|
||||
EXPECT_EQ(sel.rangeEnd().line, 6); // 9 - 3
|
||||
EXPECT_EQ(sel.rangeEnd().col, 4);
|
||||
sel.shiftForEviction(0); // no-op
|
||||
EXPECT_EQ(sel.rangeEnd().line, 6);
|
||||
sel.shiftForEviction(100); // whole selection evicted -> dropped
|
||||
EXPECT_FALSE(sel.active());
|
||||
}
|
||||
|
||||
// extract(): inactive -> "", active -> the joined selected text.
|
||||
{
|
||||
std::vector<std::string> lines = {"hello", "world"};
|
||||
auto get = [&lines](int i) -> const std::string& { return lines[i]; };
|
||||
ConsoleSelectionController sel;
|
||||
EXPECT_EQ(sel.extract(2, get, {}), std::string(""));
|
||||
sel.beginDrag({0, 1});
|
||||
sel.updateDrag({1, 3});
|
||||
EXPECT_EQ(sel.extract(2, get, {}), std::string("ello\nwor"));
|
||||
// Filtered so only line 0 is visible -> selection clips to it.
|
||||
EXPECT_EQ(sel.extract(2, get, std::vector<int>{0}), std::string("ello"));
|
||||
}
|
||||
}
|
||||
|
||||
// ConsoleModel — thread-safe ingest queue drained on the main thread, with a line cap.
|
||||
void testConsoleModel()
|
||||
{
|
||||
@@ -5104,6 +5192,7 @@ int main()
|
||||
testDaemonLifecycleAdapters();
|
||||
testConsoleTextLayout();
|
||||
testConsoleModel();
|
||||
testConsoleSelectionController();
|
||||
testRendererHelpers();
|
||||
testConsoleInputModel();
|
||||
testMiningBenchmarkModel();
|
||||
|
||||
Reference in New Issue
Block a user