Renderers: linear per-row measuring in ImGui; one tab width; Qt drawing fixes

ImGui measured every span, highlight and the cursor from column 0 of the
line (and rescanned the line for each column conversion), which is
quadratic in line length on minified files. Columns now come from one
table per line and pixel offsets from the nearest position already
measured. Output is pixel-identical to before (compared under Xvfb, with
syntax colouring, tabs, UTF-8 and search highlights).

kte::kTabWidth replaces the literal 8s, and Qt's 4: Qt expanded tabs to
4 columns while the command layer scrolled by 8, misplacing the cursor
and horizontal scroll on lines with tabs. MeasureChar and
DisplayColumns (TermWidth.h) are the command layer's column model, now
shared, and Qt draws by it instead of one column per byte, which
shifted everything after a multi-byte character.

Qt also:
- drew operators in the dark-mode background colour, so they vanished;
- computed the editor's column count three ways (different glyph,
  padding and status-row accounting) from painting, and positioned text
  with a rounded cell width, so the cursor drifted off the glyphs on long
  lines. One TextLayout now serves painting, resizing, font changes and
  wheel scrolling, with a fractional cell width; runs are drawn only
  where visible;
- drew each non-ASCII character at its cell, since a fallback-font glyph
  wider than a cell pushed the rest of the run out of place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
