(in-package #:lime.x11.test) (in-suite lime-x11-suite) ;; Pango/Cairo measurements are headless: these tests do not open DISPLAY. (lime.config:defconfiguration font-config-fixture (:font :family "sans-serif, monospace" :size 18 :weight :bold)) (test font-configuration-is-shared-and-validated (let ((font (lime-configuration-font font-config-fixture))) (is (string= "sans-serif, monospace" (ui-font-style-family font))) (is (= 18 (ui-font-style-size font))) (is (eq :bold (ui-font-style-weight font))) (is (eq font-config-fixture (lime.x11::%validate-lime-configuration font-config-fixture)))) (dolist (style (list (make-ui-font-style :family " ") (make-ui-font-style :family (format nil "bad~%family")) (make-ui-font-style :size 0) (make-ui-font-style :size 97) (make-ui-font-style :size "14") (make-ui-font-style :weight :unknown))) (signals invalid-transition (lime.x11::%validate-ui-font-style style))) (signals error (macroexpand-1 '(lime.config:defconfiguration bad-font (:font) (:font)))) (signals error (macroexpand-1 '(lime.config:defconfiguration bad-font (:font :typo 10))))) (test shaped-text-fits-without-splitting-graphemes (let ((font (lime.x11::%open-text-font (make-ui-font-style :family "sans-serif")))) (unwind-protect (progn (is (> (lime.x11::%text-width font "WWW") (lime.x11::%text-width font "iii"))) (is (= 0 (lime.x11::%text-width font ""))) (dolist (text '("é" "👩‍💻" "🇲🇽")) (is (equalp (vector 0 (length text)) (lime.x11::text-metrics-boundaries (lime.x11::%text-metrics font text))))) (dolist (text '("ééé café" "👩‍💻 👩‍💻 long title" "🇲🇽🇲🇽 日本語" "مرحبا بالعالم")) (let* ((metrics (lime.x11::%text-metrics font text)) (boundaries (lime.x11::text-metrics-boundaries metrics))) ;; Check every pixel budget, not one lucky truncation boundary. (is (loop for width from 0 to (lime.x11::text-metrics-width metrics) always (multiple-value-bind (fitted prefix) (lime.x11::%fit-ui-text font text width) (and (<= (lime.x11::%text-width font fitted) width) (find prefix boundaries) (string= text fitted :end1 prefix :end2 prefix))))))) (dotimes (i 300) (lime.x11::%text-width font (format nil "cache ~D" i))) (is (<= (hash-table-count (lime.x11::ui-text-font-metrics font)) 256))) (lime.x11::%free-text-font font)) (is-false (lime.x11::ui-text-font-layout font)) (is-false (lime.x11::ui-canvas-surface (lime.x11::ui-text-font-canvas font))) (lime.x11::%free-text-font font))) (test absent-font-family-falls-back-and-font-resources-replace-safely (let* ((style (make-ui-font-style :family "Lime Deliberately Nonexistent Font")) (font (lime.x11::%open-text-font style)) (new nil)) (unwind-protect (progn (is (= 0 (lime.x11::text-metrics-unknown-glyphs (lime.x11::%text-metrics font "Hello")))) (is (eq font (lime.x11::%ensure-text-font font style))) (signals invalid-transition (lime.x11::%ensure-text-font font (make-ui-font-style :size 0))) (is-true (lime.x11::ui-text-font-layout font)) (setf new (lime.x11::%ensure-text-font font (make-ui-font-style :size 28))) (is-false (lime.x11::ui-text-font-layout font)) (is (> (lime.x11::%font-line-height new) 28))) (lime.x11::%free-text-font font) (lime.x11::%free-text-font new)))) (test status-shaping-keeps-widget-boundaries-and-rtl-click-ranges (let ((font (lime.x11::%open-text-font (make-ui-font-style)))) (unwind-protect (progn (multiple-value-bind (text prefix width runs) (lime.x11::%fit-bar-status font (list (make-head-bar-segment :text "مرحبا") (make-head-bar-segment :text "🎮")) 300) (declare (ignore text prefix)) (is (= 3 (length runs))) (is (equal '("مرحبا" " | " "🎮") (mapcar (lambda (run) (getf run :text)) runs))) (is (= width (reduce #'+ runs :key (lambda (run) (getf run :width)))))) (let* ((text "مرحبا...") (ranges (lime.x11::%text-prefix-ranges font text 5))) (is-true ranges) ;; For this RTL run the logical suffix dots are visually at left. (is (plusp (caar ranges))) (is (every (lambda (range) (<= 0 (car range) (cdr range) (lime.x11::%text-width font text))) ranges)))) (lime.x11::%free-text-font font)))) (test prefix-color-rasterization-keeps-unicode-shaping-and-resets-between-fields (let ((font (lime.x11::%open-text-font (make-ui-font-style))) (canvas (lime.x11::%make-ui-canvas 300 28))) (unwind-protect (labels ((pixels () (lime.x11::%cairo-flush (lime.x11::ui-canvas-surface canvas)) (let ((data (lime.x11::%cairo-data (lime.x11::ui-canvas-surface canvas))) (stride (lime.x11::%cairo-stride (lime.x11::ui-canvas-surface canvas)))) (loop for y below 28 append (loop for x below 300 collect (logand #xffffff (cffi:mem-ref data :uint32 (+ (* y stride) (* x 4)))))))) (red-p (pixel) (and (plusp pixel) (zerop (logand pixel #xffff)))) (gray-p (pixel) (= (ldb (byte 8 16) pixel) (ldb (byte 8 8) pixel) (ldb (byte 8 0) pixel)))) (dolist (text '("A title" "é 日本語" "é مرحبا")) (let ((width (lime.x11::%text-width font text)) (normal nil)) (lime.x11::%canvas-rectangle canvas 0 0 300 28 "#000000") (lime.x11::%canvas-text canvas font text 0 0 300 28 "#ffffff") (setf normal (pixels)) (lime.x11::%canvas-rectangle canvas 0 0 300 28 "#000000") (lime.x11::%canvas-text canvas font text 0 0 300 28 "#ffffff" :prefix-length 1 :prefix-color "#ff0000") (let ((colored (pixels))) (is (some #'red-p colored)) (is (some (lambda (p) (and (plusp p) (gray-p p))) colored)) ;; Foreground attributes must not move glyphs or change coverage. (is (equal (mapcar (lambda (p) (ldb (byte 8 16) p)) normal) (mapcar (lambda (p) (ldb (byte 8 16) p)) colored)))) (is (= width (lime.x11::%text-width font text))) (lime.x11::%canvas-rectangle canvas 0 0 300 28 "#000000") (lime.x11::%canvas-text canvas font text 0 0 300 28 "#ffffff") (is (equal normal (pixels))))) ;; With no retained source characters, truncation dots stay neutral. (multiple-value-bind (ignored prefix width runs) (lime.x11::%fit-bar-status font (list (make-head-bar-segment :text "Long icon and label" :prefix-length 4 :prefix-foreground "#ff0000")) (lime.x11::%text-width font "...")) (declare (ignore ignored prefix width)) (let ((run (first runs))) (is (= 0 (getf run :prefix))) (lime.x11::%canvas-rectangle canvas 0 0 300 28 "#000000") (lime.x11::%canvas-text canvas font (getf run :text) 0 0 300 28 "#ffffff" :prefix-length (getf run :prefix) :prefix-color "#ff0000") (is (every #'gray-p (pixels)))))) (lime.x11::%free-ui-canvas canvas) (lime.x11::%free-text-font font)))) (test opaque-rgb-upload-respects-truecolor-masks (is (= #xf800 (lime.x11::%rgb-to-visual-pixel #xff0000 #xf800 #x7e0 #x1f 16))) (is (= #xffff (lime.x11::%rgb-to-visual-pixel #xffffff #xf800 #x7e0 #x1f 16))) (is (= #x563412 (lime.x11::%rgb-to-visual-pixel #x123456 #xff #xff00 #xff0000 24))) (is (= #x3fffffff (lime.x11::%rgb-to-visual-pixel #xffffff #x3ff00000 #xffc00 #x3ff 30))) (is (= #xff123456 (lime.x11::%rgb-to-visual-pixel #x123456 #xff0000 #xff00 #xff 32)))) (test measured-palette-rows-keep-selection-visible (is (equal '(0 8) (multiple-value-list (lime.x11::%ui-visible-range 20 0 8)))) (is (equal '(5 13) (multiple-value-list (lime.x11::%ui-visible-range 20 12 8)))) (is (equal '(12 20) (multiple-value-list (lime.x11::%ui-visible-range 20 19 8)))) (is (equal '(0 2) (multiple-value-list (lime.x11::%ui-visible-range 2 1 8)))) (is (equal '(0 0) (multiple-value-list (lime.x11::%ui-visible-range 0 0 0))))) (defun text-test-ink-bounds (canvas) "Bounds of nonblack pixels, for checking real rasterization rather than math alone." (lime.x11::%cairo-flush (lime.x11::ui-canvas-surface canvas)) (let ((data (lime.x11::%cairo-data (lime.x11::ui-canvas-surface canvas))) (stride (lime.x11::%cairo-stride (lime.x11::ui-canvas-surface canvas))) (left (lime.x11::ui-canvas-width canvas)) (right 0) (top (lime.x11::ui-canvas-height canvas)) (bottom 0)) (dotimes (y (lime.x11::ui-canvas-height canvas)) (dotimes (x (lime.x11::ui-canvas-width canvas)) (when (plusp (logand #xffffff (cffi:mem-ref data :uint32 (+ (* y stride) (* x 4))))) (setf left (min left x) right (max right (1+ x)) top (min top y) bottom (max bottom (1+ y)))))) (values left top right bottom))) (test bar-baseline-centers-letter-bodies-not-empty-font-leading (dolist (style (list (make-ui-font-style) (make-ui-font-style :family "Dina, Fira Code Nerd Font" :size 12 :weight :bold))) (let ((font (lime.x11::%open-text-font style)) (canvas (lime.x11::%make-ui-canvas 240 22))) (unwind-protect (let ((baseline (lime.x11::%font-centered-baseline font 22))) (lime.x11::%canvas-rectangle canvas 0 0 240 22 "#000000") (lime.x11::%canvas-text canvas font "ABCabc012" 10 0 220 22 "#ffffff" :baseline baseline) (multiple-value-bind (left top right bottom) (text-test-ink-bounds canvas) (is (<= 10 left right 230)) (is (<= (abs (- top (- 22 bottom))) 1)) (is (< 0 top bottom 22))) ;; A descender changes ink bounds, but must not move existing letters. (let ((old-pixels nil) (sample-width (lime.x11::%text-width font "ABCabc012"))) (labels ((sample-pixels () (lime.x11::%cairo-flush (lime.x11::ui-canvas-surface canvas)) (let ((data (lime.x11::%cairo-data (lime.x11::ui-canvas-surface canvas))) (stride (lime.x11::%cairo-stride (lime.x11::ui-canvas-surface canvas)))) (loop for y below 22 append (loop for x from 10 below (+ 10 sample-width) collect (logand #xffffff (cffi:mem-ref data :uint32 (+ (* y stride) (* x 4))))))))) (setf old-pixels (sample-pixels)) (lime.x11::%canvas-rectangle canvas 0 0 240 22 "#000000") (lime.x11::%canvas-text canvas font "ABCabc012 gj" 10 0 220 22 "#ffffff" :baseline baseline) (is (equal old-pixels (sample-pixels))))) ;; Fallback glyphs and other measurements don't alter the reference. (lime.x11::%text-metrics font "日本語 🎮 Égj") (is (= baseline (lime.x11::%font-centered-baseline font 22)))) (lime.x11::%free-ui-canvas canvas) (lime.x11::%free-text-font font)))))