perf(console): wrap only new console lines via a per-line cache
Each batch of new output re-wrapped all N visible lines glyph by glyph, allocating a fresh segment vector per line; a line evicted at the 10,000-line cap shifted every index, so nothing could be kept. And on every frame, idle or not, seeding the scanline shading summed the wrap rows of every line above the view. - The model numbers its lines and the view keeps one wrap per line, aligned by that number, so an eviction drops a prefix instead of forcing a rebuild. New output wraps only the new lines; the layout is then a prefix sum over the N visible lines, with no measuring and no per-line allocation. - The wraps are kept while the wrap width, line height, font size, font (by ImFont::FontId) and rasterizer density are exactly unchanged. A line-spacing change only re-sums heights; filters and fold toggles reuse the cached wraps. A resize drag still re-wraps every visible line per frame, as before, but reuses their storage. - The scanline seed is read from the prefix sum: O(1) per frame. Heights, offsets and segments are bit-identical to the old rebuild. A font reload at an unchanged size (the colour-emoji toggle) now re-wraps the console at once; before, it waited for the next line of output. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,7 @@ ConsoleModel::DrainResult ConsoleModel::drain()
|
||||
while (lines_.size() > max_lines_) {
|
||||
lines_.pop_front();
|
||||
++result.popped;
|
||||
++first_seq_;
|
||||
}
|
||||
++revision_; // deque changed — lets the view memoize its filter/layout passes
|
||||
return result;
|
||||
@@ -43,6 +44,7 @@ ConsoleModel::DrainResult ConsoleModel::drain()
|
||||
|
||||
void ConsoleModel::clear()
|
||||
{
|
||||
first_seq_ += lines_.size(); // the discarded lines keep their sequence numbers (view caches realign by seq)
|
||||
lines_.clear();
|
||||
++revision_;
|
||||
}
|
||||
|
||||
@@ -81,12 +81,18 @@ public:
|
||||
// fold toggle). The view memoizes its per-frame filter + text-layout passes against this.
|
||||
std::uint64_t revision() const { return revision_; }
|
||||
|
||||
// Absolute sequence number of lines_.front(): +1 per line evicted from the front by the cap, +size() on
|
||||
// clear(). Line text never changes after ingest, so a view cache keyed by sequence number realigns with
|
||||
// one subtraction (eviction becomes an offset, not a rebuild). Never decreases.
|
||||
std::uint64_t firstSeq() const { return first_seq_; }
|
||||
|
||||
private:
|
||||
const std::size_t max_lines_;
|
||||
std::deque<ConsoleModelLine> lines_; // visible model — main thread only
|
||||
std::vector<ConsoleModelLine> pending_; // guarded by ingest_mutex_
|
||||
std::mutex ingest_mutex_;
|
||||
std::uint64_t revision_ = 0;
|
||||
std::uint64_t first_seq_ = 0; // see firstSeq()
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
|
||||
@@ -829,11 +829,25 @@ void ConsoleTab::renderOutput()
|
||||
layoutKey = layoutKey * 131ull + static_cast<std::uint64_t>(line_height * 16.0f);
|
||||
layoutKey = layoutKey * 131ull + static_cast<std::uint64_t>(interLineGap * 16.0f);
|
||||
layoutKey = layoutKey * 131ull + static_cast<std::uint64_t>(s_console_zoom * 1000.0f);
|
||||
if (layoutKey != layout_key_) {
|
||||
ImFontConsoleMeasure measure(ImGui::GetFont(), ImGui::GetFontSize());
|
||||
layout_ = BuildConsoleLayout(
|
||||
static_cast<int>(visible_indices_.size()),
|
||||
[this](int vi) -> const std::string& { return model_[visible_indices_[vi]].text; },
|
||||
// Exact inputs of the per-line wrap (the gap is excluded: it is added at assembly, so a line-spacing change
|
||||
// only re-sums heights). FontId, not the ImFont* address (an atlas reload can reuse it), catches a font reload
|
||||
// at an unchanged size, e.g. the stb_truetype <-> FreeType swap of the chat colour-emoji toggle.
|
||||
ImFont* outFont = ImGui::GetFont();
|
||||
ConsoleWrapKey wrapKey;
|
||||
wrapKey.wrapWidth = wrap_width;
|
||||
wrapKey.lineHeight = line_height;
|
||||
wrapKey.fontSize = ImGui::GetFontSize();
|
||||
wrapKey.rasterDensity = outFont->CurrentRasterizerDensity;
|
||||
wrapKey.font = outFont;
|
||||
wrapKey.fontId = outFont->FontId;
|
||||
if (layoutKey != layout_key_ || !wrap_cache_.matches(wrapKey)) {
|
||||
ImFontConsoleMeasure measure(outFont, ImGui::GetFontSize());
|
||||
// Align the per-line cache with the model (evicted prefix dropped by sequence number, new lines appended,
|
||||
// everything invalidated on a geometry/font change), then re-point layout_ at it: only lines not yet
|
||||
// wrapped at this geometry are measured; the rest is an O(visible) prefix sum with no allocation.
|
||||
wrap_cache_.sync(model_.firstSeq(), model_.size(), wrapKey);
|
||||
AssembleConsoleLayout(layout_, wrap_cache_, visible_indices_,
|
||||
[this](int raw) -> const std::string& { return model_[static_cast<std::size_t>(raw)].text; },
|
||||
wrap_width, line_height, interLineGap, measure);
|
||||
layout_key_ = layoutKey;
|
||||
}
|
||||
@@ -1042,8 +1056,9 @@ void ConsoleTab::drawVisibleLines(float padX, float lineHeight, bool hasTextFilt
|
||||
// gap) and skipped rows, breaking the alternation. Seed it with the sub-row ordinal of
|
||||
// the first on-screen line so the shading stays stable across scrolling.
|
||||
int scanRowOrdinal = 0;
|
||||
for (int k = 0; k < first_visible && k < static_cast<int>(layout_.segments.size()); k++)
|
||||
scanRowOrdinal += static_cast<int>(layout_.segments[k].size());
|
||||
if (!layout_.rowStart.empty()) // prefix sub-row counts from AssembleConsoleLayout: same value as the old sum
|
||||
scanRowOrdinal = layout_.rowStart[std::min<std::size_t>(static_cast<std::size_t>(first_visible),
|
||||
layout_.rowStart.size() - 1)];
|
||||
|
||||
int last_rendered_vi = first_visible - 1;
|
||||
for (int vi = first_visible; vi < visible_count; vi++) {
|
||||
@@ -2112,6 +2127,8 @@ void ConsoleTab::clear()
|
||||
vis_key_ = ~0ull;
|
||||
layout_key_ = ~0ull;
|
||||
++vis_generation_;
|
||||
layout_ = ConsoleLayout{}; // drop the refs into wrap_cache_ BEFORE resetting it (no dangling segments)
|
||||
wrap_cache_.reset();
|
||||
stop_confirm_pending_ = false; // a pending 'stop' confirmation is cancelled by clearing
|
||||
addLine(TR("console_cleared"), ConsoleChannel::Info);
|
||||
}
|
||||
|
||||
@@ -195,11 +195,15 @@ private:
|
||||
// resulting visible-set generation + wrap width + line height/zoom.
|
||||
std::uint64_t vis_key_ = ~0ull; // key of the last computeVisibleLines
|
||||
std::uint64_t vis_generation_ = 0; // bumped whenever visible_indices_ is rebuilt
|
||||
std::uint64_t layout_key_ = ~0ull; // key of the last BuildConsoleLayout
|
||||
std::uint64_t layout_key_ = ~0ull; // key of the last layout assembly
|
||||
|
||||
// Wrap layout for the visible lines (segments + per-line heights + cumulative Y),
|
||||
// recomputed each frame by the pure BuildConsoleLayout (console_text_layout.h) and
|
||||
// consumed by the renderer + hit-testing.
|
||||
// Per-raw-line wrap cache (index == model_ index after sync). layout_.segments point INTO it, so it is only
|
||||
// mutated by sync() immediately followed by AssembleConsoleLayout (renderOutput), or by reset() together
|
||||
// with clearing layout_ (clear()). Touch it nowhere else.
|
||||
ConsoleWrapCache wrap_cache_;
|
||||
|
||||
// Wrap layout for the visible lines (segments + per-line heights + cumulative Y), re-assembled from
|
||||
// wrap_cache_ when the visible set / geometry changes, and consumed by the renderer + hit-testing.
|
||||
mutable ConsoleLayout layout_;
|
||||
|
||||
// Commands popup (command explorer) — indexes into the active executor's commandReference()
|
||||
|
||||
@@ -9,63 +9,127 @@
|
||||
#include "console_text_layout.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <numeric>
|
||||
|
||||
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<int>(cur - textStart);
|
||||
int byteEnd = static_cast<int>(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<int>(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<int>& 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<std::size_t>(visibleRaw[vi]), getRawText,
|
||||
wrapWidth, lineHeight, measure);
|
||||
out.segments.push_back(ConsoleSegRef{&w.segs});
|
||||
out.rowStart.push_back(rows);
|
||||
rows += static_cast<int>(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)
|
||||
{
|
||||
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<int>(cur - textStart);
|
||||
int byteEnd = static_cast<int>(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];
|
||||
}
|
||||
|
||||
auto cache = std::make_shared<ConsoleWrapCache>();
|
||||
cache->sync(0, static_cast<std::size_t>(visibleCount), ConsoleWrapKey{});
|
||||
std::vector<int> ids(static_cast<std::size_t>(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;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,11 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -26,12 +30,27 @@ struct ConsoleWrapSegment {
|
||||
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<ConsoleWrapSegment>* 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<std::vector<ConsoleWrapSegment>> segments; // per visible line
|
||||
std::vector<ConsoleSegRef> segments; // per visible line; points into a ConsoleWrapCache (see owner)
|
||||
std::vector<float> heights; // per visible line (includes the inter-line gap)
|
||||
std::vector<float> cumulativeY; // cumulative Y offset of each visible line
|
||||
std::vector<int> rowStart; // V+1 entries: # wrap sub-rows before each visible line (scanline seed)
|
||||
float totalHeight = 0.0f;
|
||||
std::shared_ptr<const void> 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
|
||||
@@ -50,11 +69,75 @@ struct ConsoleTextMeasure {
|
||||
// Accessor returning a visible/raw line's text by index (avoids copying the buffer).
|
||||
using ConsoleLineAccessor = std::function<const std::string&(int)>;
|
||||
|
||||
// Build the wrap layout for `visibleCount` lines (text via getVisibleText(0..N-1)).
|
||||
// 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<ConsoleWrapSegment> 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<ConsoleWrapLine> 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<int>& 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,
|
||||
|
||||
Reference in New Issue
Block a user