// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 // // ConsoleTextLayout — pure (ImGui-free) word-wrap layout + hit-testing + selection // extraction for the console output. Text measurement is injected via ConsoleTextMeasure // so the production path uses ImFont exactly as before while tests drive it with a // fixed-width stub. Extracted from ConsoleTab::renderOutput / screenToTextPos / // getSelectedText so the trickiest console logic (byte-offset selection across wrapped // rows) becomes unit-testable. #pragma once #include #include #include #include #include #include #include namespace dragonx { namespace ui { // One visual sub-row of a (possibly wrapped) logical line. struct ConsoleWrapSegment { int byteStart = 0; // byte offset into the line's text int byteEnd = 0; // byte offset past the last char on this sub-row float yOffset = 0; // Y offset of this sub-row relative to the line's top float height = 0; // visual height of this sub-row }; // Non-owning, read-only view of one line's wrap segments (points into a ConsoleWrapCache entry). Mirrors the // subset of std::vector the readers use (size/empty/[]/back/range-for), so HitTestConsoleLayout, // ConsoleTab::drawVisibleLines and the existing tests compile unchanged. begin()/end() are null-safe. struct ConsoleSegRef { const std::vector* v = nullptr; std::size_t size() const { return v ? v->size() : 0; } bool empty() const { return size() == 0; } const ConsoleWrapSegment& operator[](std::size_t i) const { return (*v)[i]; } const ConsoleWrapSegment& back() const { return v->back(); } const ConsoleWrapSegment* begin() const { return v ? v->data() : nullptr; } const ConsoleWrapSegment* end() const { return v ? v->data() + v->size() : nullptr; } }; // The wrap layout for a set of visible lines. struct ConsoleLayout { std::vector segments; // per visible line; points into a ConsoleWrapCache (see owner) std::vector heights; // per visible line (includes the inter-line gap) std::vector cumulativeY; // cumulative Y offset of each visible line std::vector rowStart; // V+1 entries: # wrap sub-rows before each visible line (scanline seed) float totalHeight = 0.0f; std::shared_ptr owner; // keeps the cache alive for BuildConsoleLayout results; null in ConsoleTab }; struct ConsoleTextPos { int line = -1; int col = 0; }; // line = raw index; col = byte offset struct ConsoleHit { int visibleRow = 0; int col = 0; }; // visibleRow = index into the visible set // Text measurement backend. Production wraps ImFont; tests inject a fixed-width stub. struct ConsoleTextMeasure { virtual ~ConsoleTextMeasure() = default; // Pixel width of the byte range [begin, end). virtual float width(const char* begin, const char* end) const = 0; // The byte position at which [begin, end) should wrap for the given width — mirrors // ImFont::CalcWordWrapPositionA. Returns `end` when the whole range fits. virtual const char* wrapPosition(const char* begin, const char* end, float wrapWidth) const = 0; }; // Accessor returning a visible/raw line's text by index (avoids copying the buffer). using ConsoleLineAccessor = std::function; // Build the wrap layout for `visibleCount` lines (text via getVisibleText(0..N-1)). One-shot: fresh cache, every // line wrapped once. ConsoleTab uses the incremental ConsoleWrapCache + AssembleConsoleLayout path below. ConsoleLayout BuildConsoleLayout(int visibleCount, const ConsoleLineAccessor& getVisibleText, float wrapWidth, float lineHeight, float interLineGap, const ConsoleTextMeasure& measure); // Exact inputs of WrapConsoleLine besides the text. Compared with == (never quantized: a quantized key would keep // old lines wrapped at the old geometry next to new lines wrapped at the new one, forever). The inter-line gap is // deliberately absent (added at assembly). `fontId` is ImFont::FontId: unique per AddFont and never reused across // an atlas Clear()/reload, unlike the ImFont* address (Typography::reload re-adds fonts in the same order/sizes). struct ConsoleWrapKey { float wrapWidth = -1.0f; float lineHeight = -1.0f; float fontSize = -1.0f; float rasterDensity = -1.0f; const void* font = nullptr; std::uint32_t fontId = 0; bool operator==(const ConsoleWrapKey& o) const { return wrapWidth == o.wrapWidth && lineHeight == o.lineHeight && fontSize == o.fontSize && rasterDensity == o.rasterDensity && font == o.font && fontId == o.fontId; } bool operator!=(const ConsoleWrapKey& o) const { return !(*this == o); } }; // One raw line's cached wrap. struct ConsoleWrapLine { std::vector segs; float heightNoGap = 0.0f; // sum of sub-row heights (the former builder's segY); gap added at assembly bool wrapped = false; // false = never wrapped, or invalidated by a key change }; // Per-RAW-line wrap cache aligned with a ConsoleModel window by absolute sequence number (index == raw model index // after sync()). Main thread only. ConsoleLayout::segments point INTO it, so every sync()/reset() must be followed // by AssembleConsoleLayout (or by clearing the layout) before the layout is read again. Appends (emplace_back) and // front evictions (pop_front) leave the other entries in place. class ConsoleWrapCache { public: // Align with the model window [modelFirstSeq, modelFirstSeq + modelSize): drop the evicted prefix (O(popped)), // append unwrapped entries for new lines (O(added)), mark every entry unwrapped when `key` changed (keeping the // vectors' capacity), and start over when the window does not chain with the cached one. void sync(std::uint64_t modelFirstSeq, std::size_t modelSize, const ConsoleWrapKey& key); // Wrap-on-demand: calls getRawText(raw) + WrapConsoleLine only when the entry is not wrapped. The geometry and // measure passed here must be the ones the synced key describes. const ConsoleWrapLine& ensure(std::size_t raw, const ConsoleLineAccessor& getRawText, float wrapWidth, float lineHeight, const ConsoleTextMeasure& measure); bool matches(const ConsoleWrapKey& key) const { return key_valid_ && key_ == key; } void reset(); // forget everything (ConsoleTab::clear) std::size_t wrapCount() const { return wrap_count_; } // lifetime WrapConsoleLine calls (tests) std::size_t size() const { return lines_.size(); } private: ConsoleWrapKey key_; bool key_valid_ = false; std::uint64_t first_seq_ = 0; std::size_t wrap_count_ = 0; std::deque lines_; }; // Wrap one line (the exact former loop body of BuildConsoleLayout). void WrapConsoleLine(const std::string& text, float wrapWidth, float lineHeight, const ConsoleTextMeasure& measure, ConsoleWrapLine& out); // Re-point `out` at the cached wraps of `visibleRaw` (raw model indices, ascending), wrapping only uncached lines, // and recompute heights / cumulativeY / rowStart / totalHeight with a fresh prefix sum (same float ops in the same // order as the former builder => bit-identical). Reuses out's capacity: no per-line allocation. void AssembleConsoleLayout(ConsoleLayout& out, ConsoleWrapCache& cache, const std::vector& visibleRaw, const ConsoleLineAccessor& getRawText, float wrapWidth, float lineHeight, float interLineGap, const ConsoleTextMeasure& measure); // Map a point relative to the layout origin to a (visibleRow, byte col). The caller maps // visibleRow -> a raw line index. Returns {0,0} when there are no visible lines. ConsoleHit HitTestConsoleLayout(const ConsoleLayout& layout, int visibleCount, const ConsoleLineAccessor& getVisibleText, float relX, float relY, const ConsoleTextMeasure& measure); // Extract the selected text between raw positions [start, end], restricted to visible // lines when a filter is active, joining lines with '\n'. Pure. std::string ExtractConsoleSelection(int lineCount, const ConsoleLineAccessor& getLine, const std::vector& visibleIndices, ConsoleTextPos start, ConsoleTextPos end); } // namespace ui } // namespace dragonx