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:
@@ -460,6 +460,7 @@ set(APP_SOURCES
|
|||||||
src/ui/windows/console_input_model.cpp
|
src/ui/windows/console_input_model.cpp
|
||||||
src/ui/windows/console_model.cpp
|
src/ui/windows/console_model.cpp
|
||||||
src/ui/windows/console_output_model.cpp
|
src/ui/windows/console_output_model.cpp
|
||||||
|
src/ui/windows/console_selection_controller.cpp
|
||||||
src/ui/windows/console_tab_helpers.cpp
|
src/ui/windows/console_tab_helpers.cpp
|
||||||
src/ui/windows/console_text_layout.cpp
|
src/ui/windows/console_text_layout.cpp
|
||||||
src/ui/windows/settings_window.cpp
|
src/ui/windows/settings_window.cpp
|
||||||
@@ -585,6 +586,7 @@ set(APP_HEADERS
|
|||||||
src/ui/windows/console_input_model.h
|
src/ui/windows/console_input_model.h
|
||||||
src/ui/windows/console_model.h
|
src/ui/windows/console_model.h
|
||||||
src/ui/windows/console_output_model.h
|
src/ui/windows/console_output_model.h
|
||||||
|
src/ui/windows/console_selection_controller.h
|
||||||
src/ui/windows/console_tab.h
|
src/ui/windows/console_tab.h
|
||||||
src/ui/windows/console_tab_helpers.h
|
src/ui/windows/console_tab_helpers.h
|
||||||
src/ui/windows/settings_window.h
|
src/ui/windows/settings_window.h
|
||||||
@@ -1023,6 +1025,7 @@ if(BUILD_TESTING)
|
|||||||
src/ui/windows/console_input_model.cpp
|
src/ui/windows/console_input_model.cpp
|
||||||
src/ui/windows/console_model.cpp
|
src/ui/windows/console_model.cpp
|
||||||
src/ui/windows/console_output_model.cpp
|
src/ui/windows/console_output_model.cpp
|
||||||
|
src/ui/windows/console_selection_controller.cpp
|
||||||
src/ui/windows/console_tab_helpers.cpp
|
src/ui/windows/console_tab_helpers.cpp
|
||||||
src/ui/windows/console_text_layout.cpp
|
src/ui/windows/console_text_layout.cpp
|
||||||
src/ui/windows/mining_benchmark.cpp
|
src/ui/windows/mining_benchmark.cpp
|
||||||
|
|||||||
89
src/ui/windows/console_selection_controller.cpp
Normal file
89
src/ui/windows/console_selection_controller.cpp
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
|
||||||
|
#include "console_selection_controller.h"
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace ui {
|
||||||
|
|
||||||
|
bool ConsoleSelectionController::isBeforeOrEqual(ConsoleTextPos a, ConsoleTextPos b)
|
||||||
|
{
|
||||||
|
if (a.line < b.line) return true;
|
||||||
|
if (a.line > b.line) return false;
|
||||||
|
return a.col <= b.col;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConsoleSelectionController::beginDrag(ConsoleTextPos pos)
|
||||||
|
{
|
||||||
|
anchor_ = pos;
|
||||||
|
caret_ = pos;
|
||||||
|
dragging_ = true;
|
||||||
|
active_ = false; // not a real selection until the caret moves off the anchor
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConsoleSelectionController::updateDrag(ConsoleTextPos pos)
|
||||||
|
{
|
||||||
|
caret_ = pos;
|
||||||
|
// Becomes a real selection once the caret leaves the anchor; never downgrades back.
|
||||||
|
if (caret_.line != anchor_.line || caret_.col != anchor_.col)
|
||||||
|
active_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConsoleSelectionController::endDrag(ConsoleTextPos pos)
|
||||||
|
{
|
||||||
|
caret_ = pos;
|
||||||
|
dragging_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConsoleSelectionController::selectAll(int lineCount, int lastLineByteLen)
|
||||||
|
{
|
||||||
|
if (lineCount <= 0) return;
|
||||||
|
anchor_ = {0, 0};
|
||||||
|
caret_ = {lineCount - 1, lastLineByteLen};
|
||||||
|
active_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConsoleSelectionController::clear()
|
||||||
|
{
|
||||||
|
active_ = false;
|
||||||
|
dragging_ = false;
|
||||||
|
anchor_ = {-1, 0};
|
||||||
|
caret_ = {-1, 0};
|
||||||
|
}
|
||||||
|
|
||||||
|
ConsoleTextPos ConsoleSelectionController::rangeStart() const
|
||||||
|
{
|
||||||
|
return isBeforeOrEqual(anchor_, caret_) ? anchor_ : caret_;
|
||||||
|
}
|
||||||
|
|
||||||
|
ConsoleTextPos ConsoleSelectionController::rangeEnd() const
|
||||||
|
{
|
||||||
|
return isBeforeOrEqual(anchor_, caret_) ? caret_ : anchor_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConsoleSelectionController::shiftForEviction(int popped)
|
||||||
|
{
|
||||||
|
if (popped <= 0 || !active_) return;
|
||||||
|
anchor_.line -= popped;
|
||||||
|
caret_.line -= popped;
|
||||||
|
if (anchor_.line < 0 && caret_.line < 0) {
|
||||||
|
// Entire selection was in the removed range.
|
||||||
|
active_ = false;
|
||||||
|
dragging_ = false;
|
||||||
|
} else {
|
||||||
|
if (anchor_.line < 0) { anchor_.line = 0; anchor_.col = 0; }
|
||||||
|
if (caret_.line < 0) { caret_.line = 0; caret_.col = 0; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ConsoleSelectionController::extract(int lineCount, const ConsoleLineAccessor& getLine,
|
||||||
|
const std::vector<int>& visibleIndices) const
|
||||||
|
{
|
||||||
|
if (!active_) return "";
|
||||||
|
return ExtractConsoleSelection(lineCount, getLine, visibleIndices,
|
||||||
|
rangeStart(), rangeEnd());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ui
|
||||||
|
} // namespace dragonx
|
||||||
65
src/ui/windows/console_selection_controller.h
Normal file
65
src/ui/windows/console_selection_controller.h
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
//
|
||||||
|
// ConsoleSelectionController — owns the console's text-selection state (anchor + caret) and
|
||||||
|
// all the pure logic around it: the mouse-drag lifecycle, select-all, ordering, extraction,
|
||||||
|
// and the index shift applied when lines are evicted from the top by the buffer cap.
|
||||||
|
//
|
||||||
|
// It is deliberately ImGui-free. Screen->text hit-testing stays in the view (it needs the
|
||||||
|
// current frame's wrapped layout + output origin); the view feeds the resulting
|
||||||
|
// ConsoleTextPos values in here. That keeps the fiddly byte-offset / eviction math unit
|
||||||
|
// testable in isolation from the renderer.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "console_text_layout.h" // ConsoleTextPos, ConsoleLineAccessor, ExtractConsoleSelection
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace ui {
|
||||||
|
|
||||||
|
class ConsoleSelectionController {
|
||||||
|
public:
|
||||||
|
// Mouse-drag lifecycle. Positions are raw {line, col}: line = index into the full model,
|
||||||
|
// col = byte offset within that line.
|
||||||
|
void beginDrag(ConsoleTextPos pos); // button press
|
||||||
|
void updateDrag(ConsoleTextPos pos); // movement while held
|
||||||
|
void endDrag(ConsoleTextPos pos); // release
|
||||||
|
|
||||||
|
// Select the whole buffer, [0,0] .. [lineCount-1, lastLineByteLen]. No-op if empty.
|
||||||
|
void selectAll(int lineCount, int lastLineByteLen);
|
||||||
|
|
||||||
|
// Drop the selection entirely (also ends any in-progress drag).
|
||||||
|
void clear();
|
||||||
|
|
||||||
|
bool active() const { return active_; } // a selection currently exists
|
||||||
|
bool dragging() const { return dragging_; } // a drag is in progress
|
||||||
|
|
||||||
|
// Ordered bounds, start <= end. Only meaningful while active().
|
||||||
|
ConsoleTextPos rangeStart() const;
|
||||||
|
ConsoleTextPos rangeEnd() const;
|
||||||
|
|
||||||
|
// After the cap evicts `popped` lines from the front, shift the selection up to track
|
||||||
|
// the same text. Drops the selection if it fell entirely off the top.
|
||||||
|
void shiftForEviction(int popped);
|
||||||
|
|
||||||
|
// Selected text, or "" when inactive. `visibleIndices` are the raw line indices currently
|
||||||
|
// shown (the selection spans raw lines but copies only the visible ones).
|
||||||
|
std::string extract(int lineCount, const ConsoleLineAccessor& getLine,
|
||||||
|
const std::vector<int>& visibleIndices) const;
|
||||||
|
|
||||||
|
// Position ordering: is a at or before b in reading order?
|
||||||
|
static bool isBeforeOrEqual(ConsoleTextPos a, ConsoleTextPos b);
|
||||||
|
|
||||||
|
private:
|
||||||
|
ConsoleTextPos anchor_{-1, 0}; // where the drag began / the selection origin
|
||||||
|
ConsoleTextPos caret_{-1, 0}; // current drag position / the selection focus
|
||||||
|
bool active_ = false;
|
||||||
|
bool dragging_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ui
|
||||||
|
} // namespace dragonx
|
||||||
@@ -456,15 +456,15 @@ void ConsoleTab::renderToolbar(ConsoleCommandExecutor& exec)
|
|||||||
// Clear button
|
// Clear button
|
||||||
if (TactileButton(TR("console_clear"), ImVec2(0, 0), schema::UI().resolveFont("button"))) {
|
if (TactileButton(TR("console_clear"), ImVec2(0, 0), schema::UI().resolveFont("button"))) {
|
||||||
clear();
|
clear();
|
||||||
clearSelection();
|
selection_.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
|
|
||||||
// Copy button — material styled
|
// Copy button — material styled
|
||||||
if (TactileButton(TR("copy"), ImVec2(0, 0), schema::UI().resolveFont("button"))) {
|
if (TactileButton(TR("copy"), ImVec2(0, 0), schema::UI().resolveFont("button"))) {
|
||||||
if (has_selection_) {
|
if (selection_.active()) {
|
||||||
std::string selected = getSelectedText();
|
std::string selected = selectedText();
|
||||||
if (!selected.empty()) {
|
if (!selected.empty()) {
|
||||||
ImGui::SetClipboardText(selected.c_str());
|
ImGui::SetClipboardText(selected.c_str());
|
||||||
}
|
}
|
||||||
@@ -480,7 +480,7 @@ void ConsoleTab::renderToolbar(ConsoleCommandExecutor& exec)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
material::Tooltip("%s", has_selection_ ? TR("console_copy_selected") : TR("console_copy_all"));
|
material::Tooltip("%s", selection_.active() ? TR("console_copy_selected") : TR("console_copy_all"));
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
@@ -676,50 +676,36 @@ void ConsoleTab::renderOutput()
|
|||||||
|
|
||||||
// Mouse press - start selection (use raw IO mouse state)
|
// Mouse press - start selection (use raw IO mouse state)
|
||||||
if (mouse_in_output && io.MouseClicked[0]) {
|
if (mouse_in_output && io.MouseClicked[0]) {
|
||||||
sel_anchor_ = screenToTextPos(mouse_pos);
|
selection_.beginDrag(screenToTextPos(mouse_pos));
|
||||||
sel_end_ = sel_anchor_;
|
|
||||||
is_selecting_ = true;
|
|
||||||
has_selection_ = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mouse drag - extend selection (continue even if mouse leaves the window)
|
// Mouse drag - extend selection (continue even if mouse leaves the window)
|
||||||
if (is_selecting_ && io.MouseDown[0]) {
|
if (selection_.dragging() && io.MouseDown[0]) {
|
||||||
TextPos new_end = screenToTextPos(mouse_pos);
|
selection_.updateDrag(screenToTextPos(mouse_pos));
|
||||||
sel_end_ = new_end;
|
|
||||||
// Consider it a real selection once the position changes
|
|
||||||
if (sel_end_.line != sel_anchor_.line || sel_end_.col != sel_anchor_.col) {
|
|
||||||
has_selection_ = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mouse release - finalize selection
|
// Mouse release - finalize selection
|
||||||
if (is_selecting_ && io.MouseReleased[0]) {
|
if (selection_.dragging() && io.MouseReleased[0]) {
|
||||||
sel_end_ = screenToTextPos(mouse_pos);
|
selection_.endDrag(screenToTextPos(mouse_pos));
|
||||||
is_selecting_ = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ctrl+C / Ctrl+A
|
// Ctrl+C / Ctrl+A
|
||||||
if (mouse_in_output || has_selection_) {
|
if (mouse_in_output || selection_.active()) {
|
||||||
if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_C)) {
|
if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_C)) {
|
||||||
std::string selected = getSelectedText();
|
std::string selected = selectedText();
|
||||||
if (!selected.empty()) {
|
if (!selected.empty()) {
|
||||||
ImGui::SetClipboardText(selected.c_str());
|
ImGui::SetClipboardText(selected.c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_A)) {
|
if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_A) && !model_.empty()) {
|
||||||
// Select all
|
selection_.selectAll(static_cast<int>(model_.size()),
|
||||||
if (!model_.empty()) {
|
static_cast<int>(model_.back().text.size()));
|
||||||
sel_anchor_ = {0, 0};
|
|
||||||
sel_end_ = {static_cast<int>(model_.size()) - 1,
|
|
||||||
static_cast<int>(model_.back().text.size())};
|
|
||||||
has_selection_ = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get selection bounds (ordered)
|
// Get selection bounds (ordered)
|
||||||
TextPos sel_start_pos = selectionStart();
|
ConsoleTextPos sel_start_pos = selection_.rangeStart();
|
||||||
TextPos sel_end_pos = selectionEnd();
|
ConsoleTextPos sel_end_pos = selection_.rangeEnd();
|
||||||
|
|
||||||
// Render lines with selection highlighting.
|
// Render lines with selection highlighting.
|
||||||
// Each line is split into pre-computed wrap segments rendered individually
|
// Each line is split into pre-computed wrap segments rendered individually
|
||||||
@@ -811,7 +797,7 @@ void ConsoleTab::renderOutput()
|
|||||||
// Determine byte-level selection range for this line
|
// Determine byte-level selection range for this line
|
||||||
int selByteStart = 0, selByteEnd = 0;
|
int selByteStart = 0, selByteEnd = 0;
|
||||||
bool lineSelected = false;
|
bool lineSelected = false;
|
||||||
if (has_selection_ && i >= sel_start_pos.line && i <= sel_end_pos.line) {
|
if (selection_.active() && i >= sel_start_pos.line && i <= sel_end_pos.line) {
|
||||||
lineSelected = true;
|
lineSelected = true;
|
||||||
selByteStart = (i == sel_start_pos.line) ? sel_start_pos.col : 0;
|
selByteStart = (i == sel_start_pos.line) ? sel_start_pos.col : 0;
|
||||||
selByteEnd = (i == sel_end_pos.line) ? sel_end_pos.col
|
selByteEnd = (i == sel_end_pos.line) ? sel_end_pos.col
|
||||||
@@ -925,7 +911,7 @@ void ConsoleTab::renderOutput()
|
|||||||
|
|
||||||
// Capture the hash/address under the cursor on right-click, for "Copy value".
|
// Capture the hash/address under the cursor on right-click, for "Copy value".
|
||||||
if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
|
if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
|
||||||
TextPos tp = screenToTextPos(ImGui::GetMousePos());
|
ConsoleTextPos tp = screenToTextPos(ImGui::GetMousePos());
|
||||||
context_token_.clear();
|
context_token_.clear();
|
||||||
if (tp.line >= 0 && tp.line < static_cast<int>(model_.size()))
|
if (tp.line >= 0 && tp.line < static_cast<int>(model_.size()))
|
||||||
context_token_ = extractCopyableToken(model_[tp.line].text, tp.col);
|
context_token_ = extractCopyableToken(model_[tp.line].text, tp.col);
|
||||||
@@ -943,25 +929,23 @@ void ConsoleTab::renderOutput()
|
|||||||
}
|
}
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
}
|
}
|
||||||
if (ImGui::MenuItem(TR("copy"), "Ctrl+C", false, has_selection_)) {
|
if (ImGui::MenuItem(TR("copy"), "Ctrl+C", false, selection_.active())) {
|
||||||
std::string selected = getSelectedText();
|
std::string selected = selectedText();
|
||||||
if (!selected.empty()) {
|
if (!selected.empty()) {
|
||||||
ImGui::SetClipboardText(selected.c_str());
|
ImGui::SetClipboardText(selected.c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ImGui::MenuItem(TR("console_select_all"), "Ctrl+A")) {
|
if (ImGui::MenuItem(TR("console_select_all"), "Ctrl+A")) {
|
||||||
if (!model_.empty()) {
|
if (!model_.empty()) {
|
||||||
sel_anchor_ = {0, 0};
|
selection_.selectAll(static_cast<int>(model_.size()),
|
||||||
sel_end_ = {static_cast<int>(model_.size()) - 1,
|
static_cast<int>(model_.back().text.size()));
|
||||||
static_cast<int>(model_.back().text.size())};
|
|
||||||
has_selection_ = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
if (ImGui::MenuItem(TR("console_clear_console"))) {
|
if (ImGui::MenuItem(TR("console_clear_console"))) {
|
||||||
// View-only clear (main thread) — drop the visible lines and the selection.
|
// View-only clear (main thread) — drop the visible lines and the selection.
|
||||||
model_.clear();
|
model_.clear();
|
||||||
has_selection_ = false;
|
selection_.clear();
|
||||||
}
|
}
|
||||||
ImGui::EndPopup();
|
ImGui::EndPopup();
|
||||||
}
|
}
|
||||||
@@ -1008,7 +992,7 @@ void ConsoleTab::renderOutput()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ConsoleTab::TextPos ConsoleTab::screenToTextPos(ImVec2 screen_pos) const
|
ConsoleTextPos ConsoleTab::screenToTextPos(ImVec2 screen_pos) const
|
||||||
{
|
{
|
||||||
if (visible_indices_.empty()) return {0, 0};
|
if (visible_indices_.empty()) return {0, 0};
|
||||||
|
|
||||||
@@ -1018,47 +1002,18 @@ ConsoleTab::TextPos ConsoleTab::screenToTextPos(ImVec2 screen_pos) const
|
|||||||
[this](int vi) -> const std::string& { return model_[visible_indices_[vi]].text; },
|
[this](int vi) -> const std::string& { return model_[visible_indices_[vi]].text; },
|
||||||
screen_pos.x - output_origin_.x, screen_pos.y - output_origin_.y, measure);
|
screen_pos.x - output_origin_.x, screen_pos.y - output_origin_.y, measure);
|
||||||
|
|
||||||
TextPos pos;
|
ConsoleTextPos pos;
|
||||||
pos.line = visible_indices_[hit.visibleRow];
|
pos.line = visible_indices_[hit.visibleRow];
|
||||||
pos.col = hit.col;
|
pos.col = hit.col;
|
||||||
return pos;
|
return pos;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ConsoleTab::isPosBeforeOrEqual(const TextPos& a, const TextPos& b) const
|
std::string ConsoleTab::selectedText() const
|
||||||
{
|
{
|
||||||
if (a.line < b.line) return true;
|
return selection_.extract(
|
||||||
if (a.line > b.line) return false;
|
|
||||||
return a.col <= b.col;
|
|
||||||
}
|
|
||||||
|
|
||||||
ConsoleTab::TextPos ConsoleTab::selectionStart() const
|
|
||||||
{
|
|
||||||
return isPosBeforeOrEqual(sel_anchor_, sel_end_) ? sel_anchor_ : sel_end_;
|
|
||||||
}
|
|
||||||
|
|
||||||
ConsoleTab::TextPos ConsoleTab::selectionEnd() const
|
|
||||||
{
|
|
||||||
return isPosBeforeOrEqual(sel_anchor_, sel_end_) ? sel_end_ : sel_anchor_;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string ConsoleTab::getSelectedText() const
|
|
||||||
{
|
|
||||||
if (!has_selection_) return "";
|
|
||||||
TextPos start = selectionStart();
|
|
||||||
TextPos end = selectionEnd();
|
|
||||||
return ExtractConsoleSelection(
|
|
||||||
static_cast<int>(model_.size()),
|
static_cast<int>(model_.size()),
|
||||||
[this](int i) -> const std::string& { return model_[i].text; },
|
[this](int i) -> const std::string& { return model_[i].text; },
|
||||||
visible_indices_,
|
visible_indices_);
|
||||||
{start.line, start.col}, {end.line, end.col});
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConsoleTab::clearSelection()
|
|
||||||
{
|
|
||||||
has_selection_ = false;
|
|
||||||
is_selecting_ = false;
|
|
||||||
sel_anchor_ = {-1, 0};
|
|
||||||
sel_end_ = {-1, 0};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConsoleTab::renderInput(ConsoleCommandExecutor& exec)
|
void ConsoleTab::renderInput(ConsoleCommandExecutor& exec)
|
||||||
@@ -1165,7 +1120,7 @@ void ConsoleTab::renderInput(ConsoleCommandExecutor& exec)
|
|||||||
if (first == "clear" || first == "cls") {
|
if (first == "clear" || first == "cls") {
|
||||||
// View-only clear — NEVER forwarded (the lite backend's `clear` wipes tx history).
|
// View-only clear — NEVER forwarded (the lite backend's `clear` wipes tx history).
|
||||||
clear();
|
clear();
|
||||||
clearSelection();
|
selection_.clear();
|
||||||
} else if (first == "help") {
|
} else if (first == "help") {
|
||||||
exec.printHelp(add);
|
exec.printHelp(add);
|
||||||
} else if (first == "quit" || first == "exit") {
|
} else if (first == "quit" || first == "exit") {
|
||||||
@@ -1428,17 +1383,7 @@ void ConsoleTab::drainModel()
|
|||||||
|
|
||||||
// Lines evicted from the front by the cap shift every index down — move the selection
|
// Lines evicted from the front by the cap shift every index down — move the selection
|
||||||
// with them so the highlight stays on the text the user selected.
|
// with them so the highlight stays on the text the user selected.
|
||||||
if (dr.popped > 0 && has_selection_) {
|
selection_.shiftForEviction(static_cast<int>(dr.popped));
|
||||||
sel_anchor_.line -= static_cast<int>(dr.popped);
|
|
||||||
sel_end_.line -= static_cast<int>(dr.popped);
|
|
||||||
if (sel_anchor_.line < 0 && sel_end_.line < 0) {
|
|
||||||
has_selection_ = false; // entire selection was in the removed range
|
|
||||||
is_selecting_ = false;
|
|
||||||
} else {
|
|
||||||
if (sel_anchor_.line < 0) { sel_anchor_.line = 0; sel_anchor_.col = 0; }
|
|
||||||
if (sel_end_.line < 0) { sel_end_.line = 0; sel_end_.col = 0; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track new output that arrived while the user is scrolled up (for the indicator).
|
// Track new output that arrived while the user is scrolled up (for the indicator).
|
||||||
if (!auto_scroll_)
|
if (!auto_scroll_)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
#include "console_channel.h"
|
#include "console_channel.h"
|
||||||
#include "console_model.h"
|
#include "console_model.h"
|
||||||
|
#include "console_selection_controller.h"
|
||||||
#include "console_text_layout.h"
|
#include "console_text_layout.h"
|
||||||
#include "../layout.h"
|
#include "../layout.h"
|
||||||
#include "../../daemon/embedded_daemon.h"
|
#include "../../daemon/embedded_daemon.h"
|
||||||
@@ -103,19 +104,12 @@ private:
|
|||||||
void renderInput(ConsoleCommandExecutor& exec);
|
void renderInput(ConsoleCommandExecutor& exec);
|
||||||
void renderCommandsPopup();
|
void renderCommandsPopup();
|
||||||
|
|
||||||
// Selection helpers
|
// Map a screen point to a raw {line, col} position via this frame's wrapped layout.
|
||||||
std::string getSelectedText() const;
|
// (View-side because it needs layout_ + output_origin_; feeds the selection controller.)
|
||||||
void clearSelection();
|
ConsoleTextPos screenToTextPos(ImVec2 screen_pos) const;
|
||||||
|
|
||||||
// Convert screen position to line/column
|
// The current selection's text (via selection_ over the visible model). "" if inactive.
|
||||||
struct TextPos {
|
std::string selectedText() const;
|
||||||
int line = -1;
|
|
||||||
int col = 0;
|
|
||||||
};
|
|
||||||
TextPos screenToTextPos(ImVec2 screen_pos) const;
|
|
||||||
bool isPosBeforeOrEqual(const TextPos& a, const TextPos& b) const;
|
|
||||||
TextPos selectionStart() const; // Returns the earlier of sel_anchor_ and sel_end_
|
|
||||||
TextPos selectionEnd() const; // Returns the later of sel_anchor_ and sel_end_
|
|
||||||
|
|
||||||
// Thread-safe line store: producers on any thread call model_.ingest(); the main
|
// Thread-safe line store: producers on any thread call model_.ingest(); the main
|
||||||
// thread drains it once per frame in render(). The visible deque is main-thread-only.
|
// thread drains it once per frame in render(). The visible deque is main-thread-only.
|
||||||
@@ -128,11 +122,8 @@ private:
|
|||||||
int new_lines_since_scroll_ = 0; // new lines while scrolled up (for indicator)
|
int new_lines_since_scroll_ = 0; // new lines while scrolled up (for indicator)
|
||||||
// (log-ingestion cursors + result queue moved to the ConsoleCommandExecutor)
|
// (log-ingestion cursors + result queue moved to the ConsoleCommandExecutor)
|
||||||
|
|
||||||
// Text selection state
|
// Text-selection state + logic (anchor/caret, ordering, extraction, eviction shift).
|
||||||
bool is_selecting_ = false;
|
ConsoleSelectionController selection_;
|
||||||
bool has_selection_ = false;
|
|
||||||
TextPos sel_anchor_; // Where the mouse was first pressed
|
|
||||||
TextPos sel_end_; // Where the mouse currently is / was released
|
|
||||||
float output_scroll_y_ = 0.0f; // Track scroll position for selection
|
float output_scroll_y_ = 0.0f; // Track scroll position for selection
|
||||||
ImVec2 output_origin_ = {0, 0}; // Top-left of output area
|
ImVec2 output_origin_ = {0, 0}; // Top-left of output area
|
||||||
float output_line_height_ = 0.0f; // Text line height (for scanline alignment)
|
float output_line_height_ = 0.0f; // Text line height (for scanline alignment)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
#include "ui/windows/console_input_model.h"
|
#include "ui/windows/console_input_model.h"
|
||||||
#include "ui/windows/console_model.h"
|
#include "ui/windows/console_model.h"
|
||||||
#include "ui/windows/console_output_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_tab_helpers.h"
|
||||||
#include "ui/windows/console_text_layout.h"
|
#include "ui/windows/console_text_layout.h"
|
||||||
#include "ui/windows/mining_benchmark.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"));
|
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.
|
// ConsoleModel — thread-safe ingest queue drained on the main thread, with a line cap.
|
||||||
void testConsoleModel()
|
void testConsoleModel()
|
||||||
{
|
{
|
||||||
@@ -5104,6 +5192,7 @@ int main()
|
|||||||
testDaemonLifecycleAdapters();
|
testDaemonLifecycleAdapters();
|
||||||
testConsoleTextLayout();
|
testConsoleTextLayout();
|
||||||
testConsoleModel();
|
testConsoleModel();
|
||||||
|
testConsoleSelectionController();
|
||||||
testRendererHelpers();
|
testRendererHelpers();
|
||||||
testConsoleInputModel();
|
testConsoleInputModel();
|
||||||
testMiningBenchmarkModel();
|
testMiningBenchmarkModel();
|
||||||
|
|||||||
Reference in New Issue
Block a user