;;;; 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))))