// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 // // Pure implementation of the console wrap layout / hit-test / selection. No ImGui — the // algorithm mirrors the former inline code in ConsoleTab exactly, with text measurement // injected via ConsoleTextMeasure. #include "console_text_layout.h" #include namespace dragonx { namespace ui { ConsoleLayout BuildConsoleLayout(int visibleCount, const ConsoleLineAccessor& getVisibleText, float wrapWidth, float lineHeight, float interLineGap, const ConsoleTextMeasure& measure) { ConsoleLayout L; if (visibleCount < 0) visibleCount = 0; L.segments.resize(visibleCount); L.heights.resize(visibleCount); L.cumulativeY.resize(visibleCount); L.totalHeight = 0.0f; for (int vi = 0; vi < visibleCount; vi++) { const std::string& text = getVisibleText(vi); auto& segs = L.segments[vi]; segs.clear(); if (text.empty()) { segs.push_back({0, 0, 0.0f, lineHeight}); L.cumulativeY[vi] = L.totalHeight; L.heights[vi] = lineHeight + interLineGap; L.totalHeight += L.heights[vi]; continue; } 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 = measure.wrapPosition(cur, textEnd, wrapWidth); if (wrapPos <= cur) wrapPos = cur + 1; // ensure forward progress if (*cur == '\n') { cur++; continue; } // skip a newline ending the prior seg int byteStart = static_cast(cur - textStart); int byteEnd = static_cast(wrapPos - textStart); if (byteEnd > byteStart && text[byteEnd - 1] == '\n') byteEnd--; // trim trailing '\n' segs.push_back({byteStart, byteEnd, segY, lineHeight}); segY += lineHeight; cur = wrapPos; } if (segs.empty()) { segs.push_back({0, 0, 0.0f, lineHeight}); segY = lineHeight; } L.cumulativeY[vi] = L.totalHeight; L.heights[vi] = segY + interLineGap; L.totalHeight += L.heights[vi]; } return L; } ConsoleHit HitTestConsoleLayout(const ConsoleLayout& layout, int visibleCount, const ConsoleLineAccessor& getVisibleText, float relX, float relY, const ConsoleTextMeasure& measure) { ConsoleHit hit{0, 0}; if (visibleCount <= 0 || layout.cumulativeY.empty()) return hit; // Binary search for the visible line containing relY. int visibleLine = 0; { int lo = 0, hi = static_cast(layout.cumulativeY.size()) - 1; while (lo < hi) { int mid = (lo + hi + 1) / 2; if (layout.cumulativeY[mid] <= relY) lo = mid; else hi = mid - 1; } visibleLine = lo; } if (visibleLine < 0) visibleLine = 0; if (visibleLine >= visibleCount) { visibleLine = visibleCount - 1; hit.visibleRow = visibleLine; hit.col = static_cast(getVisibleText(visibleLine).size()); return hit; } hit.visibleRow = visibleLine; const std::string& text = getVisibleText(visibleLine); if (text.empty()) { hit.col = 0; return hit; } // Which wrap sub-row does relY fall into. const auto& segs = layout.segments[visibleLine]; float lineRelY = relY - layout.cumulativeY[visibleLine]; int segIdx = 0; for (int s = 0; s < static_cast(segs.size()); s++) if (lineRelY >= segs[s].yOffset) segIdx = s; const auto& seg = segs[segIdx]; if (relX <= 0.0f) { hit.col = seg.byteStart; return hit; } const char* segStart = text.c_str() + seg.byteStart; int segLen = seg.byteEnd - seg.byteStart; hit.col = seg.byteEnd; // default: past the end of the segment for (int c = 0; c < segLen; c++) { float wCur = measure.width(segStart, segStart + c + 1); float wPrev = (c > 0) ? measure.width(segStart, segStart + c) : 0.0f; float charMid = (wPrev + wCur) * 0.5f; if (relX < charMid) { hit.col = seg.byteStart + c; return hit; } } return hit; } std::string ExtractConsoleSelection(int lineCount, const ConsoleLineAccessor& getLine, const std::vector& visibleIndices, ConsoleTextPos start, ConsoleTextPos end) { if (start.line < 0 || start.line >= lineCount) return ""; // Only restrict to visible lines when a filter is active (fewer visible than total). const bool hasFilter = !visibleIndices.empty() && static_cast(visibleIndices.size()) < lineCount; std::vector visible; if (hasFilter) { visible.assign(lineCount, false); for (int idx : visibleIndices) if (idx >= 0 && idx < lineCount) visible[idx] = true; } std::string result; bool firstLine = true; for (int i = start.line; i <= end.line && i < lineCount; i++) { if (hasFilter && !visible[i]) continue; const std::string& text = getLine(i); int colStart = 0; int colEnd = static_cast(text.size()); if (i == start.line) colStart = std::min(start.col, static_cast(text.size())); if (i == end.line) colEnd = std::min(end.col, static_cast(text.size())); if (!firstLine) result += "\n"; firstLine = false; if (colStart < colEnd) result += text.substr(colStart, colEnd - colStart); } return result; } } // namespace ui } // namespace dragonx