While the node-status banner shows, its detail line was fitted every frame by popping one character at a time and re-measuring the whole "· " + detail + "…" string: O(L^2) per frame in the detail length L, which adds up on the ~1.5 KB Windows crash report. The pop also left a dangling UTF-8 lead byte before the "…", so a cut in non-ASCII text drew replacement characters in place of the ellipsis. - util::textfit::ellipsizeTail builds the same string in O(L + m log m), m being the length of the line the cut lands in: ImGui measures multi-line text as its widest line, so the old loop could only stop inside the first line that overflows on its own. Byte-identical for ASCII; the cut now lands on a code-point boundary. - EllipsisFitCache keeps the result until the detail, budget, font size, font (by FontId) or rasterizer density changes, so an unchanged frame costs one string compare. - When ImGui would draw a line wider than the per-line split predicts (a malformed byte joined with the following newline, as in CP-1252 output from the Windows daemon), the old whole-string loop is used for that text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
154 lines
7.0 KiB
C++
154 lines
7.0 KiB
C++
// DragonX Wallet - ImGui Edition
|
|
// Copyright 2024-2026 The Hush Developers
|
|
// Released under the GPLv3
|
|
//
|
|
// Pure (no ImGui) helpers for display-only ellipsis truncation. Callers inject the width function, so unit
|
|
// tests need no font. Every cut lands on a UTF-8 code-point boundary (never inside a multi-byte sequence).
|
|
#pragma once
|
|
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <string>
|
|
|
|
namespace dragonx {
|
|
namespace util {
|
|
namespace textfit {
|
|
|
|
inline bool isCont(char c) { return (static_cast<unsigned char>(c) & 0xC0) == 0x80; }
|
|
|
|
// Largest code-point boundary <= i (i clamped to n; n is a boundary).
|
|
inline std::size_t snapDown(const char* s, std::size_t n, std::size_t i) {
|
|
if (i >= n) return n;
|
|
while (i > 0 && isCont(s[i])) --i;
|
|
return i;
|
|
}
|
|
|
|
// Smallest code-point boundary >= i (clamped to n).
|
|
inline std::size_t snapUp(const char* s, std::size_t n, std::size_t i) {
|
|
if (i > n) i = n;
|
|
while (i < n && isCont(s[i])) ++i;
|
|
return i;
|
|
}
|
|
|
|
// Largest code-point boundary k in [0, n] with fits(k), by bisection: ceil(log2(n + 1)) fits() calls.
|
|
// Exact when fits is monotone (true up to some length, false after), which holds for single-line text in any
|
|
// font with non-negative advances. Whatever fits does, the result r satisfies fits(r) or r == 0.
|
|
template <class Fits>
|
|
std::size_t largestFittingPrefix(const char* s, std::size_t n, Fits&& fits) {
|
|
std::size_t lo = 0, hi = n;
|
|
while (lo < hi) {
|
|
const std::size_t mid = lo + (hi - lo + 1) / 2;
|
|
if (fits(snapDown(s, n, mid))) lo = mid; else hi = mid - 1;
|
|
}
|
|
return snapDown(s, n, lo);
|
|
}
|
|
|
|
// Middle ellipsis "front...back" so measure(result) <= maxWidth; measure(b, e) = pixel width of [b, e).
|
|
// Same contract as the old ui::material::TruncateToWidth scan (largest front length f in [3, n/2] that fits,
|
|
// back = max(f - 2, 3); n < 6, or nothing fitting at n == 6 -> text; nothing fitting at n > 6 -> the f = 3
|
|
// candidate) and byte-identical to it for ASCII. Cuts snap to code points (front end down, back start up),
|
|
// so valid UTF-8 in -> valid UTF-8 out. Single-line text: O(log n) measures into one reused buffer.
|
|
template <class Measure>
|
|
std::string middleEllipsis(const std::string& text, float maxWidth, Measure&& measure) {
|
|
const char* s = text.data();
|
|
const std::size_t n = text.size();
|
|
if (n == 0 || maxWidth <= 0.0f) return text;
|
|
if (measure(s, s + n) <= maxWidth) return text;
|
|
if (n < 6) return text; // old scan (f = n/2 .. 3) never ran, no fallback
|
|
std::string cand;
|
|
cand.reserve(n + 3);
|
|
auto build = [&](std::size_t f) {
|
|
const std::size_t b = (f >= 5) ? f - 2 : 3; // == old (f - 2 > 3) ? f - 2 : 3
|
|
const std::size_t pe = snapDown(s, n, f);
|
|
const std::size_t ss = snapUp(s, n, n - b);
|
|
cand.assign(s, pe);
|
|
cand += "...";
|
|
cand.append(s + ss, n - ss);
|
|
};
|
|
auto fits = [&](std::size_t f) {
|
|
build(f);
|
|
return measure(cand.data(), cand.data() + cand.size()) <= maxWidth;
|
|
};
|
|
std::size_t best = 3; // the old n > 6 fallback is exactly the f = 3 candidate
|
|
if (std::memchr(s, '\n', n) == nullptr) {
|
|
// Single line: width is non-decreasing in f, so the largest fit (== the old first fit scanning down
|
|
// from n/2) is found by bisection.
|
|
std::size_t lo = 3, hi = n / 2;
|
|
while (lo < hi) {
|
|
const std::size_t mid = lo + (hi - lo + 1) / 2;
|
|
if (fits(mid)) lo = mid; else hi = mid - 1;
|
|
}
|
|
best = lo;
|
|
} else {
|
|
// Multi-line: the measured width is the WIDEST line, which is not monotone in f -> keep the old scan.
|
|
for (std::size_t f = n / 2; f > 3; --f) {
|
|
if (fits(f)) { best = f; break; }
|
|
}
|
|
}
|
|
if (n == 6 && !fits(3)) return text; // old: n == 6 returned f = 3 only if it fit
|
|
build(best);
|
|
return cand;
|
|
}
|
|
|
|
// The node-status banner fit "prefix + text + ell". Byte-identical for ASCII to the old loop that popped one
|
|
// character at a time off `text` until it fit: ImGui measures multi-line text as its WIDEST line, so that loop
|
|
// could only stop inside the first line that overflows on its own. O(L + m log m) instead of O(L^2), and the
|
|
// cut is always on a code-point boundary (the old pop left a dangling UTF-8 lead byte before the ellipsis).
|
|
// Returns prefix + text when every line fits, else prefix + text[0, k) + ell for the largest fitting
|
|
// code-point boundary k (k = 0 when nothing fits). lineWidth(b, e) = width of the single line [b, e) exactly
|
|
// as the renderer measures it (e.g. ImGui::CalcTextSize(b, e).x, rounding included).
|
|
template <class LineWidth>
|
|
std::string ellipsizeTail(const std::string& prefix, const std::string& text, const std::string& ell,
|
|
float budget, LineWidth&& lineWidth) {
|
|
const char* s = text.data();
|
|
const std::size_t n = text.size();
|
|
std::string buf;
|
|
auto width = [&](bool withPrefix, std::size_t b, std::size_t e, bool withEll) {
|
|
buf.clear();
|
|
if (withPrefix) buf += prefix;
|
|
buf.append(s + b, e - b);
|
|
if (withEll) buf += ell;
|
|
return lineWidth(buf.data(), buf.data() + buf.size());
|
|
};
|
|
std::size_t start = 0;
|
|
for (bool first = true;; first = false) {
|
|
const void* nl = std::memchr(s + start, '\n', n - start);
|
|
const std::size_t end = nl ? static_cast<std::size_t>(static_cast<const char*>(nl) - s) : n;
|
|
if (width(first, start, end, false) > budget) {
|
|
// Every earlier line fits; any cut past `end` keeps this whole line, so the cut is inside it. A cut
|
|
// at this line's start leaves "" + ell on it, so if ell alone overflows nothing fits at all.
|
|
if (!first && width(false, start, start, true) > budget) return prefix + ell;
|
|
const std::size_t len = largestFittingPrefix(s + start, end - start, [&](std::size_t l) {
|
|
return width(first, start, start + l, true) <= budget;
|
|
});
|
|
return prefix + text.substr(0, start + len) + ell;
|
|
}
|
|
if (!nl) return prefix + text;
|
|
start = end + 1;
|
|
}
|
|
}
|
|
|
|
// One-slot memo: "fit this text to this budget in this font". compute(text) -> std::string runs only when the
|
|
// text, budget, font (ImFont::FontId: new on every atlas rebuild, even at the same size/address), font size or
|
|
// rasterizer density changes.
|
|
struct EllipsisFitCache {
|
|
std::string src, shown;
|
|
float budget = 0.0f, fontSize = 0.0f, density = 0.0f;
|
|
std::uint32_t fontId = 0;
|
|
bool valid = false;
|
|
int computes = 0; // test hook
|
|
template <class Fn>
|
|
const std::string& get(const std::string& text, float b, std::uint32_t fid, float fs, float dens, Fn&& compute) {
|
|
if (!valid || b != budget || fid != fontId || fs != fontSize || dens != density || text != src) {
|
|
shown = compute(text);
|
|
src = text; budget = b; fontId = fid; fontSize = fs; density = dens; valid = true; ++computes;
|
|
}
|
|
return shown;
|
|
}
|
|
};
|
|
|
|
} // namespace textfit
|
|
} // namespace util
|
|
} // namespace dragonx
|