// 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 #include namespace dragonx { namespace ui { void WrapConsoleLine(const std::string& text, float wrapWidth, float lineHeight, const ConsoleTextMeasure& measure, ConsoleWrapLine& out) { auto& segs = out.segs; segs.clear(); out.wrapped = true; if (text.empty()) { segs.push_back({0, 0, 0.0f, lineHeight}); out.heightNoGap = lineHeight; return; } 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; } out.heightNoGap = segY; } void ConsoleWrapCache::sync(std::uint64_t modelFirstSeq, std::size_t modelSize, const ConsoleWrapKey& key) { if (!key_valid_ || modelFirstSeq < first_seq_ || modelFirstSeq - first_seq_ > lines_.size()) { lines_.clear(); // first use / reset() / the window jumped past us } else { for (std::uint64_t n = modelFirstSeq - first_seq_; n > 0; --n) lines_.pop_front(); // cap eviction = offset } first_seq_ = modelFirstSeq; if (lines_.size() > modelSize) lines_.clear(); // the model never shrinks at the back: distrust, restart if (!key_valid_ || key_ != key) { for (ConsoleWrapLine& l : lines_) l.wrapped = false; // width/font/size changed: lazy re-wrap, keep capacity key_ = key; key_valid_ = true; } while (lines_.size() < modelSize) lines_.emplace_back(); // new lines start unwrapped } const ConsoleWrapLine& ConsoleWrapCache::ensure(std::size_t raw, const ConsoleLineAccessor& getRawText, float wrapWidth, float lineHeight, const ConsoleTextMeasure& measure) { while (raw >= lines_.size()) lines_.emplace_back(); // defensive: sync() already sized us to the model ConsoleWrapLine& w = lines_[raw]; if (!w.wrapped) { WrapConsoleLine(getRawText(static_cast(raw)), wrapWidth, lineHeight, measure, w); ++wrap_count_; } return w; } void ConsoleWrapCache::reset() { lines_.clear(); key_ = ConsoleWrapKey{}; key_valid_ = false; first_seq_ = 0; } void AssembleConsoleLayout(ConsoleLayout& out, ConsoleWrapCache& cache, const std::vector& visibleRaw, const ConsoleLineAccessor& getRawText, float wrapWidth, float lineHeight, float interLineGap, const ConsoleTextMeasure& measure) { const std::size_t V = visibleRaw.size(); out.owner.reset(); out.segments.clear(); out.segments.reserve(V); out.heights.clear(); out.heights.reserve(V); out.cumulativeY.clear(); out.cumulativeY.reserve(V); out.rowStart.clear(); out.rowStart.reserve(V + 1); float total = 0.0f; int rows = 0; for (std::size_t vi = 0; vi < V; vi++) { const ConsoleWrapLine& w = cache.ensure(static_cast(visibleRaw[vi]), getRawText, wrapWidth, lineHeight, measure); out.segments.push_back(ConsoleSegRef{&w.segs}); out.rowStart.push_back(rows); rows += static_cast(w.segs.size()); out.cumulativeY.push_back(total); const float h = w.heightNoGap + interLineGap; // same op order as the former builder => bit-identical out.heights.push_back(h); total += h; } out.rowStart.push_back(rows); out.totalHeight = total; } ConsoleLayout BuildConsoleLayout(int visibleCount, const ConsoleLineAccessor& getVisibleText, float wrapWidth, float lineHeight, float interLineGap, const ConsoleTextMeasure& measure) { if (visibleCount < 0) visibleCount = 0; auto cache = std::make_shared(); cache->sync(0, static_cast(visibleCount), ConsoleWrapKey{}); std::vector ids(static_cast(visibleCount)); std::iota(ids.begin(), ids.end(), 0); ConsoleLayout L; AssembleConsoleLayout(L, *cache, ids, getVisibleText, wrapWidth, lineHeight, interLineGap, measure); L.owner = cache; // segment refs point into the cache's deque nodes, which outlive moves/copies of L 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