Skip to content

Commit 4c191c3

Browse files
committed
Fix wide-char pixel overflow compensation for emoji
The old guard condition (string-width > length) failed for multi-codepoint emoji like variation selectors and ZWJ sequences, where string-width can equal the character count. This meant compensation never ran for those lines. Rewrite ghostel--compensate-wide-chars to: - Remove the broken string-width heuristic and check pixel overflow directly - Use window-text-pixel-size for accurate measurement in the actual window - Strip stale display properties before measuring to avoid false negatives - Replace all trailing spaces with a single pixel-accurate stretch glyph - Only shrink trailing spaces, never widen (prevents false-positive truncation) - Run after every redraw from Elisp, outside inhibit-redisplay Use explicit window and Zig-side flag for wide-char compensation Address review feedback on the wide-char compensation: - Use get-buffer-window for window-text-pixel-size and window-body-width so measurements are correct when the ghostel buffer is not in the selected window - Replace the redundant Zig-side ghostel--compensate-wide-chars call (which ran under inhibit-redisplay) with a ghostel--has-wide-chars flag that the Elisp side checks after the redraw returns - Only run compensation when the flag is set, skipping the overhead for ASCII-only output
1 parent 63f5550 commit 4c191c3

3 files changed

Lines changed: 45 additions & 36 deletions

File tree

ghostel.el

Lines changed: 43 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,11 @@ DIR is the module directory."
608608
(defvar-local ghostel--force-next-redraw nil
609609
"When non-nil, redraw regardless of synchronized output mode.")
610610

