ekp/tests/ekp-gui-verify.el
Kinneyzhang 29cef97ef8 fix: justified lines fit the real display, not an idealized one
Two root causes made justified text overrun the window's right edge
in real user sessions (every line ending in the "$" truncation
marker), while emacs -Q looked fine:

1. Windows WITHOUT fringes draw the truncation/continuation
   indicator in the text area's LAST COLUMN, so the usable width is
   one character less than window-body-width.  ekp reserved only a
   2px margin: with fringes disabled (a common minimal setup, and
   all ttys) every line that hit the target width exactly had its
   final glyph displaced by the "$".  ekp-region--window-pixel now
   reserves one frame-char-width when the window has no right
   fringe.  (The same lesson ebox-playground encodes in its
   viewport-width reserve.)

2. Measurement was blind to the buffer's display context:
   string-pixel-width works in a bare hidden buffer, ignoring
   face-remapping-alist — which is where text-scale-mode, themes
   and per-buffer font tweaks live.  Under a remap, the DP laid
   lines out with one font's metrics and the display rendered them
   with another's: scale +3 made 5 of 7 sample lines overflow a
   1330px window by up to 900px (GUI-measured).  Measurement now
   runs with the destination buffer's face-remapping-alist (the
   29/30-compatible equivalent of Emacs 31's string-pixel-width
   BUFFER argument), and the width/paragraph caches key on that
   context so buffers at different scales never alias.  text-scale
   changes also trigger a re-flow in ekp-auto-justify-mode.

Ground truth, measured with window-text-pixel-size in GUI Emacs
across 7 display contexts (plain / text-scale ±| face remap /
no-fringes / no-fringes+scale / narrow+scale): the widest justified
line equals the target width exactly in every case, zero lines
overflow.  Verbatim code blocks are exempt by design (they never
reflow, like any code line in a narrow window).

New tooling so this never regresses invisibly:
- M-x ekp-diagnose: renders a probe line in YOUR buffer and reports
  target vs rendered width — run it in any session where justified
  text looks wrong.
- tests/ekp-gui-verify.el: M-x ekp-gui-verify (single check in a
  customized session) and ekp-gui-verify-matrix (the 7-case table,
  for emacs -Q).
- 2 new batch ERT tests pin the context-keyed caches (94 total).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 03:19:27 +08:00

196 lines
7.9 KiB
EmacsLisp

