feat: interactive showcase — M-x ekp-showcase

One self-contained demo buffer for every feature, driven by single
keys, with per-reflow milliseconds in the header line:

  -/+  width ±20 px (arrows: ±4)   d  animated width sweep + fps report
  a    cycle 4 alignments          p  hanging punctuation
  i    first-line indent (2 em)    s  wedge parshape (Elisp 2D path)
  c    C engine vs pure Elisp      w  follow window width (auto-mode)

The sample text bundles a verbatim-protected code block, an inline
no-break atom, and NBSP-joined figures; the engine indicator honestly
shows when a feature (indent/parshape) bypasses C.  Launch:

  emacs -Q -L /path/to/emacs-kp -l tests/ekp-showcase.el -f ekp-showcase

Verified interactively in GUI Emacs (justify 480→360 px, protrusion +
indent on: state and timing update live).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kinneyzhang 2026-07-26 23:48:35 +08:00
parent dc2f6da846
commit 5233baeea0
3 changed files with 265 additions and 0 deletions

View File

@ -233,6 +233,20 @@ off most for optimal-width search and long multi-paragraph texts.
- In batch/tty Emacs, pixel widths degrade to character columns (the
full pipeline still works; useful for testing).
## Interactive Demo
```bash
emacs -Q -L /path/to/emacs-kp -l tests/ekp-showcase.el -f ekp-showcase
```
One buffer, live keys: `-`/`+` change the pixel width (per-reflow time
in the header line), `d` runs an animated width sweep with an fps
report, `a` cycles alignment, `p` toggles hanging punctuation, `i`
first-line indent, `s` a wedge parshape, `c` compares the C engine
with pure Elisp, `w` follows the window width via
`ekp-auto-justify-mode`. The sample text includes a protected code
block, an inline no-break atom and NBSP-joined numbers.
## Testing
```bash

View File

@ -202,6 +202,18 @@ Silicon 测得;方法见 DEVELOPER_ZH.md:
代价关于宽度并非严格单峰,结果是很好的局部最优,不保证全局最优。
- batch/tty 模式下像素宽度退化为字符列数(整条管线仍可工作,便于测试)。
## 交互式演示
```bash
emacs -Q -L /path/to/emacs-kp -l tests/ekp-showcase.el -f ekp-showcase
```
单 buffer 按键交互:`-`/`+` 增减像素宽度(头行实时显示本次重排毫秒
数),`d` 宽度扫掠动画并报告 fps,`a` 循环四种对齐,`p` 标点悬挂,
`i` 首行缩进,`s` 楔形 parshape,`c` 切换 C/Elisp 引擎对比性能,
`w` 进入跟随窗口宽度模式(真实 `ekp-auto-justify-mode`)。样例文本
内置受保护代码块、行内不可断原子与 NBSP 锁定的数字。
## 测试
```bash

239
tests/ekp-showcase.el Normal file
View File