This commit is contained in:
Claude
2026-09-24 12:19:59 -07:00
committed by Kyle Isom
parent 29d0ee8244
commit 323e2377fe
6 changed files with 227 additions and 223 deletions
+7 -31
View File
@@ -140,33 +140,6 @@ line_view(const std::string &l)
} }
// Keep buffer viewport offsets so that the cursor stays within the visible
// window based on the editor's current dimensions. The bottom row is reserved
// for the status line.
// Decode the character at line[i]: its byte length and display width in cells
// (tabs expand to the next multiple of tabw from column rx). Measures the way
// TerminalRenderer draws: invalid bytes are one cell each.
static void
measure_char(std::string_view line, std::size_t i, std::size_t rx, std::size_t tabw, std::size_t &len,
std::size_t &width)
{
std::mbstate_t state{};
wchar_t wch = 0;
const auto rc = std::mbrtowc(&wch, line.data() + i, line.size() - i, &state);
if (rc == static_cast<std::size_t>(-1) || rc == static_cast<std::size_t>(-2) || rc == 0) {
// Invalid byte or NUL: the renderer draws the byte value itself.
wch = static_cast<unsigned char>(line[i]);
len = 1;
} else {
len = rc;
}
if (wch == L'\t')
width = tabw - (rx % tabw);
else
width = static_cast<std::size_t>(kte::CellWidth(wch));
}
// Display column of byte offset curx, in terminal cells. // Display column of byte offset curx, in terminal cells.
static std::size_t static std::size_t
compute_render_x(std::string_view line, const std::size_t curx, const std::size_t tabw) compute_render_x(std::string_view line, const std::size_t curx, const std::size_t tabw)
@@ -175,7 +148,7 @@ compute_render_x(std::string_view line, const std::size_t curx, const std::size_
std::size_t i = 0; std::size_t i = 0;
while (i < curx && i < line.size()) { while (i < curx && i < line.size()) {
std::size_t len = 1, width = 1; std::size_t len = 1, width = 1;
measure_char(line, i, rx, tabw, len, width); kte::MeasureChar(line, i, rx, len, width, tabw);
rx += width; rx += width;
i += len; i += len;
} }
@@ -219,6 +192,9 @@ clamp_cursor_to_buffer(Buffer &buf)
} }
// Keep buffer viewport offsets so that the cursor stays within the visible
// window based on the editor's current dimensions. The bottom row is reserved
// for the status line.
static void static void
ensure_cursor_visible(const Editor &ed, Buffer &buf) ensure_cursor_visible(const Editor &ed, Buffer &buf)
{ {
@@ -256,7 +232,7 @@ ensure_cursor_visible(const Editor &ed, Buffer &buf)
if (cury < total_rows) { if (cury < total_rows) {
// GetLineView would materialize the whole buffer after an edit; copying // GetLineView would materialize the whole buffer after an edit; copying
// the one line is cheaper. // the one line is cheaper.
rx = compute_render_x(buf.GetLineString(cury), curx, 8); rx = compute_render_x(buf.GetLineString(cury), curx, kte::kTabWidth);
} }
if (rx < coloffs) { if (rx < coloffs) {
coloffs = rx; coloffs = rx;
@@ -802,7 +778,7 @@ inverse_render_to_source_col(const std::string &line, std::size_t rx_target, std
if (i >= line.size() || rx >= rx_target) if (i >= line.size() || rx >= rx_target)
break; break;
std::size_t len = 1, width = 1; std::size_t len = 1, width = 1;
measure_char(line, i, rx, tabw, len, width); kte::MeasureChar(line, i, rx, len, width, tabw);
rx += width; rx += width;
i += len; i += len;
} }
@@ -854,7 +830,7 @@ cmd_move_cursor_to(CommandContext &ctx)
by = lines2.size() - 1; by = lines2.size() - 1;
std::string line2 = static_cast<std::string>(lines2[by]); std::string line2 = static_cast<std::string>(lines2[by]);
std::size_t rx_target = bco + vx; std::size_t rx_target = bco + vx;
std::size_t sx = inverse_render_to_source_col(line2, rx_target, 8); std::size_t sx = inverse_render_to_source_col(line2, rx_target, kte::kTabWidth);
row = by; row = by;
col = sx; col = sx;
} else { } else {
+1 -1
View File
@@ -281,7 +281,7 @@ SyntaxInk(const TokenKind k)
case TokenKind::Function: case TokenKind::Function:
return dark ? RGBA(0x88C0D0) : RGBA(0x3465A4); return dark ? RGBA(0x88C0D0) : RGBA(0x3465A4);
case TokenKind::Operator: case TokenKind::Operator:
return dark ? RGBA(0x2E3440) : RGBA(0x2E3440); return dark ? RGBA(0xECEFF4) : RGBA(0x2E3440); // as the ImGui palette
case TokenKind::Punctuation: case TokenKind::Punctuation:
return dark ? RGBA(0xECEFF4) : RGBA(0x2E3440); return dark ? RGBA(0xECEFF4) : RGBA(0x2E3440);
case TokenKind::Identifier: case TokenKind::Identifier:
+41 -45
View File
@@ -10,6 +10,7 @@
#include "ImGuiRenderer.h" #include "ImGuiRenderer.h"
#include "SearchHighlight.h" #include "SearchHighlight.h"
#include "TermWidth.h"
#include "Highlight.h" #include "Highlight.h"
#include "GUITheme.h" #include "GUITheme.h"
#include "Buffer.h" #include "Buffer.h"
@@ -220,7 +221,7 @@ ImGuiRenderer::Draw(Editor &ed)
// Expand tabs for the clicked line // Expand tabs for the clicked line
std::string line_clicked = buf->GetLineString(by); std::string line_clicked = buf->GetLineString(by);
const std::size_t tabw = 8; const std::size_t tabw = kte::kTabWidth;
std::string click_expanded; std::string click_expanded;
click_expanded.reserve(line_clicked.size() + 16); click_expanded.reserve(line_clicked.size() + 16);
std::size_t click_rx = 0; std::size_t click_rx = 0;
@@ -315,48 +316,52 @@ ImGuiRenderer::Draw(Editor &ed)
ImVec2 line_pos = ImGui::GetCursorScreenPos(); ImVec2 line_pos = ImGui::GetCursorScreenPos();
std::string line = buf->GetLineString(i); std::string line = buf->GetLineString(i);
// Expand tabs to spaces with width=8 // Expand tabs to spaces with width=8, recording each source
const std::size_t tabw = 8; // byte's rendered column (rx_of[line.size()] is the line's width).
const std::size_t tabw = kte::kTabWidth;
std::string expanded; std::string expanded;
expanded.reserve(line.size() + 16); expanded.reserve(line.size() + 16);
std::size_t rx_abs_draw = 0; std::vector<std::size_t> rx_of(line.size() + 1);
for (std::size_t src = 0; src < line.size(); ++src) { for (std::size_t src = 0; src < line.size(); ++src) {
char c = line[src]; rx_of[src] = expanded.size();
if (c == '\t') { if (line[src] == '\t')
std::size_t adv = (tabw - (rx_abs_draw % tabw)); expanded.append(tabw - (expanded.size() % tabw), ' ');
expanded.append(adv, ' '); else
rx_abs_draw += adv; expanded.push_back(line[src]);
} else {
expanded.push_back(c);
rx_abs_draw += 1;
}
} }
rx_of[line.size()] = expanded.size();
auto src_to_rx = [&](std::size_t src) -> std::size_t {
return rx_of[std::min(src, line.size())];
};
// Helper: convert a rendered column position to an absolute // Rendered column -> pixel offset from the start of the line.
// pixel x offset from the start of the line. ImGui's scroll // Measuring from column 0 for every span was quadratic in line
// handles viewport clipping so we measure from column 0. // length; measure on from the nearest earlier position already
// measured (only character boundaries are kept, so a multi-byte
// character is never measured in pieces).
std::vector<std::pair<std::size_t, float> > px_marks{{0, 0.0f}}; // unrounded
auto rx_to_px = [&](std::size_t rx_col) -> float { auto rx_to_px = [&](std::size_t rx_col) -> float {
std::size_t end = std::min(expanded.size(), rx_col); const std::size_t end = std::min(expanded.size(), rx_col);
if (end == 0) auto it = std::upper_bound(px_marks.begin(), px_marks.end(), end,
return 0.0f; [](std::size_t v, const std::pair<std::size_t, float> &m) {
return ImGui::CalcTextSize(expanded.c_str(), return v < m.first;
});
--it; // px_marks[0] is column 0
float px = it->second;
if (it->first != end) {
px += ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, 0.0f,
expanded.c_str() + it->first,
expanded.c_str() + end).x; expanded.c_str() + end).x;
if (end == expanded.size() || (static_cast<unsigned char>(expanded[end]) & 0xC0) != 0x80)
px_marks.insert(it + 1, {end, px});
}
// Rounded up as ImGui::CalcTextSize does (sums of rounded
// pieces would drift).
return end == 0 ? 0.0f : static_cast<float>(static_cast<int>(px + 0.99999f));
}; };
// Search highlight ranges for this line in source indices // Search highlight ranges for this line in source indices
search_hl.Ranges(line, hl_src_ranges); search_hl.Ranges(line, hl_src_ranges);
auto src_to_rx = [&](std::size_t upto_src_exclusive) -> std::size_t {
std::size_t rx = 0;
std::size_t s = 0;
while (s < upto_src_exclusive && s < line.size()) {
if (line[s] == '\t')
rx += (tabw - (rx % tabw));
else
rx += 1;
++s;
}
return rx;
};
// Draw background highlights (under text) // Draw background highlights (under text)
if (search_mode && !hl_src_ranges.empty()) { if (search_mode && !hl_src_ranges.empty()) {
// Current match emphasis // Current match emphasis
@@ -461,18 +466,9 @@ ImGuiRenderer::Draw(Editor &ed)
return a.s < b.s; return a.s < b.s;
}); });
// Helper to convert a src column to expanded rx position
auto src_to_rx_full = [&](std::size_t sidx) -> std::size_t {
std::size_t rx = 0;
for (std::size_t k = 0; k < sidx && k < line.size(); ++k) {
rx += (line[k] == '\t') ? (tabw - (rx % tabw)) : 1;
}
return rx;
};
for (const auto &sp: spans) { for (const auto &sp: spans) {
std::size_t rx_s = src_to_rx_full(sp.s); std::size_t rx_s = src_to_rx(sp.s);
std::size_t rx_e = src_to_rx_full(sp.e); std::size_t rx_e = src_to_rx(sp.e);
std::size_t draw_start = rx_s; std::size_t draw_start = rx_s;
if (draw_start >= expanded.size()) if (draw_start >= expanded.size())
continue; continue;
@@ -515,7 +511,7 @@ ImGuiRenderer::Draw(Editor &ed)
// measured per frame; if the user scrolls or edits, the cache is // measured per frame; if the user scrolls or edits, the cache is
// refreshed accordingly. // refreshed accordingly.
if (!expanded.empty()) { if (!expanded.empty()) {
float line_w = ImGui::CalcTextSize(expanded.c_str()).x; const float line_w = rx_to_px(expanded.size());
if (line_w > max_width_px_) if (line_w > max_width_px_)
max_width_px_ = line_w; max_width_px_ = line_w;
} }
@@ -575,7 +571,7 @@ ImGuiRenderer::Draw(Editor &ed)
float cursor_px_abs = 0.0f; float cursor_px_abs = 0.0f;
if (cy < nlines) { if (cy < nlines) {
std::string cur_line = buf->GetLineString(cy); std::string cur_line = buf->GetLineString(cy);
const std::size_t tabw = 8; const std::size_t tabw = kte::kTabWidth;
// Expand tabs for cursor line to measure pixel position // Expand tabs for cursor line to measure pixel position
std::string cur_expanded; std::string cur_expanded;
cur_expanded.reserve(cur_line.size() + 16); cur_expanded.reserve(cur_line.size() + 16);
+121 -142
View File
@@ -1,5 +1,6 @@
#include "QtFrontend.h" #include "QtFrontend.h"
#include "SearchHighlight.h" #include "SearchHighlight.h"
#include "TermWidth.h"
#include <QApplication> #include <QApplication>
#include <QWidget> #include <QWidget>
@@ -8,6 +9,7 @@
#include <QScreen> #include <QScreen>
#include <QFont> #include <QFont>
#include <QFontMetrics> #include <QFontMetrics>
#include <QFontMetricsF>
#include <QFontDatabase> #include <QFontDatabase>
#include <QFileDialog> #include <QFileDialog>
#include <QFontDialog> #include <QFontDialog>
@@ -22,6 +24,36 @@
#include "Highlight.h" #include "Highlight.h"
namespace { namespace {
// Text layout, shared by painting and the editor's row/column count: they
// must agree, or the editor scrolls the cursor to a column the window does
// not show.
struct TextLayout {
int line_h;
qreal cell_w; // fractional: rounding it drifts over long lines
int ch_w; // cell_w rounded, for pixel-delta arithmetic
int status_h;
static constexpr int pad_l = 8, pad_t = 6, pad_r = 8, pad_b = 6;
explicit TextLayout(const QFont &f)
: line_h(std::max(1, QFontMetrics(f).height())),
cell_w(std::max<qreal>(1.0, QFontMetricsF(f).horizontalAdvance(QStringLiteral(" ")))),
ch_w(std::max(1, qRound(cell_w))),
status_h(line_h + 6) {}
// Rows (text rows plus the status row, see Editor::ContentRows) and
// columns for a w x h window.
void SetEditorDimensions(Editor &ed, int w, int h) const
{
const int avail_w = std::max(0, w - pad_l - pad_r);
const int avail_h = std::max(0, h - pad_t - pad_b - status_h);
ed.SetDimensions(std::max<std::size_t>(1, avail_h / line_h) + 1,
std::max<std::size_t>(1, static_cast<std::size_t>(avail_w / cell_w)));
}
};
class MainWindow : public QWidget { class MainWindow : public QWidget {
public: public:
explicit MainWindow(class QtInputHandler &ih, QWidget *parent = nullptr) explicit MainWindow(class QtInputHandler &ih, QWidget *parent = nullptr)
@@ -100,15 +132,14 @@ protected:
QFont f(font_family_, font_px_); QFont f(font_family_, font_px_);
p.setFont(f); p.setFont(f);
QFontMetrics fm(f); QFontMetrics fm(f);
const int line_h = fm.height(); const TextLayout layout(f);
const int ch_w = std::max(1, fm.horizontalAdvance(QStringLiteral(" "))); const int line_h = layout.line_h;
const qreal cell_w = layout.cell_w;
// Layout metrics const int pad_l = TextLayout::pad_l;
const int pad_l = 8; const int pad_t = TextLayout::pad_t;
const int pad_t = 6; const int pad_r = TextLayout::pad_r;
const int pad_r = 8; const int pad_b = TextLayout::pad_b;
const int pad_b = 6; const int status_h = layout.status_h;
const int status_h = line_h + 6; // status bar height
// Content area (text viewport) // Content area (text viewport)
const QRect content_rect(pad_l, const QRect content_rect(pad_l,
@@ -134,8 +165,6 @@ protected:
const std::size_t last_row = std::min<std::size_t>( const std::size_t last_row = std::min<std::size_t>(
nrows, rowoffs + std::max(0, max_lines)); nrows, rowoffs + std::max(0, max_lines));
// Tab width: follow ImGuiRenderer default of 4
const std::size_t tabw = 4;
// Prepare painter clip to viewport // Prepare painter clip to viewport
p.save(); p.save();
@@ -145,6 +174,14 @@ protected:
const kte::SearchHighlight search_hl(*ed_); const kte::SearchHighlight search_hl(*ed_);
std::vector<std::pair<std::size_t, std::size_t> > hl_src_ranges; std::vector<std::pair<std::size_t, std::size_t> > hl_src_ranges;
std::vector<std::size_t> col_of; // per line, see DisplayColumns
// Left edge of display column col (fractional pixels).
auto col_x = [&](std::size_t col) -> qreal {
return viewport.x() + (static_cast<qreal>(col) - static_cast<qreal>(coloffs)) * cell_w;
};
// Columns the viewport shows, plus one partly visible.
const std::size_t vis_cols = static_cast<std::size_t>(viewport.width() / cell_w) + 1;
// Iterate visible lines // Iterate visible lines
for (std::size_t i = rowoffs, vis_idx = 0; i < last_row; ++i, ++vis_idx) { for (std::size_t i = rowoffs, vis_idx = 0; i < last_row; ++i, ++vis_idx) {
// Get line as string for regex/iterator usage and general string ops. // Get line as string for regex/iterator usage and general string ops.
@@ -152,13 +189,57 @@ protected:
const int y = viewport.y() + static_cast<int>(vis_idx) * line_h; const int y = viewport.y() + static_cast<int>(vis_idx) * line_h;
const int baseline = y + fm.ascent(); const int baseline = y + fm.ascent();
// Helper: convert src col -> rx with tab expansion // Display column (cells, as the command layer's scrolling
// counts them) of each source byte.
kte::DisplayColumns(line, col_of);
auto src_to_rx_line = [&](std::size_t src_col) -> std::size_t { auto src_to_rx_line = [&](std::size_t src_col) -> std::size_t {
std::size_t rx = 0; return col_of[std::min(src_col, line.size())];
for (std::size_t k = 0; k < src_col && k < line.size(); ++k) { };
rx += (line[k] == '\t') ? (tabw - (rx % tabw)) : 1; // Draw source bytes [s, e) (tabs expanded) at their column;
// the viewport clip hides what lies left of coloffs. ASCII runs
// are drawn whole; other characters one at a time at their
// cell, since a glyph from a fallback font can be wider than a
// cell and would push the rest of the run out of place.
auto draw_text_at = [&](std::size_t col, const char *d, std::size_t n) {
p.drawText(QPointF(col_x(col), baseline), QString::fromUtf8(d, static_cast<int>(n)));
};
auto draw_seg = [&](std::size_t s, std::size_t e, const QColor &color) {
// Only the visible columns: characters left of coloffs or
// right of the viewport are skipped.
s = static_cast<std::size_t>(std::lower_bound(col_of.begin() + s, col_of.begin() + e,
coloffs) - col_of.begin());
e = static_cast<std::size_t>(std::lower_bound(col_of.begin() + s, col_of.begin() + e,
coloffs + vis_cols) - col_of.begin());
if (e <= s)
return;
p.setPen(color);
std::string run; // ASCII, tabs expanded
std::size_t run_col = col_of[s];
for (std::size_t k = s; k < e;) {
const unsigned char c = static_cast<unsigned char>(line[k]);
if (c < 0x80) {
if (run.empty())
run_col = col_of[k];
if (c == '\t')
run.append(col_of[k + 1] - col_of[k], ' ');
else
run.push_back(static_cast<char>(c));
++k;
continue;
} }
return rx; if (!run.empty()) {
draw_text_at(run_col, run.data(), run.size());
run.clear();
}
std::size_t n = 1; // bytes sharing this character's column
while (k + n < e && col_of[k + n] == col_of[k] &&
(static_cast<unsigned char>(line[k + n]) & 0xC0) == 0x80)
++n;
draw_text_at(col_of[k], line.data() + k, n);
k += n;
}
if (!run.empty())
draw_text_at(run_col, run.data(), run.size());
}; };
// Search-match background highlights first (under text) // Search-match background highlights first (under text)
@@ -177,11 +258,8 @@ protected:
std::size_t rx_e = src_to_rx_line(ex); std::size_t rx_e = src_to_rx_line(ex);
if (rx_e <= coloffs) if (rx_e <= coloffs)
continue; // fully left of view continue; // fully left of view
int vx0 = viewport.x() + static_cast<int>(( const int vx0 = qRound(col_x(std::max(rx_s, coloffs)));
(rx_s > coloffs ? rx_s - coloffs : 0) const int vx1 = qRound(col_x(rx_e));
* ch_w));
int vx1 = viewport.x() + static_cast<int>((
(rx_e - coloffs) * ch_w));
QRect r(vx0, y, std::max(0, vx1 - vx0), line_h); QRect r(vx0, y, std::max(0, vx1 - vx0), line_h);
if (r.width() <= 0) if (r.width() <= 0)
continue; continue;
@@ -217,33 +295,17 @@ protected:
std::size_t rx_s = src_to_rx_line(sx); std::size_t rx_s = src_to_rx_line(sx);
std::size_t rx_e = src_to_rx_line(ex); std::size_t rx_e = src_to_rx_line(ex);
if (rx_e > coloffs) { if (rx_e > coloffs) {
int vx0 = viewport.x() + static_cast<int>((rx_s > coloffs const int vx0 = qRound(col_x(std::max(rx_s, coloffs)));
? rx_s - coloffs const int vx1 = qRound(col_x(rx_e));
: 0) * ch_w);
int vx1 = viewport.x() + static_cast<int>(
(rx_e - coloffs) * ch_w);
QRect sel_r(vx0, y, std::max(0, vx1 - vx0), line_h); QRect sel_r(vx0, y, std::max(0, vx1 - vx0), line_h);
if (sel_r.width() > 0) if (sel_r.width() > 0)
p.fillRect(sel_r, sel_bg); p.fillRect(sel_r, sel_bg);
} }
} }
// Build expanded line (tabs -> spaces) for drawing // Syntax-colored spans, the text between them in the default
std::string expanded; // color (drawing only the spans dropped unhighlighted text such
expanded.reserve(line.size() + 8); // as operators).
std::size_t rx_acc = 0;
for (char c: line) {
if (c == '\t') {
std::size_t adv = (tabw - (rx_acc % tabw));
expanded.append(adv, ' ');
rx_acc += adv;
} else {
expanded.push_back(c);
rx_acc += 1;
}
}
// Syntax highlighting spans or plain text
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()-> if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->
HasHighlighter()) { HasHighlighter()) {
kte::LineHighlight lh = buf->Highlighter()->GetLine( kte::LineHighlight lh = buf->Highlighter()->GetLine(
@@ -280,76 +342,24 @@ protected:
int(v.z * 255.0f), int(v.w * 255.0f)); int(v.z * 255.0f), int(v.w * 255.0f));
}; };
// Helper to convert src col to expanded rx std::size_t pos = 0;
auto src_to_rx_full = [&](std::size_t sidx) -> std::size_t {
std::size_t rx = 0;
for (std::size_t k = 0; k < sidx && k < line.size(); ++k) {
rx += (line[k] == '\t') ? (tabw - (rx % tabw)) : 1;
}
return rx;
};
if (spans.empty()) {
// No highlight spans: draw the whole (visible) expanded line in default fg
if (coloffs < expanded.size()) {
const char *start =
expanded.c_str() + static_cast<int>(coloffs);
p.setPen(fg);
p.drawText(viewport.x(), baseline,
QString::fromUtf8(start));
}
} else {
// Draw colored spans
for (const auto &sp: spans) { for (const auto &sp: spans) {
std::size_t rx_s = src_to_rx_full(sp.s); const std::size_t s = std::max(sp.s, pos);
std::size_t rx_e = src_to_rx_full(sp.e); draw_seg(pos, s, fg);
if (rx_e <= coloffs) draw_seg(s, sp.e, colorFor(sp.k));
continue; // left of viewport pos = std::max(pos, sp.e);
std::size_t draw_start = (rx_s > coloffs)
? rx_s
: coloffs;
std::size_t draw_end = std::min<std::size_t>(
rx_e, expanded.size());
if (draw_end <= draw_start)
continue;
std::size_t screen_x = draw_start - coloffs;
int px = viewport.x() + int(screen_x * ch_w);
int len = int(draw_end - draw_start);
p.setPen(colorFor(sp.k));
p.drawText(px, baseline,
QString::fromUtf8(
expanded.c_str() + draw_start, len));
}
} }
draw_seg(pos, line.size(), fg);
} else { } else {
// Draw expanded text clipped by coloffs draw_seg(0, line.size(), fg);
if (static_cast<std::size_t>(coloffs) < expanded.size()) {
const char *start =
expanded.c_str() + static_cast<int>(coloffs);
p.setPen(fg);
p.drawText(viewport.x(), baseline, QString::fromUtf8(start));
}
} }
// Cursor indicator on current line // Cursor indicator on current line
if (i == cy) { if (i == cy) {
std::size_t rx_cur = src_to_rx_line(cx); const std::size_t rx_cur = src_to_rx_line(cx);
if (rx_cur >= coloffs) { if (rx_cur >= coloffs) {
// Compute exact pixel x by measuring expanded substring [coloffs, rx_cur) const int x0 = qRound(col_x(rx_cur));
std::size_t start = std::min<std::size_t>( p.fillRect(QRect(x0, y, qRound(col_x(rx_cur + 1)) - x0, line_h), cur_bg);
coloffs, expanded.size());
std::size_t end = std::min<
std::size_t>(rx_cur, expanded.size());
int px_advance = 0;
if (end > start) {
const QString sub = QString::fromUtf8(
expanded.c_str() + start,
static_cast<int>(end - start));
px_advance = fm.horizontalAdvance(sub);
}
int x0 = viewport.x() + px_advance;
QRect r(x0, y, ch_w, line_h);
p.fillRect(r, cur_bg);
} }
} }
} }
@@ -511,16 +521,7 @@ protected:
return; return;
// Update editor dimensions based on new size // Update editor dimensions based on new size
QFont f(font_family_, font_px_); QFont f(font_family_, font_px_);
QFontMetrics fm(f); TextLayout(f).SetEditorDimensions(*ed_, width(), height());
const int line_h = std::max(12, fm.height());
const int ch_w = std::max(6, fm.horizontalAdvance(QStringLiteral(" ")));
const int pad_l = 8, pad_r = 8, pad_t = 6, pad_b = 6;
const int status_h = line_h + 6;
const int avail_w = std::max(0, width() - pad_l - pad_r);
const int avail_h = std::max(0, height() - pad_t - pad_b - status_h);
std::size_t rows = std::max<std::size_t>(1, (avail_h / line_h));
std::size_t cols = std::max<std::size_t>(1, (avail_w / ch_w));
ed_->SetDimensions(rows, cols);
} }
@@ -537,10 +538,9 @@ protected:
} }
// Recompute metrics to map pixel deltas to rows/cols // Recompute metrics to map pixel deltas to rows/cols
QFont f(font_family_, font_px_); const TextLayout layout(QFont(font_family_, font_px_));
QFontMetrics fm(f); const int line_h = layout.line_h;
const int line_h = std::max(12, fm.height()); const int ch_w = layout.ch_w;
const int ch_w = std::max(6, fm.horizontalAdvance(QStringLiteral(" ")));
// Determine scroll intent: use pixelDelta when available (trackpads), otherwise angleDelta // Determine scroll intent: use pixelDelta when available (trackpads), otherwise angleDelta
QPoint pixel = event->pixelDelta(); QPoint pixel = event->pixelDelta();
@@ -721,18 +721,7 @@ GUIFrontend::Init(int &argc, char **argv, Editor &ed)
// Set initial dimensions based on font metrics // Set initial dimensions based on font metrics
QFont f(family, px_size); QFont f(family, px_size);
QFontMetrics fm(f); TextLayout(f).SetEditorDimensions(ed, window_->width(), window_->height());
const int line_h = std::max(12, fm.height());
const int ch_w = std::max(6, fm.horizontalAdvance(QStringLiteral("M")));
const int w = window_->width();
const int h = window_->height();
const int pad = 16;
const int status_h = line_h + 4;
const int avail_w = std::max(0, w - 2 * pad);
const int avail_h = std::max(0, h - 2 * pad - status_h);
std::size_t rows = std::max<std::size_t>(1, (avail_h / line_h) + 1); // + status
std::size_t cols = std::max<std::size_t>(1, (avail_w / ch_w));
ed.SetDimensions(rows, cols);
return true; return true;
} }
@@ -911,18 +900,8 @@ GUIFrontend::Step(Editor &ed, bool &running)
// Recompute editor dimensions to match new metrics // Recompute editor dimensions to match new metrics
QFont f(target_family, target_px); QFont f(target_family, target_px);
QFontMetrics fm(f); TextLayout(f).SetEditorDimensions(ed, window_ ? window_->width() : 0,
const int line_h = std::max(12, fm.height()); window_ ? window_->height() : 0);
const int ch_w = std::max(6, fm.horizontalAdvance(QStringLiteral("M")));
const int w = window_ ? window_->width() : 0;
const int h = window_ ? window_->height() : 0;
const int pad = 16;
const int status_h = line_h + 4;
const int avail_w = std::max(0, w - 2 * pad);
const int avail_h = std::max(0, h - 2 * pad - status_h);
std::size_t rows = std::max<std::size_t>(1, (avail_h / line_h) + 1); // + status
std::size_t cols = std::max<std::size_t>(1, (avail_w / ch_w));
ed.SetDimensions(rows, cols);
if (window_) if (window_)
window_->update(); window_->update();
+53
View File
@@ -5,9 +5,18 @@
// Tabs are handled by the callers, which know the current column. // Tabs are handled by the callers, which know the current column.
#pragma once #pragma once
#include <cstddef>
#include <cwchar> #include <cwchar>
#include <string_view>
#include <vector>
namespace kte { namespace kte {
// Tab stops every kTabWidth columns, in every frontend and in the command
// layer's horizontal scrolling (they must agree, or the cursor and the
// scroll offset are misplaced on lines with tabs).
constexpr std::size_t kTabWidth = 8;
// C0 controls and DEL are drawn as caret notation ("^M", "^?"): two cells. // C0 controls and DEL are drawn as caret notation ("^M", "^?"): two cells.
// ncurses would otherwise act on them (carriage return, backspace) and // ncurses would otherwise act on them (carriage return, backspace) and
// corrupt the line. // corrupt the line.
@@ -37,4 +46,48 @@ CellWidth(wchar_t wch)
const int w = wcwidth(wch); const int w = wcwidth(wch);
return w < 0 ? 1 : w; return w < 0 ? 1 : w;
} }
// Decode the character at line[i]: its byte length and display width in
// cells (a tab reaches the next multiple of tabw from column col). Measures
// the way TerminalRenderer draws: an invalid byte or NUL is one byte, measured
// as the byte value.
inline void
MeasureChar(std::string_view line, std::size_t i, std::size_t col, std::size_t &len, std::size_t &width,
std::size_t tabw = kTabWidth)
{
std::mbstate_t state{};
wchar_t wch = 0;
const auto rc = std::mbrtowc(&wch, line.data() + i, line.size() - i, &state);
if (rc == static_cast<std::size_t>(-1) || rc == static_cast<std::size_t>(-2) || rc == 0) {
wch = static_cast<unsigned char>(line[i]);
len = 1;
} else {
len = rc;
}
if (wch == L'\t')
width = tabw - (col % tabw);
else
width = static_cast<std::size_t>(CellWidth(wch));
}
// The display column at which each byte of `line` is drawn (bytes inside a
// multi-byte character share its column); out[line.size()] is the line's
// width. Lets a renderer map byte offsets to columns without rescanning.
inline void
DisplayColumns(std::string_view line, std::vector<std::size_t> &out)
{
out.assign(line.size() + 1, 0);
std::size_t col = 0;
for (std::size_t i = 0; i < line.size();) {
std::size_t len = 1, width = 1;
MeasureChar(line, i, col, len, width);
for (std::size_t k = 0; k < len && i + k < line.size(); ++k)
out[i + k] = col;
col += width;
i += len;
}
out[line.size()] = col;
}
} // namespace kte } // namespace kte
+2 -2
View File
@@ -49,7 +49,7 @@ TerminalRenderer::Draw(Editor &ed)
std::size_t rowoffs = buf->Rowoffs(); std::size_t rowoffs = buf->Rowoffs();
std::size_t coloffs = buf->Coloffs(); std::size_t coloffs = buf->Coloffs();
const int tabw = 8; const int tabw = static_cast<int>(kte::kTabWidth);
// Phase 3: prefetch visible viewport highlights (current terminal area) // Phase 3: prefetch visible viewport highlights (current terminal area)
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) { if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
int fr = static_cast<int>(rowoffs); int fr = static_cast<int>(rowoffs);
@@ -147,7 +147,7 @@ TerminalRenderer::Draw(Editor &ed)
wch_len = static_cast<int>(res); wch_len = static_cast<int>(res);
} }
if (wch == L'\t') { if (wch == L'\t') {
constexpr std::size_t tab_width = 8; constexpr std::size_t tab_width = kte::kTabWidth;
const std::size_t next_tab = tab_width - (rc % tab_width); const std::size_t next_tab = tab_width - (rc % tab_width);
rc += next_tab; rc += next_tab;
} else { } else {