;;; ekp-gui-verify.el --- GUI pixel-fit verification for ekp -*- lexical-binding: t; -*-
;; Copyright (C) 2024-2026 Kinney Zhang
;; Author: Kinney Zhang <kinneyzhang666@gmail.com>
;; This file is NOT part of GNU Emacs.
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; Ground-truth verification that justified text really fits the
;; window, measured with `window-text-pixel-size' on the live display
;; — the one thing batch tests cannot check.
;;
;; Two ways to run:
;;
;; M-x ekp-gui-verify in ANY running GUI Emacs — including your
;; fully customized session. Use this when
;; justified text looks truncated: it tells
;; you whether measurement matches rendering
;; under your fonts/remappings.
;;
;; emacs -Q -L . -L tests -l tests/ekp-gui-verify.el \
;; -f ekp-gui-verify-matrix
;; runs the full matrix (plain, text-scale
;; up/down, face remap, narrow+scale) and
;; prints a PASS/FAIL table.
;;
;; Criterion: every justified line's rendered width equals the target
;; width (± `ekp-region-margin-pixel'). Verbatim paragraphs
;; (`ekp-verbatim') are exempt — code blocks pass through unwrapped by
;; design and may exceed a narrow window, like any code line.
;;; Code:
(require 'ekp)
(require 'ekp-region)
(require 'ekp-showcase)
(defun ekp-gui-verify--scan (buffer)
"Measure every line of BUFFER in its window; return a result plist."
(with-current-buffer buffer
(let* ((win (get-buffer-window buffer))
(body (window-body-width win t))
(target ekp-region--auto-width)
(worst 0) (over 0) (n 0) (exempt 0))
(save-excursion
(goto-char (point-min))
(while (not (eobp))
(let* ((bol (line-beginning-position))
(eol (line-end-position))
(px (if (= bol eol) 0
(car (window-text-pixel-size win bol eol t)))))
(when (> px 0)
(if (text-property-not-all bol eol 'ekp-verbatim nil)
(setq exempt (1+ exempt))
(setq n (1+ n))
(when (> px worst) (setq worst px))
(when (> px body) (setq over (1+ over))))))
(forward-line 1)))
(list :body body :target target :widest worst :over over
:lines n :exempt exempt
:pass (and (= over 0)
(<= (abs (- worst target))
(max 2 ekp-region-margin-pixel)))))))
;;;###autoload
(defun ekp-gui-verify ()
"Verify pixel-exact justification against this session's display.
Opens the ekp showcase, enables follow-window justification, and
checks with `window-text-pixel-size' that every justified line
renders at exactly the window's text width — under YOUR fonts,
themes, remappings and text-scale. Reports PASS or FAIL."
(interactive)
(unless (display-graphic-p)
(user-error "GUI verification needs a graphical frame"))
(ekp-showcase)
(redisplay t)
(with-current-buffer "*ekp-showcase*"
(ekp-auto-justify-mode 1)
(when (timerp ekp-region--resize-timer)
(cancel-timer ekp-region--resize-timer))
(ekp-region--reflow (current-buffer) (ekp-region--effective-width))
(redisplay t)
(let* ((r (ekp-gui-verify--scan (current-buffer)))
(msg (format
"ekp-gui-verify: %s — %d lines, widest %dpx vs target %dpx (window %dpx)%s"
(if (plist-get r :pass) "PASS" "FAIL")
(plist-get r :lines) (plist-get r :widest)
(plist-get r :target) (plist-get r :body)
(if (> (plist-get r :exempt) 0)
(format ", %d verbatim lines exempt"
(plist-get r :exempt))
""))))
(message "%s" msg)
r)))
(defun ekp-gui-verify--case (name setup)
"Run one matrix case NAME with buffer SETUP; return a report line."
;; Leftover debounce timers from the previous case must not fire
;; into this case's fresh buffer.
(dolist (fn (list #'ekp-region--reflow
#'ekp-region--flush-dirty
#'ekp-region--process-chunk))
(cancel-function-timers fn))
(when (get-buffer "*ekp-showcase*")
(kill-buffer "*ekp-showcase*"))
(ekp-showcase)
(redisplay t)
(with-current-buffer "*ekp-showcase*"
(funcall setup)
(redisplay t)
(ekp-auto-justify-mode 1)
(when (timerp ekp-region--resize-timer)
(cancel-timer ekp-region--resize-timer))
(ekp-region--reflow (current-buffer) (ekp-region--effective-width))
(redisplay t)
(let ((r (ekp-gui-verify--scan (current-buffer))))
(prog1 (format "%-22s body=%4d target=%4d widest=%4d over=%d/%d %s"
name (plist-get r :body) (plist-get r :target)
(plist-get r :widest) (plist-get r :over)
(plist-get r :lines)
(if (plist-get r :pass) "PASS" "FAIL"))
(ekp-auto-justify-mode -1)))))
;;;###autoload
(defun ekp-gui-verify-matrix ()
"Run the display-context matrix and print a PASS/FAIL table.
Covers: plain, text-scale up/down, family+height face remap, and a
narrow frame with scaling. Intended for `emacs -Q'; in a customized
session prefer `ekp-gui-verify'."
(interactive)
(unless (display-graphic-p)
(user-error "GUI verification needs a graphical frame"))
(save-current-buffer
(ekp-gui-verify--matrix-1)))
(defun ekp-gui-verify--matrix-1 ()
"Run the matrix cases; caller guards the current buffer."
(let (results)
(set-frame-size (selected-frame) 190 40)
(push (ekp-gui-verify--case "base" #'ignore) results)
(push (ekp-gui-verify--case "text-scale +3"
(lambda () (text-scale-set 3)))
results)
(push (ekp-gui-verify--case "text-scale -2"
(lambda () (text-scale-set -2)))
results)
(push (ekp-gui-verify--case "remap family+height"
(lambda ()
(face-remap-add-relative
'default :height 1.15)))
results)
(push (ekp-gui-verify--case "no fringes"
(lambda ()
(set-window-fringes
(get-buffer-window (current-buffer))
0 0)))
results)
(push (ekp-gui-verify--case "no fringes + scale +2"
(lambda ()
(set-window-fringes
(get-buffer-window (current-buffer))
0 0)
(text-scale-set 2)))
results)
(set-frame-size (selected-frame) 70 40)
(push (ekp-gui-verify--case "narrow + scale +2"
(lambda () (text-scale-set 2)))
results)
(let ((table (string-join (nreverse results) "\n")))
(if noninteractive
(princ (concat table "\n"))
(with-current-buffer (get-buffer-create "*ekp-gui-verify*")
(erase-buffer)
(insert table "\n")
(display-buffer (current-buffer))))
table)))
(provide 'ekp-gui-verify)
;;; ekp-gui-verify.el ends here