611+
(defvar-local ghostel--has-wide-chars nil
612+
"Set by the native renderer when wide characters are present.
613+
Cleared before each redraw; checked afterwards to decide whether
614+
pixel-based trailing-space compensation is needed.")
615+
611616
(defvar-local ghostel--resize-timer nil
612617
"Timer for debounced SIGWINCH on alt screen.")
613618

@@ -1548,41 +1553,39 @@ Skips regions that already have a `help-echo' property (e.g. from OSC 8)."
15481553

15491554

15501555
(defun ghostel--compensate-wide-chars ()
1551-
"Hide trailing spaces on lines where wide-char glyphs cause pixel overflow.
1556+
"Shrink trailing spaces on lines where wide-char glyphs cause pixel overflow.
15521557
Emoji glyphs often render wider than `char-width' times `frame-char-width'
15531558
pixels, making the display engine treat the line as wider than the window
15541559
even though `string-width' equals the terminal column count. For each
1555-
overflowing line, hide the minimal trailing spaces via `display' properties.
1556-
Only called by the native renderer when wide characters are present."
1557-
(when (and (display-graphic-p)
1558-
(fboundp 'string-pixel-width))
1559-
(let ((char-w (frame-char-width))
1560-
(win-w (window-body-width nil t)))
1561-
(save-excursion
1562-
(goto-char (point-min))
1563-
(while (not (eobp))
1564-
(let* ((bol (line-beginning-position))
1565-
(eol (line-end-position))
1566-
(len (- eol bol)))
1567-
;; Only measure pixel width when the line has wide characters.
1568-
;; string-width > length means at least one char has char-width > 1.
1569-
(when (and (> len 0)
1570-
(> (string-width (buffer-substring bol eol)) len))
1571-
(let* ((line (buffer-substring bol eol))
1572-
(pw (string-pixel-width line))
1573-
(overshoot (- pw win-w)))
1574-
(when (> overshoot 0)
1575-
(let* ((spaces-start (save-excursion
1576-
(goto-char eol)
1577-
(skip-chars-backward " " bol)
1578-
(point)))
1579-
(avail (- eol spaces-start))
1580-
(hide (min (ceiling (/ (float overshoot) char-w))
1581-
avail)))
1582-
(when (> hide 0)
1583-
(put-text-property (- eol hide) eol
1584-
'display "")))))))
1585-
(forward-line 1))))))
1560+
overflowing line we replace the trailing whitespace with a single stretch
1561+
glyph of exactly the remaining pixel width."
1562+
(let ((win (get-buffer-window)))
1563+
(when (and win (display-graphic-p))
1564+
(let ((win-w (window-body-width win t))
1565+
(inhibit-read-only t))
1566+
(save-excursion
1567+
(goto-char (point-min))
1568+
(while (not (eobp))
1569+
(let* ((bol (line-beginning-position))
1570+
(eol (line-end-position))
1571+
(spaces-start (save-excursion
1572+
(goto-char eol)
1573+
(skip-chars-backward " " bol)
1574+
(point)))
1575+
(avail (- eol spaces-start)))
1576+
(when (> avail 0)
1577+
;; Strip stale compensation so pixel measurement is accurate.
1578+
(remove-text-properties spaces-start eol '(display nil))
1579+
(let* ((content-pw (car (window-text-pixel-size win bol spaces-start)))
1580+
(remaining (max 0 (- win-w content-pw)))
1581+
(natural-pw (* avail (frame-char-width (window-frame win)))))
1582+
;; Only compensate when we would shrink the trailing spaces;
1583+
;; never widen them as that could introduce truncation on
1584+
;; lines that fit naturally.
1585+
(when (< remaining natural-pw)
1586+
(put-text-property spaces-start eol 'display
1587+
`(space :width (,remaining)))))))
1588+
(forward-line 1)))))))
15861589

15871590

15881591
;;; Prompt navigation (OSC 133)
@@ -2250,17 +2253,23 @@ frame after idle to improve interactive responsiveness."
22502253
(unless (and (not ghostel--force-next-redraw)
22512254
(ghostel--mode-enabled ghostel--term 2026))
22522255
(setq ghostel--force-next-redraw nil)
2256+
(setq ghostel--has-wide-chars nil)
22532257
(let ((inhibit-read-only t)
22542258
(inhibit-redisplay t)
22552259
(inhibit-modification-hooks t))
2256-
(ghostel--redraw ghostel--term ghostel-full-redraw)))))))
2260+
(ghostel--redraw ghostel--term ghostel-full-redraw))
2261+
(when ghostel--has-wide-chars
2262+
(ghostel--compensate-wide-chars)))))))
22572263

22582264
(defun ghostel-force-redraw ()
22592265
"Force a full terminal redraw (for debugging)."
22602266
(interactive)
22612267
(when ghostel--term
2268+
(setq ghostel--has-wide-chars nil)
22622269
(let ((inhibit-read-only t))
2263-
(ghostel--redraw ghostel--term ghostel-full-redraw))))
2270+
(ghostel--redraw ghostel--term ghostel-full-redraw))
2271+
(when ghostel--has-wide-chars
2272+
(ghostel--compensate-wide-chars))))
22642273

22652274

22662275
;;; Window resize

src/emacs.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ pub const Sym = struct {
308308
@"ghostel-link-map": Value,
309309
@"ghostel--set-buffer-face": Value,
310310
@"ghostel--detect-urls": Value,
311-
@"ghostel--compensate-wide-chars": Value,
311+
@"ghostel--has-wide-chars": Value,
312312
@"ghostel--set-cursor-style": Value,
313313
@"ghostel--update-directory": Value,
314314
@"ghostel--osc51-eval": Value,

src/render.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -856,7 +856,7 @@ pub fn redraw(env: emacs.Env, term: *Terminal, force_full: bool) void {
856856
if (dirty != gt.DIRTY_FALSE) {
857857
_ = env.call0(emacs.sym.@"ghostel--detect-urls");
858858
if (has_wide_chars) {
859-
_ = env.call0(emacs.sym.@"ghostel--compensate-wide-chars");
859+
_ = env.call2(env.intern("set"), emacs.sym.@"ghostel--has-wide-chars", env.t());
860860
}
861861
}
862862

0 commit comments

Comments
 (0)