diff --git a/Command.cc b/Command.cc index acb2f18..ab8e90f 100644 --- a/Command.cc +++ b/Command.cc @@ -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(-1) || rc == static_cast(-2) || rc == 0) { - // Invalid byte or NUL: the renderer draws the byte value itself. - wch = static_cast(line[i]); - len = 1; - } else { - len = rc; - } - if (wch == L'\t') - width = tabw - (rx % tabw); - else - width = static_cast(kte::CellWidth(wch)); -} - - // Display column of byte offset curx, in terminal cells. static std::size_t 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; while (i < curx && i < line.size()) { 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; 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 ensure_cursor_visible(const Editor &ed, Buffer &buf) { @@ -256,7 +232,7 @@ ensure_cursor_visible(const Editor &ed, Buffer &buf) if (cury < total_rows) { // GetLineView would materialize the whole buffer after an edit; copying // 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) { 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) break; 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; i += len; } @@ -854,7 +830,7 @@ cmd_move_cursor_to(CommandContext &ctx) by = lines2.size() - 1; std::string line2 = static_cast(lines2[by]); 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; col = sx; } else { diff --git a/GUITheme.h b/GUITheme.h index 19ecfb4..77e94e3 100644 --- a/GUITheme.h +++ b/GUITheme.h @@ -281,7 +281,7 @@ SyntaxInk(const TokenKind k) case TokenKind::Function: return dark ? RGBA(0x88C0D0) : RGBA(0x3465A4); case TokenKind::Operator: - return dark ? RGBA(0x2E3440) : RGBA(0x2E3440); + return dark ? RGBA(0xECEFF4) : RGBA(0x2E3440); // as the ImGui palette case TokenKind::Punctuation: return dark ? RGBA(0xECEFF4) : RGBA(0x2E3440); case TokenKind::Identifier: diff --git a/ImGuiRenderer.cc b/ImGuiRenderer.cc index e2b0e46..fd74926 100644 --- a/ImGuiRenderer.cc +++ b/ImGuiRenderer.cc @@ -10,6 +10,7 @@ #include "ImGuiRenderer.h" #include "SearchHighlight.h" +#include "TermWidth.h" #include "Highlight.h" #include "GUITheme.h" #include "Buffer.h" @@ -220,7 +221,7 @@ ImGuiRenderer::Draw(Editor &ed) // Expand tabs for the clicked line std::string line_clicked = buf->GetLineString(by); - const std::size_t tabw = 8; + const std::size_t tabw = kte::kTabWidth; std::string click_expanded; click_expanded.reserve(line_clicked.size() + 16); std::size_t click_rx = 0; @@ -315,48 +316,52 @@ ImGuiRenderer::Draw(Editor &ed) ImVec2 line_pos = ImGui::GetCursorScreenPos(); std::string line = buf->GetLineString(i); - // Expand tabs to spaces with width=8 - const std::size_t tabw = 8; + // Expand tabs to spaces with width=8, recording each source + // byte's rendered column (rx_of[line.size()] is the line's width). + const std::size_t tabw = kte::kTabWidth; std::string expanded; expanded.reserve(line.size() + 16); - std::size_t rx_abs_draw = 0; + std::vector rx_of(line.size() + 1); for (std::size_t src = 0; src < line.size(); ++src) { - char c = line[src]; - if (c == '\t') { - std::size_t adv = (tabw - (rx_abs_draw % tabw)); - expanded.append(adv, ' '); - rx_abs_draw += adv; - } else { - expanded.push_back(c); - rx_abs_draw += 1; - } + rx_of[src] = expanded.size(); + if (line[src] == '\t') + expanded.append(tabw - (expanded.size() % tabw), ' '); + else + expanded.push_back(line[src]); } + 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 - // pixel x offset from the start of the line. ImGui's scroll - // handles viewport clipping so we measure from column 0. + // Rendered column -> pixel offset from the start of the line. + // Measuring from column 0 for every span was quadratic in line + // 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 > px_marks{{0, 0.0f}}; // unrounded auto rx_to_px = [&](std::size_t rx_col) -> float { - std::size_t end = std::min(expanded.size(), rx_col); - if (end == 0) - return 0.0f; - return ImGui::CalcTextSize(expanded.c_str(), - expanded.c_str() + end).x; + const std::size_t end = std::min(expanded.size(), rx_col); + auto it = std::upper_bound(px_marks.begin(), px_marks.end(), end, + [](std::size_t v, const std::pair &m) { + 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; + if (end == expanded.size() || (static_cast(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(static_cast(px + 0.99999f)); }; // Search highlight ranges for this line in source indices 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) if (search_mode && !hl_src_ranges.empty()) { // Current match emphasis @@ -461,18 +466,9 @@ ImGuiRenderer::Draw(Editor &ed) 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) { - std::size_t rx_s = src_to_rx_full(sp.s); - std::size_t rx_e = src_to_rx_full(sp.e); + std::size_t rx_s = src_to_rx(sp.s); + std::size_t rx_e = src_to_rx(sp.e); std::size_t draw_start = rx_s; if (draw_start >= expanded.size()) continue; @@ -515,7 +511,7 @@ ImGuiRenderer::Draw(Editor &ed) // measured per frame; if the user scrolls or edits, the cache is // refreshed accordingly. 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_) max_width_px_ = line_w; } @@ -575,7 +571,7 @@ ImGuiRenderer::Draw(Editor &ed) float cursor_px_abs = 0.0f; if (cy < nlines) { 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 std::string cur_expanded; cur_expanded.reserve(cur_line.size() + 16); diff --git a/QtFrontend.cc b/QtFrontend.cc index 968c852..16e3870 100644 --- a/QtFrontend.cc +++ b/QtFrontend.cc @@ -1,5 +1,6 @@ #include "QtFrontend.h" #include "SearchHighlight.h" +#include "TermWidth.h" #include #include @@ -8,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -22,6 +24,36 @@ #include "Highlight.h" 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(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(1, avail_h / line_h) + 1, + std::max(1, static_cast(avail_w / cell_w))); + } +}; + + class MainWindow : public QWidget { public: explicit MainWindow(class QtInputHandler &ih, QWidget *parent = nullptr) @@ -100,15 +132,14 @@ protected: QFont f(font_family_, font_px_); p.setFont(f); QFontMetrics fm(f); - const int line_h = fm.height(); - const int ch_w = std::max(1, fm.horizontalAdvance(QStringLiteral(" "))); - - // Layout metrics - const int pad_l = 8; - const int pad_t = 6; - const int pad_r = 8; - const int pad_b = 6; - const int status_h = line_h + 6; // status bar height + const TextLayout layout(f); + const int line_h = layout.line_h; + const qreal cell_w = layout.cell_w; + const int pad_l = TextLayout::pad_l; + const int pad_t = TextLayout::pad_t; + const int pad_r = TextLayout::pad_r; + const int pad_b = TextLayout::pad_b; + const int status_h = layout.status_h; // Content area (text viewport) const QRect content_rect(pad_l, @@ -134,8 +165,6 @@ protected: const std::size_t last_row = std::min( 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 p.save(); @@ -145,6 +174,14 @@ protected: const kte::SearchHighlight search_hl(*ed_); std::vector > hl_src_ranges; + std::vector 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(col) - static_cast(coloffs)) * cell_w; + }; + // Columns the viewport shows, plus one partly visible. + const std::size_t vis_cols = static_cast(viewport.width() / cell_w) + 1; + // Iterate visible lines 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. @@ -152,13 +189,57 @@ protected: const int y = viewport.y() + static_cast(vis_idx) * line_h; 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 { - std::size_t rx = 0; - for (std::size_t k = 0; k < src_col && k < line.size(); ++k) { - rx += (line[k] == '\t') ? (tabw - (rx % tabw)) : 1; + return col_of[std::min(src_col, line.size())]; + }; + // 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(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::lower_bound(col_of.begin() + s, col_of.begin() + e, + coloffs) - col_of.begin()); + e = static_cast(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(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(c)); + ++k; + continue; + } + 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(line[k + n]) & 0xC0) == 0x80) + ++n; + draw_text_at(col_of[k], line.data() + k, n); + k += n; } - return rx; + if (!run.empty()) + draw_text_at(run_col, run.data(), run.size()); }; // Search-match background highlights first (under text) @@ -177,11 +258,8 @@ protected: std::size_t rx_e = src_to_rx_line(ex); if (rx_e <= coloffs) continue; // fully left of view - int vx0 = viewport.x() + static_cast(( - (rx_s > coloffs ? rx_s - coloffs : 0) - * ch_w)); - int vx1 = viewport.x() + static_cast(( - (rx_e - coloffs) * ch_w)); + const int vx0 = qRound(col_x(std::max(rx_s, coloffs))); + const int vx1 = qRound(col_x(rx_e)); QRect r(vx0, y, std::max(0, vx1 - vx0), line_h); if (r.width() <= 0) continue; @@ -217,33 +295,17 @@ protected: std::size_t rx_s = src_to_rx_line(sx); std::size_t rx_e = src_to_rx_line(ex); if (rx_e > coloffs) { - int vx0 = viewport.x() + static_cast((rx_s > coloffs - ? rx_s - coloffs - : 0) * ch_w); - int vx1 = viewport.x() + static_cast( - (rx_e - coloffs) * ch_w); + const int vx0 = qRound(col_x(std::max(rx_s, coloffs))); + const int vx1 = qRound(col_x(rx_e)); QRect sel_r(vx0, y, std::max(0, vx1 - vx0), line_h); if (sel_r.width() > 0) p.fillRect(sel_r, sel_bg); } } - // Build expanded line (tabs -> spaces) for drawing - std::string expanded; - expanded.reserve(line.size() + 8); - 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 + // Syntax-colored spans, the text between them in the default + // color (drawing only the spans dropped unhighlighted text such + // as operators). if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()-> HasHighlighter()) { kte::LineHighlight lh = buf->Highlighter()->GetLine( @@ -280,76 +342,24 @@ protected: int(v.z * 255.0f), int(v.w * 255.0f)); }; - // Helper to convert src col to expanded rx - 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(coloffs); - p.setPen(fg); - p.drawText(viewport.x(), baseline, - QString::fromUtf8(start)); - } - } else { - // Draw colored spans - for (const auto &sp: spans) { - std::size_t rx_s = src_to_rx_full(sp.s); - std::size_t rx_e = src_to_rx_full(sp.e); - if (rx_e <= coloffs) - continue; // left of viewport - std::size_t draw_start = (rx_s > coloffs) - ? rx_s - : coloffs; - std::size_t draw_end = std::min( - 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)); - } + std::size_t pos = 0; + for (const auto &sp: spans) { + const std::size_t s = std::max(sp.s, pos); + draw_seg(pos, s, fg); + draw_seg(s, sp.e, colorFor(sp.k)); + pos = std::max(pos, sp.e); } + draw_seg(pos, line.size(), fg); } else { - // Draw expanded text clipped by coloffs - if (static_cast(coloffs) < expanded.size()) { - const char *start = - expanded.c_str() + static_cast(coloffs); - p.setPen(fg); - p.drawText(viewport.x(), baseline, QString::fromUtf8(start)); - } + draw_seg(0, line.size(), fg); } // Cursor indicator on current line 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) { - // Compute exact pixel x by measuring expanded substring [coloffs, rx_cur) - std::size_t start = std::min( - 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(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); + const int x0 = qRound(col_x(rx_cur)); + p.fillRect(QRect(x0, y, qRound(col_x(rx_cur + 1)) - x0, line_h), cur_bg); } } } @@ -511,16 +521,7 @@ protected: return; // Update editor dimensions based on new size QFont f(font_family_, font_px_); - QFontMetrics fm(f); - 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(1, (avail_h / line_h)); - std::size_t cols = std::max(1, (avail_w / ch_w)); - ed_->SetDimensions(rows, cols); + TextLayout(f).SetEditorDimensions(*ed_, width(), height()); } @@ -537,10 +538,9 @@ protected: } // Recompute metrics to map pixel deltas to rows/cols - QFont f(font_family_, font_px_); - QFontMetrics fm(f); - const int line_h = std::max(12, fm.height()); - const int ch_w = std::max(6, fm.horizontalAdvance(QStringLiteral(" "))); + const TextLayout layout(QFont(font_family_, font_px_)); + const int line_h = layout.line_h; + const int ch_w = layout.ch_w; // Determine scroll intent: use pixelDelta when available (trackpads), otherwise angleDelta QPoint pixel = event->pixelDelta(); @@ -721,18 +721,7 @@ GUIFrontend::Init(int &argc, char **argv, Editor &ed) // Set initial dimensions based on font metrics QFont f(family, px_size); - QFontMetrics fm(f); - 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(1, (avail_h / line_h) + 1); // + status - std::size_t cols = std::max(1, (avail_w / ch_w)); - ed.SetDimensions(rows, cols); + TextLayout(f).SetEditorDimensions(ed, window_->width(), window_->height()); return true; } @@ -911,18 +900,8 @@ GUIFrontend::Step(Editor &ed, bool &running) // Recompute editor dimensions to match new metrics QFont f(target_family, target_px); - QFontMetrics fm(f); - const int line_h = std::max(12, fm.height()); - 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(1, (avail_h / line_h) + 1); // + status - std::size_t cols = std::max(1, (avail_w / ch_w)); - ed.SetDimensions(rows, cols); + TextLayout(f).SetEditorDimensions(ed, window_ ? window_->width() : 0, + window_ ? window_->height() : 0); if (window_) window_->update(); diff --git a/TermWidth.h b/TermWidth.h index 674e354..05c8a1a 100644 --- a/TermWidth.h +++ b/TermWidth.h @@ -5,9 +5,18 @@ // Tabs are handled by the callers, which know the current column. #pragma once +#include #include +#include +#include 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. // ncurses would otherwise act on them (carriage return, backspace) and // corrupt the line. @@ -37,4 +46,48 @@ CellWidth(wchar_t wch) const int w = wcwidth(wch); 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(-1) || rc == static_cast(-2) || rc == 0) { + wch = static_cast(line[i]); + len = 1; + } else { + len = rc; + } + if (wch == L'\t') + width = tabw - (col % tabw); + else + width = static_cast(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 &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 diff --git a/TerminalRenderer.cc b/TerminalRenderer.cc index 971254d..7fae63b 100644 --- a/TerminalRenderer.cc +++ b/TerminalRenderer.cc @@ -49,7 +49,7 @@ TerminalRenderer::Draw(Editor &ed) std::size_t rowoffs = buf->Rowoffs(); std::size_t coloffs = buf->Coloffs(); - const int tabw = 8; + const int tabw = static_cast(kte::kTabWidth); // Phase 3: prefetch visible viewport highlights (current terminal area) if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) { int fr = static_cast(rowoffs); @@ -147,7 +147,7 @@ TerminalRenderer::Draw(Editor &ed) wch_len = static_cast(res); } 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); rc += next_tab; } else {