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>
This commit is contained in:
parent
448198bdb9
commit
29cef97ef8
@ -166,13 +166,27 @@ Non-zero only while `ekp-protrusion' is enabled: protruding glyphs
|
|||||||
extend past the flush edge, so the layout width must leave room."
|
extend past the flush edge, so the layout width must leave room."
|
||||||
(if ekp-protrusion
|
(if ekp-protrusion
|
||||||
(max 2 (ceiling (* (alist-get 'cjk-close ekp-protrusion-ratios 0.5)
|
(max 2 (ceiling (* (alist-get 'cjk-close ekp-protrusion-ratios 0.5)
|
||||||
(string-pixel-width "。"))))
|
(ekp--measured-width "。"))))
|
||||||
0))
|
0))
|
||||||
|
|
||||||
|
(defun ekp-region--indicator-reserve (&optional window)
|
||||||
|
"Pixels the truncation/continuation indicator eats in WINDOW.
|
||||||
|
With no right fringe (and on text terminals), Emacs draws the `$'
|
||||||
|
or `\\' indicator in the text area's LAST COLUMN — a line that
|
||||||
|
fills the body width exactly gets its final glyph displaced and
|
||||||
|
every justified line appears truncated. Reserve that column; with
|
||||||
|
a right fringe the indicator lives in the fringe and costs nothing."
|
||||||
|
(let ((win (or window (selected-window))))
|
||||||
|
(if (and (display-graphic-p (window-frame win))
|
||||||
|
(> (or (cadr (window-fringes win)) 0) 0))
|
||||||
|
0
|
||||||
|
(frame-char-width (window-frame win)))))
|
||||||
|
|
||||||
(defun ekp-region--window-pixel (&optional window)
|
(defun ekp-region--window-pixel (&optional window)
|
||||||
"Usable text width in pixels of WINDOW (default: selected window)."
|
"Usable text width in pixels of WINDOW (default: selected window)."
|
||||||
(max 1 (- (window-body-width window t)
|
(max 1 (- (window-body-width window t)
|
||||||
ekp-region-margin-pixel
|
ekp-region-margin-pixel
|
||||||
|
(ekp-region--indicator-reserve window)
|
||||||
(ekp-region-protrusion-reserve))))
|
(ekp-region-protrusion-reserve))))
|
||||||
|
|
||||||
(defun ekp-region--effective-width (&optional buffer)
|
(defun ekp-region--effective-width (&optional buffer)
|
||||||
@ -185,6 +199,43 @@ Falls back to the selected window when the buffer is not displayed."
|
|||||||
(apply #'min (mapcar #'ekp-region--window-pixel wins))
|
(apply #'min (mapcar #'ekp-region--window-pixel wins))
|
||||||
(ekp-region--window-pixel))))
|
(ekp-region--window-pixel))))
|
||||||
|
|
||||||
|
;;;###autoload
|
||||||
|
(defun ekp-diagnose ()
|
||||||
|
"Check that ekp's measurement matches this window's real rendering.
|
||||||
|
Justifies a probe line to the window width, renders it invisibly in
|
||||||
|
this buffer, and compares the rendered pixel width against the
|
||||||
|
target. A mismatch means glyph metrics differ between measurement
|
||||||
|
and display (e.g. a face-remapping ekp does not see) and justified
|
||||||
|
lines would come out over- or under-full."
|
||||||
|
(interactive)
|
||||||
|
(let* ((win (or (get-buffer-window (current-buffer)) (selected-window)))
|
||||||
|
(target (ekp-region--window-pixel win))
|
||||||
|
(probe (ekp-pixel-justify
|
||||||
|
(concat "汉字排版像素精确性探针,中英混排 probe line with "
|
||||||
|
"Latin words, 标点。悬挂?以及 hyphenation-ready "
|
||||||
|
"vocabulary examples 结尾。")
|
||||||
|
target))
|
||||||
|
(line (car (split-string probe "\n")))
|
||||||
|
(rendered
|
||||||
|
(with-silent-modifications
|
||||||
|
(let ((beg (point-max)))
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(goto-char beg)
|
||||||
|
(insert "\n" line)
|
||||||
|
(car (window-text-pixel-size win (1+ beg) (point-max))))
|
||||||
|
(delete-region beg (point-max))))))
|
||||||
|
(delta (- rendered target)))
|
||||||
|
(message (concat "ekp-diagnose: target %dpx, rendered %dpx (Δ%+d) — %s"
|
||||||
|
(if (buffer-local-value 'face-remapping-alist
|
||||||
|
(current-buffer))
|
||||||
|
" [buffer has face remappings]" ""))
|
||||||
|
target rendered delta
|
||||||
|
(if (<= (abs delta) ekp-region-margin-pixel)
|
||||||
|
"OK, measurement matches rendering"
|
||||||
|
"MISMATCH: justified lines will not fit this window"))
|
||||||
|
delta))
|
||||||
|
|
||||||
;;;; Pure string transforms
|
;;;; Pure string transforms
|
||||||
|
|
||||||
(defun ekp-region--split-hard (string)
|
(defun ekp-region--split-hard (string)
|
||||||
@ -703,6 +754,19 @@ Catches the buffer becoming displayed (possibly for the first time),
|
|||||||
window splits, and deletions of the narrowest window."
|
window splits, and deletions of the narrowest window."
|
||||||
(ekp-region--schedule-reflow))
|
(ekp-region--schedule-reflow))
|
||||||
|
|
||||||
|
(defun ekp-region--on-text-scale ()
|
||||||
|
"Re-flow after a text-scale change.
|
||||||
|
Scaling remaps the default face, so every glyph metric changed: the
|
||||||
|
current layout is wrong at the same pixel width and must be re-done
|
||||||
|
under the new measurement context."
|
||||||
|
(when (and ekp-auto-justify-mode ekp-region--auto-width)
|
||||||
|
(when (timerp ekp-region--resize-timer)
|
||||||
|
(cancel-timer ekp-region--resize-timer))
|
||||||
|
(setq ekp-region--resize-timer
|
||||||
|
(run-with-timer ekp-auto-justify-resize-delay nil
|
||||||
|
#'ekp-region--reflow
|
||||||
|
(current-buffer) ekp-region--auto-width))))
|
||||||
|
|
||||||
(defun ekp-region--schedule-reflow ()
|
(defun ekp-region--schedule-reflow ()
|
||||||
"Debounce a re-flow of the current buffer to its effective width."
|
"Debounce a re-flow of the current buffer to its effective width."
|
||||||
(when ekp-auto-justify-mode
|
(when ekp-auto-justify-mode
|
||||||
@ -855,6 +919,7 @@ the buffer text is restored exactly when the mode is turned off."
|
|||||||
(add-hook 'window-configuration-change-hook
|
(add-hook 'window-configuration-change-hook
|
||||||
#'ekp-region--on-window-change nil t)
|
#'ekp-region--on-window-change nil t)
|
||||||
(add-hook 'window-scroll-functions #'ekp-region--on-scroll nil t)
|
(add-hook 'window-scroll-functions #'ekp-region--on-scroll nil t)
|
||||||
|
(add-hook 'text-scale-mode-hook #'ekp-region--on-text-scale nil t)
|
||||||
(add-hook 'after-change-functions #'ekp-region--after-change nil t)
|
(add-hook 'after-change-functions #'ekp-region--after-change nil t)
|
||||||
(ekp-region--install-integrations)
|
(ekp-region--install-integrations)
|
||||||
;; Turning the major mode off/over kills local hooks silently;
|
;; Turning the major mode off/over kills local hooks silently;
|
||||||
@ -864,6 +929,7 @@ the buffer text is restored exactly when the mode is turned off."
|
|||||||
(remove-hook 'window-configuration-change-hook
|
(remove-hook 'window-configuration-change-hook
|
||||||
#'ekp-region--on-window-change t)
|
#'ekp-region--on-window-change t)
|
||||||
(remove-hook 'window-scroll-functions #'ekp-region--on-scroll t)
|
(remove-hook 'window-scroll-functions #'ekp-region--on-scroll t)
|
||||||
|
(remove-hook 'text-scale-mode-hook #'ekp-region--on-text-scale t)
|
||||||
(remove-hook 'after-change-functions #'ekp-region--after-change t)
|
(remove-hook 'after-change-functions #'ekp-region--after-change t)
|
||||||
(remove-hook 'change-major-mode-hook #'ekp-region--teardown t)
|
(remove-hook 'change-major-mode-hook #'ekp-region--teardown t)
|
||||||
(ekp-region--teardown)
|
(ekp-region--teardown)
|
||||||
|
|||||||
@ -114,6 +114,8 @@ family when STRING contains no Latin letter."
|
|||||||
;; no latin letter in string, use default
|
;; no latin letter in string, use default
|
||||||
(face-attribute 'default :family)))
|
(face-attribute 'default :family)))
|
||||||
|
|
||||||
|
(declare-function ekp--measured-width "ekp")
|
||||||
|
|
||||||
(defun ekp-word-spacing-pixel (string)
|
(defun ekp-word-spacing-pixel (string)
|
||||||
"Return the pixel width of an inter-word space for STRING.
|
"Return the pixel width of an inter-word space for STRING.
|
||||||
Use the blank glyph of STRING's Latin font; for a monospace font
|
Use the blank glyph of STRING's Latin font; for a monospace font
|
||||||
@ -121,11 +123,11 @@ that width is the space's own advance."
|
|||||||
;; font is monospace, use the pixel of blank
|
;; font is monospace, use the pixel of blank
|
||||||
;; as word spacing pixel
|
;; as word spacing pixel
|
||||||
(if-let ((font-family (ekp-monospace-p string)))
|
(if-let ((font-family (ekp-monospace-p string)))
|
||||||
(string-pixel-width
|
(ekp--measured-width
|
||||||
(propertize " " 'face `(:family ,font-family)))
|
(propertize " " 'face `(:family ,font-family)))
|
||||||
(let* ((letter (ekp-get-latin-letter string))
|
(let* ((letter (ekp-get-latin-letter string))
|
||||||
(font-family (ekp-font-family letter)))
|
(font-family (ekp-font-family letter)))
|
||||||
(string-pixel-width
|
(ekp--measured-width
|
||||||
(propertize
|
(propertize
|
||||||
" " 'face `(:family ,font-family))))))
|
" " 'face `(:family ,font-family))))))
|
||||||
|
|
||||||
|
|||||||
52
ekp.el
52
ekp.el
@ -249,7 +249,7 @@ when non-zero the C module is bypassed automatically."
|
|||||||
"Cache: equal-keyed table, content key → ekp-para struct.")
|
"Cache: equal-keyed table, content key → ekp-para struct.")
|
||||||
|
|
||||||
(defvar ekp--last-para nil
|
(defvar ekp--last-para nil
|
||||||
"Fast path: (string-object lang para) of the most recent lookup.
|
"Fast path: (string-object lang width-context para), most recent lookup.
|
||||||
One justification call resolves the same string object many times;
|
One justification call resolves the same string object many times;
|
||||||
this avoids recomputing the full cache key each time. Invalidated
|
this avoids recomputing the full cache key each time. Invalidated
|
||||||
by parameter changes, language changes, style-variable changes (see
|
by parameter changes, language changes, style-variable changes (see
|
||||||
@ -671,16 +671,48 @@ before.")
|
|||||||
(defvar ekp--box-width-cache-limit 65536
|
(defvar ekp--box-width-cache-limit 65536
|
||||||
"Entry cap for `ekp--box-width-cache'; the cache is flushed beyond it.")
|
"Entry cap for `ekp--box-width-cache'; the cache is flushed beyond it.")
|
||||||
|
|
||||||
|
(defun ekp--string-pixel-width (string)
|
||||||
|
"Pixel width of STRING as it will render in the current buffer.
|
||||||
|
Like `string-pixel-width', but honors the current buffer's
|
||||||
|
`face-remapping-alist' — which is where `text-scale-mode', themes
|
||||||
|
and mode-specific font tweaks live. Plain `string-pixel-width'
|
||||||
|
measures in a bare hidden buffer, so in any buffer with remapped
|
||||||
|
faces it reports the wrong font's metrics and every \"pixel-exact\"
|
||||||
|
line comes out wrong on screen (Emacs 31 grew a BUFFER argument for
|
||||||
|
exactly this; this is the 29/30-compatible equivalent)."
|
||||||
|
(if (null face-remapping-alist)
|
||||||
|
(string-pixel-width string)
|
||||||
|
(let ((remap face-remapping-alist))
|
||||||
|
(with-current-buffer (get-buffer-create " *ekp-pixel-width*" t)
|
||||||
|
(setq-local face-remapping-alist remap)
|
||||||
|
(delete-region (point-min) (point-max))
|
||||||
|
;; Keep line-affecting context out, like string-pixel-width.
|
||||||
|
(setq-local line-prefix nil wrap-prefix nil)
|
||||||
|
(insert string)
|
||||||
|
(prog1 (car (buffer-text-pixel-size nil nil t))
|
||||||
|
(delete-region (point-min) (point-max)))))))
|
||||||
|
|
||||||
|
(defun ekp--width-context ()
|
||||||
|
"The display context that box measurement depends on.
|
||||||
|
nil in an unremapped buffer (the common case); otherwise the
|
||||||
|
buffer's `face-remapping-alist', which changes glyph metrics and
|
||||||
|
therefore must key every measurement and paragraph cache entry."
|
||||||
|
face-remapping-alist)
|
||||||
|
|
||||||
(defun ekp--measured-width (str)
|
(defun ekp--measured-width (str)
|
||||||
"`string-pixel-width' of STR, through the global width cache."
|
"Pixel width of STR in the current display context, cached."
|
||||||
(let* ((ivs (ekp--key-intervals str))
|
(let* ((ivs (ekp--key-intervals str))
|
||||||
(key (if ivs (cons str ivs) str)))
|
(ctx (ekp--width-context))
|
||||||
|
(key (cond ((and (null ivs) (null ctx)) str)
|
||||||
|
((null ctx) (cons str ivs))
|
||||||
|
(t (list str ivs ctx)))))
|
||||||
(or (gethash key ekp--box-width-cache)
|
(or (gethash key ekp--box-width-cache)
|
||||||
(progn
|
(progn
|
||||||
(when (>= (hash-table-count ekp--box-width-cache)
|
(when (>= (hash-table-count ekp--box-width-cache)
|
||||||
ekp--box-width-cache-limit)
|
ekp--box-width-cache-limit)
|
||||||
(clrhash ekp--box-width-cache))
|
(clrhash ekp--box-width-cache))
|
||||||
(puthash key (string-pixel-width str) ekp--box-width-cache)))))
|
(puthash key (ekp--string-pixel-width str)
|
||||||
|
ekp--box-width-cache)))))
|
||||||
|
|
||||||
(defun ekp--para-key (string)
|
(defun ekp--para-key (string)
|
||||||
"Compute cache key for STRING.
|
"Compute cache key for STRING.
|
||||||
@ -694,6 +726,10 @@ are derived per string)."
|
|||||||
(list string
|
(list string
|
||||||
(prin1-to-string (ekp--key-intervals string))
|
(prin1-to-string (ekp--key-intervals string))
|
||||||
latin-font cjk-font
|
latin-font cjk-font
|
||||||
|
;; Buffers with remapped faces (text-scale, themes) render
|
||||||
|
;; — and therefore measure — differently: never alias their
|
||||||
|
;; paragraphs with an unremapped buffer's.
|
||||||
|
(ekp--width-context)
|
||||||
ekp-latin-lang
|
ekp-latin-lang
|
||||||
ekp-alignment
|
ekp-alignment
|
||||||
ekp-ragged-stretch-pixel
|
ekp-ragged-stretch-pixel
|
||||||
@ -970,8 +1006,9 @@ Computes ALL data in one pass: text, params, and prefix arrays."
|
|||||||
This is the main entry point for cached paragraph data."
|
This is the main entry point for cached paragraph data."
|
||||||
(if (and ekp--last-para
|
(if (and ekp--last-para
|
||||||
(eq (car ekp--last-para) string)
|
(eq (car ekp--last-para) string)
|
||||||
(equal (nth 1 ekp--last-para) ekp-latin-lang))
|
(equal (nth 1 ekp--last-para) ekp-latin-lang)
|
||||||
(nth 2 ekp--last-para)
|
(equal (nth 2 ekp--last-para) (ekp--width-context)))
|
||||||
|
(nth 3 ekp--last-para)
|
||||||
(unless ekp--para-cache
|
(unless ekp--para-cache
|
||||||
(setq ekp--para-cache (make-hash-table :test 'equal :size 100)))
|
(setq ekp--para-cache (make-hash-table :test 'equal :size 100)))
|
||||||
(let* ((key (ekp--para-key string))
|
(let* ((key (ekp--para-key string))
|
||||||
@ -986,7 +1023,8 @@ This is the main entry point for cached paragraph data."
|
|||||||
(let ((p (ekp--make-para string)))
|
(let ((p (ekp--make-para string)))
|
||||||
(puthash key p ekp--para-cache)
|
(puthash key p ekp--para-cache)
|
||||||
p)))))
|
p)))))
|
||||||
(setq ekp--last-para (list string ekp-latin-lang para))
|
(setq ekp--last-para
|
||||||
|
(list string ekp-latin-lang (ekp--width-context) para))
|
||||||
para)))
|
para)))
|
||||||
|
|
||||||
;;;###autoload
|
;;;###autoload
|
||||||
|
|||||||
10
readme.md
10
readme.md
@ -262,9 +262,13 @@ the point.)
|
|||||||
|
|
||||||
## Known Limitations
|
## Known Limitations
|
||||||
|
|
||||||
- Widths are computed from the string's own text properties. If the
|
- Measurement follows the current buffer's face remappings
|
||||||
destination buffer remaps faces (different `:height`, themes), widths
|
(`text-scale-mode`, themes, `ekp-org-setup`-style tweaks) and
|
||||||
may differ; justify with the same properties you will display.
|
reserves the truncation-indicator column in windows without
|
||||||
|
fringes, so justified lines fit the real display. If lines ever
|
||||||
|
look truncated or short in an exotic setup, run `M-x ekp-diagnose`
|
||||||
|
in that buffer — it reports whether measurement matches rendering
|
||||||
|
(and `M-x ekp-gui-verify` runs a full fit check).
|
||||||
- One font is assumed per Latin/CJK script per paragraph when computing
|
- One font is assumed per Latin/CJK script per paragraph when computing
|
||||||
spacing defaults; mixed-font paragraphs work but spacing defaults come
|
spacing defaults; mixed-font paragraphs work but spacing defaults come
|
||||||
from the first font found.
|
from the first font found.
|
||||||
|
|||||||
@ -224,8 +224,11 @@ Silicon 测得;方法见 DEVELOPER_ZH.md:
|
|||||||
|
|
||||||
## 已知限制
|
## 已知限制
|
||||||
|
|
||||||
- 宽度按字符串自身的文本属性测量。若目标 buffer 重映射了 face(不同
|
- 测量会跟随当前 buffer 的 face 重映射(`text-scale-mode`、主题等),
|
||||||
`:height`、主题),宽度可能有偏差;请用与显示时相同的属性做排版。
|
并在无 fringe 的窗口里为截断指示符预留一列,排版行贴合真实显示。
|
||||||
|
若在特殊配置下仍出现截断或偏短,在该 buffer 里执行
|
||||||
|
`M-x ekp-diagnose`——它会报告测量与渲染是否一致
|
||||||
|
(`M-x ekp-gui-verify` 可跑完整贴合检查)。
|
||||||
- 计算默认间距时假定每段落的拉丁/CJK 各使用一种字体;混合字体段落可以
|
- 计算默认间距时假定每段落的拉丁/CJK 各使用一种字体;混合字体段落可以
|
||||||
工作,但默认间距取自找到的第一个字体。
|
工作,但默认间距取自找到的第一个字体。
|
||||||
- `ekp-pixel-range-justify` 用三分搜索加局部扫描最小化平均 demerits;
|
- `ekp-pixel-range-justify` 用三分搜索加局部扫描最小化平均 demerits;
|
||||||
|
|||||||
195
tests/ekp-gui-verify.el
Normal file
195
tests/ekp-gui-verify.el
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
;;; 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
|
||||||
@ -764,6 +764,42 @@ the (position × line-count) Elisp DP — they must agree."
|
|||||||
(ekp-clear-caches)
|
(ekp-clear-caches)
|
||||||
(should (equal-including-properties via-c via-el)))))))))
|
(should (equal-including-properties via-c via-el)))))))))
|
||||||
|
|
||||||
|
;;;; Display-context measurement (M5 wave)
|
||||||
|
|
||||||
|
(ert-deftest ekp-test-width-context-keys-caches ()
|
||||||
|
"Buffers with face remappings must never share cached paragraphs.
|
||||||
|
`text-scale-mode' and theme tweaks live in `face-remapping-alist';
|
||||||
|
glyphs render at different sizes there, so paragraph data measured
|
||||||
|
in one context is wrong in another."
|
||||||
|
(ekp-clear-caches)
|
||||||
|
(let ((s "上下文键控检查内容足够长断行"))
|
||||||
|
(should-not (equal (ekp--para-key s)
|
||||||
|
(let ((face-remapping-alist
|
||||||
|
'((default :height 1.5))))
|
||||||
|
(ekp--para-key s))))
|
||||||
|
;; the same-string fast path must not leak across contexts either
|
||||||
|
(ekp-clear-caches)
|
||||||
|
(let* ((p1 (ekp--get-para s))
|
||||||
|
(p2 (let ((face-remapping-alist '((default :height 1.5))))
|
||||||
|
(ekp--get-para s))))
|
||||||
|
(should-not (eq p1 p2)))
|
||||||
|
(ekp-clear-caches)))
|
||||||
|
|
||||||
|
(ert-deftest ekp-test-width-context-measurement-cached-separately ()
|
||||||
|
"The width cache keeps remapped and plain measurements apart."
|
||||||
|
(ekp-clear-caches)
|
||||||
|
(let* ((s "宽")
|
||||||
|
(plain (ekp--measured-width s))
|
||||||
|
(remapped (let ((face-remapping-alist '((default :height 2.0))))
|
||||||
|
(ekp--measured-width s))))
|
||||||
|
;; In batch both degrade to columns (equal values); the point is
|
||||||
|
;; that neither call poisons the other's cache entry.
|
||||||
|
(should (= plain (ekp--measured-width s)))
|
||||||
|
(should (= remapped
|
||||||
|
(let ((face-remapping-alist '((default :height 2.0))))
|
||||||
|
(ekp--measured-width s))))
|
||||||
|
(ekp-clear-caches)))
|
||||||
|
|
||||||
(provide 'ekp-tests)
|
(provide 'ekp-tests)
|
||||||
|
|
||||||
;;; ekp-tests.el ends here
|
;;; ekp-tests.el ends here
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user