Files
lime/test/x11-font-smoke.lisp
2026-10-02 16:35:17 -05:00

164 lines
12 KiB
Common Lisp

;;;; Opt-in isolated-X font/overlay regression, including live reload/rollback.
;;;; Requires Xvfb, Pango/Cairo, and fonts covering Japanese and color emoji.
;;;; Optional: bind CL-USER::*FONT-SMOKE-IMAGE-DIRECTORY* before loading to save
;;;; bar.png and palette.png for inspection. Never uses the current DISPLAY.
;;;; *FONT-SMOKE-FONT-OPTIONS* optionally supplies MAKE-UI-FONT-STYLE keywords.
(require :asdf)
(asdf:load-asd (truename (merge-pathnames "../lime.asd" *load-truename*)))
(asdf:load-system "lime/x11")
(defvar *font-smoke-image-directory* nil)
(defvar *font-smoke-font-options* nil)
(dolist (depth '(24 16))
(let ((server (uiop:launch-program
(list "Xvfb" "-displayfd" "1" "-screen" "0"
(format nil "1000x622x~D" depth) "-nolisten" "tcp")
:output :stream :error-output :interactive))
(backend nil) (checks 0))
(unwind-protect
(let* ((number (parse-integer (read-line (uiop:process-info-output server))))
(configuration (lime.x11:make-lime-configuration
:default-keybindings-p nil
:bar-style (lime.x11:make-head-bar-style
:foreground "#dbc9a6" :focused-foreground "#dbc9a6"
:accent-color "#ffb054" :menu-label-color "#dbc9a6"
:logo-color "#768565")
:font (apply #'lime.x11:make-ui-font-style *font-smoke-font-options*)
:bar-height 24 :bar-height-supplied-p t)))
(setf backend (lime.x11:start-lime
:display-name (format nil ":~D" number) :head-discovery :none))
(lime.x11:apply-lime-configuration backend configuration)
(let* ((display (lime.x11::x11-backend-display backend))
(head (first (lime.x11:backend-head-ids backend)))
(bar (gethash head (lime.x11::x11-backend-head-bars backend)))
(client (xlib:create-window :parent (lime.x11::x11-backend-root backend)
:x 0 :y 0 :width 100 :height 100))
(labels '("café — 日本語 🎮" "👩‍💻 Coding and debugging"
"مرحبا بالعالم" "é — combining accents"))
(session (lime.ui:make-ui-session
(loop for label in labels for id from 0 collect
(lime.ui:make-ui-candidate :id id :label label :detail "Unicode sample")))))
(labels ((check (truth label)
(assert truth () "Font smoke (~D-bit): ~A" depth label)
(incf checks))
(check-frame (canvas window)
(xlib:display-finish-output display)
(check (equalp (lime.x11::ui-canvas-pixels canvas)
(xlib:image-z-pixarray
(xlib:get-image window :x 0 :y 0
:width (lime.x11::ui-canvas-width canvas)
:height (lime.x11::ui-canvas-height canvas)
:format :z-pixmap)))
"painted frame survives the X11 pixel upload"))
(snapshot (canvas name)
(when (and (= depth 24) *font-smoke-image-directory*)
(check (zerop (lime.x11::%cairo-write-png
(lime.x11::ui-canvas-surface canvas)
(namestring (merge-pathnames name *font-smoke-image-directory*))))
"PNG snapshot"))))
(xlib:change-property client :_NET_WM_NAME
(babel:string-to-octets (first labels) :encoding :utf-8)
:UTF8_STRING 8)
(lime.x11:manage-window backend client)
(lime.x11:backend-set-head-bar-status-providers
backend (list (lambda (&rest ignored)
(declare (ignore ignored))
(list (lime.x11:format-music-status '(:state :playing :title "スパイダー")
:playing-icon "▶" :icon-foreground "#76929f")
(lime.x11:format-volume-status '(:volume 25) :icon "♪"
:icon-foreground "#76929f")))))
(check-frame (lime.x11::x11-head-bar-canvas bar) (lime.x11::x11-head-bar-window bar))
(let ((pixels (lime.x11::ui-canvas-pixels (lime.x11::x11-head-bar-canvas bar))))
(check (loop for y below 24 always
(loop for x below 10 always
(= (aref pixels y x) (aref pixels 0 0)
(aref pixels y (- 999 x)))))
"both outer text margins remain clear"))
;; Inspect native RGB pixels: the logo is green, not the cream
;; used by neighboring labels (before depth-specific upload).
(let* ((canvas (lime.x11::x11-head-bar-canvas bar))
(data (lime.x11::%cairo-data (lime.x11::ui-canvas-surface canvas)))
(stride (lime.x11::%cairo-stride (lime.x11::ui-canvas-surface canvas)))
(right (lime.x11::x11-head-bar-hit-region-right
(first (lime.x11::x11-head-bar-hit-regions bar)))))
(check (loop for y below 24 thereis
(loop for x from 10 below right
for pixel = (cffi:mem-ref data :uint32 (+ (* y stride) (* x 4)))
thereis (> (ldb (byte 8 8) pixel)
(max (ldb (byte 8 16) pixel) (ldb (byte 8 0) pixel)))))
"Lime logo uses its independent green foreground"))
(snapshot (lime.x11::x11-head-bar-canvas bar) "bar.png")
(lime.x11::%open-ui-session backend session)
(let* ((overlay (lime.x11::x11-backend-ui-overlay backend))
(old-bar-font (lime.x11::x11-head-bar-font bar))
(old-font (lime.x11::x11-ui-overlay-font overlay))
(old-canvas (lime.x11::x11-ui-overlay-canvas overlay))
(old-height (lime.x11::x11-ui-overlay-height overlay))
(large (lime.x11::copy-lime-configuration configuration)))
(dolist (text labels)
(check (zerop (lime.x11::text-metrics-unknown-glyphs
(lime.x11::%text-metrics old-font text)))
(format nil "installed fonts cover ~S" text)))
(check-frame old-canvas (lime.x11::x11-ui-overlay-window overlay))
(snapshot old-canvas "palette.png")
(lime.x11::%render-command-palette backend)
(check (eq old-canvas (lime.x11::x11-ui-overlay-canvas overlay)) "reuse same-size canvas")
(check (eq old-font (lime.x11::x11-ui-overlay-font overlay)) "reuse unchanged font")
(setf (lime.x11:lime-configuration-font large)
(lime.x11:make-ui-font-style :family "sans-serif, Noto Color Emoji" :size 22 :weight :bold)
(lime.x11:lime-configuration-bar-height large) 36)
(setf (lime.x11:lime-configuration-bar-style large)
(lime.x11:make-head-bar-style :padding-x 16))
(lime.x11:apply-lime-configuration backend large)
(check (= (+ 16 (lime.x11::%text-width (lime.x11::x11-head-bar-font bar) "Lime"))
(lime.x11::x11-head-bar-hit-region-right
(first (lime.x11::x11-head-bar-hit-regions bar))))
"padding reload also updates click geometry")
(check (eq session (lime.x11::x11-backend-ui-session backend)) "reload preserves open session")
(check (eq overlay (lime.x11::x11-backend-ui-overlay backend)) "reload preserves overlay window")
(check (> (lime.x11::x11-ui-overlay-height overlay) old-height) "font increases measured row height")
(check (= 36 (xlib:drawable-y (lime.x11::x11-ui-overlay-window overlay))) "overlay follows new bar height")
(check (null (lime.x11::ui-text-font-layout old-font)) "reload frees old palette font")
(check (null (lime.x11::ui-text-font-layout old-bar-font)) "reload frees old bar font")
(check (null (lime.x11::ui-canvas-surface old-canvas)) "resize frees old canvas")
(check (= 22 (lime.x11:ui-font-style-size
(lime.x11::ui-text-font-style (lime.x11::x11-ui-overlay-font overlay))))
"palette uses configured pixel size")
(check-frame (lime.x11::x11-ui-overlay-canvas overlay) (lime.x11::x11-ui-overlay-window overlay))
(let ((bad (lime.x11::copy-lime-configuration large))
(failed nil) (failed-font nil))
(setf (lime.x11:lime-configuration-font bad) (lime.x11:make-ui-font-style :size 10)
(lime.x11:lime-configuration-startup-hooks bad)
(list (lambda (ignored)
(declare (ignore ignored))
(setf failed-font (lime.x11::x11-ui-overlay-font overlay))
(error "Deliberate font rollback"))))
(handler-case (lime.x11:apply-lime-configuration backend bad :run-startup-hooks-p t)
(error () (setf failed t)))
(check (and failed failed-font) "installation reaches deliberate rollback")
(check (eq large (lime.x11:x11-backend-configuration backend)) "previous config restored")
(check (null (lime.x11::ui-text-font-layout failed-font)) "rollback frees failed font")
(check (= 22 (lime.x11:ui-font-style-size
(lime.x11::ui-text-font-style (lime.x11::x11-head-bar-font bar))))
"rollback restores bar font")
(check (= 22 (lime.x11:ui-font-style-size
(lime.x11::ui-text-font-style (lime.x11::x11-ui-overlay-font overlay))))
"rollback restores overlay font")
(check-frame (lime.x11::x11-ui-overlay-canvas overlay) (lime.x11::x11-ui-overlay-window overlay)))
;; Closing and reopening frees all transient native resources.
(lime.x11:backend-close-command-palette backend)
(check (null (lime.x11::ui-text-font-layout (lime.x11::x11-ui-overlay-font overlay)))
"close frees palette layout")
(check (null (lime.x11::ui-canvas-surface (lime.x11::x11-ui-overlay-canvas overlay)))
"close frees palette canvas")
(lime.x11::%open-ui-session backend session)
(check (not (eq overlay (lime.x11::x11-backend-ui-overlay backend))) "reopen builds a new overlay"))
(lime.x11:stop-backend backend)
(setf backend nil)
(check (null (lime.x11::ui-text-font-layout (lime.x11::x11-head-bar-font bar))) "shutdown frees bar font")
(check (null (lime.x11::ui-canvas-surface (lime.x11::x11-head-bar-canvas bar))) "shutdown frees bar canvas")
(format t "~&Live X11 fonts (~D-bit): ~D checks passed.~%" depth checks))))
(when backend (lime.x11:stop-backend backend))
(uiop:terminate-process server)
(uiop:wait-process server))))