@ -0,0 +1,239 @@
;;; ekp-showcase.el --- Interactive feature & performance showcase -*- lexical-binding: t; -*-
;;; Commentary:
;; One self-contained interactive demo for everything ekp does.
;;
;; emacs -Q -L /path/to/emacs-kp -l tests/ekp-showcase.el -f ekp-showcase
;;
;; or, with the package on `load-path': M-x ekp-showcase
;;
;; Keys (also shown by `?'):
;; -/+ or ←/→ width 20 / +20 px (Shift variants: ±4 px)
;; d animated width sweep, reports ms/frame and fps
;; a cycle alignment: justify → ragged-right → center → ragged-left
;; p toggle hanging punctuation (protrusion)
;; i toggle first-line indent (2 em)
;; s toggle a wedge parshape (Elisp 2D path — watch the ms)
;; c toggle C engine (compare per-reflow time live)
;; w follow window width (real ekp-auto-justify-mode; resize away)
;; g re-justify now r reset all q quit
;;; Code:
(require 'ekp)
(require 'ekp-region)
(defvar ekp-showcase-min-width 140)
(defvar ekp-showcase-max-width 1200)
(defvar-local ekp-showcase--width 480)
(defvar-local ekp-showcase--last-ms nil)
(defvar-local ekp-showcase--parshape-on nil)
(defconst ekp-showcase--alignments
'(justify ragged-right center ragged-left))
(defun ekp-showcase--sample ()
"Build the demo text: prose, punctuation, atoms, NBSP, a code block."
(let ((zh1 "汉字排版的质感藏在细节里:开启标点悬挂之后,行尾的句号。逗号,和问号?都会把墨迹之外的空白悬出齐边,右边缘因此在视觉上更加平直。避头尾规则保证「引号」与《书名号》永远紧贴内容,连续闭合标点如此。」也绝不拆行。")
(mixed "中英混排是 Emacs 里最常见的场景:The quick brown fox jumps over the lazy dog, 而 internationalization 这样的长词在窄栏会按 Liang 模式断词,连字符同样参与悬挂。")
(atoms (concat "行内原子演示:代码片段 "
(propertize "(ekp-pixel-justify STR W)"
'ekp-no-break t
'face '(:inherit fixed-pitch
:background "#3a3f4b"
:foreground "#98c379"))
" 永不拆散、空格保持字面宽度;不间断空格让 100"
(string #x00A0)
"000 与 3.14"
(string #x00A0)
"MB 这类数字单位锁在同一行。"))
(code (propertize
"(defun ekp-demo (text width) ; ekp-verbatim: 代码块整段豁免\n (insert (ekp-pixel-justify text width)))"
'ekp-verbatim t
'face '(:inherit fixed-pitch :foreground "#61afef")))
(zh2 "把窗口拉宽或压窄(按 w 进入跟随窗口模式),或者按住 - 与 + 连续改变像素宽度,头行会实时显示这一次重排消耗的毫秒数:段落缓存与渲染缓存命中时,数字会明显变小。")
(en "Purely Latin paragraphs justify with natural word spacing, hyphenation, and hanging periods. Try width sweeps with the d key and compare the C engine against pure Elisp with c."))
(string-join (list zh1 mixed atoms code zh2 en) "\n\n")))
(defun ekp-showcase--header ()
(format " EKP 展示 宽 %dpx 对齐 %s 悬挂 %s 缩进 %s parshape %s 引擎 %s%s | 重排 %s | ? 帮助 q 退出"
ekp-showcase--width
ekp-alignment
(if ekp-protrusion "on" "off")
(if ekp-first-line-indent "2em" "off")
(if ekp-showcase--parshape-on "wedge" "off")
(if (and ekp-use-c-module (ekp--c-available-p)) "C" "elisp")
(if ekp-auto-justify-mode " [跟随窗口]" "")
(if ekp-showcase--last-ms
(format "%.1f ms" ekp-showcase--last-ms)
"")))
(defun ekp-showcase--apply-parshape ()
"Recompute the wedge parshape for the current width."
(setq-local ekp-parshape
(when ekp-showcase--parshape-on
(let ((w ekp-showcase--width))
(list (cons 0 w)
(cons 40 (max 120 (- w 80)))
(cons 80 (max 120 (- w 160)))
(cons 40 (max 120 (- w 80)))
(cons 0 w))))))
(defun ekp-showcase--refresh ()
"Re-justify the buffer at the current width; record the time."
(ekp-showcase--apply-parshape)
(let ((inhibit-read-only t)
(t0 (float-time)))
(ekp-justify-region (point-min) (point-max) ekp-showcase--width)
(setq ekp-showcase--last-ms (* 1000 (- (float-time) t0))))
(setq header-line-format (ekp-showcase--header))
(force-mode-line-update))
(defun ekp-showcase-set-width (w)
"Set demo width to W and re-justify."
(interactive "n宽度(px): ")
(if ekp-auto-justify-mode
(message "跟随窗口模式下宽度由窗口决定;按 w 退出该模式")
(setq ekp-showcase--width
(max ekp-showcase-min-width (min ekp-showcase-max-width w)))
(ekp-showcase--refresh)))
(defun ekp-showcase-wider () (interactive) (ekp-showcase-set-width (+ ekp-showcase--width 20)))
(defun ekp-showcase-narrower () (interactive) (ekp-showcase-set-width (- ekp-showcase--width 20)))
(defun ekp-showcase-wider-1 () (interactive) (ekp-showcase-set-width (+ ekp-showcase--width 4)))
(defun ekp-showcase-narrower-1 () (interactive) (ekp-showcase-set-width (- ekp-showcase--width 4)))
(defun ekp-showcase-sweep ()
"Animate the width down and back up, then report ms/frame."
(interactive)
(when ekp-auto-justify-mode (ekp-auto-justify-mode -1))
(let* ((start ekp-showcase--width)
(low (max ekp-showcase-min-width (- start 240)))
(widths (append (number-sequence start low -8)
(number-sequence low start 8)))
(times nil))
(dolist (w widths)
(setq ekp-showcase--width w)
(let ((t0 (float-time)))
(ekp-showcase--refresh)
(push (* 1000 (- (float-time) t0)) times))
(sit-for 0.001))
(let* ((n (length times))
(avg (/ (apply #'+ times) n)))
(message "扫掠 %d 帧:平均 %.1f ms/帧(≈%d fps 能力),最慢 %.1f ms — 再按一次 d 体验缓存命中后的速度"
n avg (round (/ 1000.0 avg)) (apply #'max times)))))
(defun ekp-showcase-cycle-alignment ()
(interactive)
(setq-local ekp-alignment
(or (cadr (memq ekp-alignment ekp-showcase--alignments))
(car ekp-showcase--alignments)))
(ekp-showcase--refresh)
(message "对齐:%s" ekp-alignment))
(defun ekp-showcase-toggle-protrusion ()
(interactive)
(setq-local ekp-protrusion (not ekp-protrusion))
(ekp-showcase--refresh)
(message "标点悬挂:%s" (if ekp-protrusion "开(看行尾标点探出齐边)" "")))
(defun ekp-showcase-toggle-indent ()
(interactive)
(setq-local ekp-first-line-indent (if ekp-first-line-indent nil t))
(ekp-showcase--refresh)
(message "首行缩进:%s" (if ekp-first-line-indent "2em" "")))
(defun ekp-showcase-toggle-parshape ()
(interactive)
(setq ekp-showcase--parshape-on (not ekp-showcase--parshape-on))
(ekp-showcase--refresh)
(message "parshape:%s" (if ekp-showcase--parshape-on
"楔形(Elisp 2D 路径,注意 ms 变化)" "")))
(defun ekp-showcase-toggle-engine ()
(interactive)
(setq-local ekp-use-c-module (not ekp-use-c-module))
(ekp-clear-caches) ; force real recompute so the comparison is honest
(ekp-showcase--refresh)
(message "引擎:%s" (if (and ekp-use-c-module (ekp--c-available-p))
"C(并行)" "纯 Elisp")))
(defun ekp-showcase-toggle-follow ()
(interactive)
(if ekp-auto-justify-mode
(progn (ekp-auto-justify-mode -1)
(ekp-showcase--refresh)
(message "退出跟随窗口模式"))
(ekp-auto-justify-mode 1)
(setq header-line-format (ekp-showcase--header))
(message "跟随窗口模式:拖动改变窗口/边框宽度试试(防抖 %.2fs)"
ekp-auto-justify-resize-delay)))
(defun ekp-showcase-reset ()
(interactive)
(when ekp-auto-justify-mode (ekp-auto-justify-mode -1))
(setq-local ekp-alignment 'justify)
(setq-local ekp-protrusion nil)
(setq-local ekp-first-line-indent nil)
(setq ekp-showcase--parshape-on nil)
(setq ekp-showcase--width 480)
(ekp-showcase--refresh)
(message "已重置"))
(defun ekp-showcase-help ()
(interactive)
(message (concat "-/+ 宽度±20 ←/→ ±4 d 扫掠动画 a 对齐 p 悬挂 "
"i 缩进 s parshape c 引擎 w 跟随窗口 g 刷新 r 重置 q 退出")))
(defvar ekp-showcase-mode-map
(let ((map (make-sparse-keymap)))
(suppress-keymap map)
(define-key map (kbd "-") #'ekp-showcase-narrower)
(define-key map (kbd "+") #'ekp-showcase-wider)
(define-key map (kbd "=") #'ekp-showcase-wider)
(define-key map (kbd "<left>") #'ekp-showcase-narrower-1)
(define-key map (kbd "<right>") #'ekp-showcase-wider-1)
(define-key map (kbd "d") #'ekp-showcase-sweep)
(define-key map (kbd "a") #'ekp-showcase-cycle-alignment)
(define-key map (kbd "p") #'ekp-showcase-toggle-protrusion)
(define-key map (kbd "i") #'ekp-showcase-toggle-indent)
(define-key map (kbd "s") #'ekp-showcase-toggle-parshape)
(define-key map (kbd "c") #'ekp-showcase-toggle-engine)
(define-key map (kbd "w") #'ekp-showcase-toggle-follow)
(define-key map (kbd "g") #'ekp-showcase--refresh)
(define-key map (kbd "W") #'ekp-showcase-set-width)
(define-key map (kbd "r") #'ekp-showcase-reset)
(define-key map (kbd "?") #'ekp-showcase-help)
(define-key map (kbd "q") #'quit-window)
map))
(define-derived-mode ekp-showcase-mode special-mode "EKP-Showcase"
"Interactive showcase for ekp typesetting features."
(setq-local truncate-lines t)
(setq-local cursor-type 'bar))
;;;###autoload
(defun ekp-showcase ()
"Open the interactive ekp feature & performance showcase."
(interactive)
(ignore-errors (ekp-c-module-load))
(let ((buf (get-buffer-create "*ekp-showcase*")))
(with-current-buffer buf
(let ((inhibit-read-only t))
(erase-buffer)
(ekp-showcase-mode)
(when (and (display-graphic-p)
(find-font (font-spec :family "Cascadia Next SC")))
(face-remap-add-relative 'default :family "Cascadia Next SC"))
(insert (ekp-showcase--sample))
(goto-char (point-min))
(ekp-showcase--refresh)))
(pop-to-buffer buf)
(delete-other-windows)
(ekp-showcase-help)))
(provide 'ekp-showcase)
;;; ekp-showcase.el ends here