nandi/jolt-nativepublic Fork 0
0336529
Commits
Clone
git clone https://git.rickub.com/nandi/jolt-native.git
git clone ssh://git@rickub.com/nandi/jolt-native.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Refuse a fallback face that cannot be the size it was asked for

Adding Noto Color Emoji behind the UI font got the icons drawing and
made the compose bar a hundred and forty pixels tall. A colour emoji
font is a BITMAP with one fixed strike: ask it for fourteen and it
answers a hundred and twenty-eight, and SDL_ttf draws it at that. One
emoji in a row reshapes the window around it.

So a fallback is measured before it is trusted, and dropped if it comes
back more than twice the face's own height. A symbol face runs a little
tall and is fine; a bitmap strike is six times over and is not. Outline
faces are tried first and colour emoji last, so a system where SDL_ttf
CAN scale one still gets it.

Noto Emoji — the monochrome outline companion to NotoColorEmoji — is
first in the list now. It is the one that works at a UI size, and a
machine that has it gets emoji rather than the missing-glyph box.

The check needs real fonts, so it is not in the headless set: those
measure with a stub of eight pixels a character and cannot see a font
metric at all, which is why nothing caught this.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-09-10T08:59:25-07:00 Browse files
0336529 parent: 7d907d0
modified jvui/src/jvui/font.clj +32 -9
@@ -45,16 +45,25 @@
4545 broken\". SDL_ttf keeps a list of fallback faces and asks each in turn,
4646 so this costs nothing for text that the main face already has.
4747
48- Emoji first: a face that has both a symbol and its emoji presentation
49- should give the emoji one, which is what a message means by it.
48+ Outline faces first, and colour emoji last, because a colour emoji
49+ font is usually BITMAP one fixed strike, commonly 109 or 128 pixels
50+ and a face that cannot be scaled to the size asked for is worse than
51+ no face at all. `usable?` below is what actually decides; the order
52+ only settles who wins when both would do.
53+
5054 JVUI_FALLBACK_FONTS overrides, colon-separated, for a machine whose
5155 fonts live somewhere else."
52- ["/usr/share/fonts/noto/NotoColorEmoji.ttf"
53- "/usr/share/fonts/truetype/noto/NotoColorEmoji.ttf"
54- "/run/current-system/sw/share/X11/fonts/NotoColorEmoji.ttf"
56+ [;; Monochrome and scalable. Noto Emoji is the outline companion to
57+ ;; NotoColorEmoji and is the one that works at a UI size.
58+ "/usr/share/fonts/noto/NotoEmoji-Regular.ttf"
59+ "/usr/share/fonts/truetype/noto/NotoEmoji-Regular.ttf"
5560 "/usr/share/fonts/noto/NotoSansSymbols2-Regular.ttf"
5661 "/usr/share/fonts/noto/NotoSansSymbols-Regular.ttf"
57- "/usr/share/fonts/truetype/noto/NotoSansSymbols2-Regular.ttf"])
62+ "/usr/share/fonts/truetype/noto/NotoSansSymbols2-Regular.ttf"
63+ "/usr/share/fonts/truetype/noto/NotoSansSymbols-Regular.ttf"
64+ ;; Last, and only where SDL_ttf can scale it.
65+ "/usr/share/fonts/noto/NotoColorEmoji.ttf"
66+ "/usr/share/fonts/truetype/noto/NotoColorEmoji.ttf"])
5867
5968 (defn- fallback-paths []
6069 (if-let [env (System/getenv "JVUI_FALLBACK_FONTS")]
@@ -76,12 +85,26 @@
7685 ;; Fallbacks are per FACE, not per family, so each size opens its
7786 ;; own and they are kept for as long as the face is — SDL_ttf
7887 ;; holds the pointer and does not copy the font.
79- (let [fbs (into []
88+ (let [want (sdl/font-height f)
89+ fbs (into []
8090 (keep (fn [path]
8191 (let [g (sdl/open-font path (float size))]
8292 (when-not (ffi-null? g)
83- (sdl/add-fallback-font! f g)
84- g))))
93+ ;; A face that answers a size it was
94+ ;; not asked for cannot be used. A
95+ ;; colour emoji font is a bitmap with
96+ ;; one strike — ask for 14 and it
97+ ;; still gives 128 — and SDL_ttf draws
98+ ;; it at that, so one emoji in a
99+ ;; message makes a row a hundred and
100+ ;; twenty-eight pixels tall and pushes
101+ ;; the rest of the window out of the
102+ ;; way. Twice the face's own height is
103+ ;; the line: a symbol face runs a
104+ ;; little tall and is fine.
105+ (if (> (sdl/font-height g) (* 2 want))
106+ (do (sdl/close-font! g) nil)
107+ (do (sdl/add-fallback-font! f g) g))))))
85108 (fallback-paths))]
86109 (swap! cache update :fallbacks (fnil into []) fbs))
87110 (swap! cache assoc-in [:faces k] f)
@@ -45,16 +45,25 @@
45 broken\". SDL_ttf keeps a list of fallback faces and asks each in turn,45 broken\". SDL_ttf keeps a list of fallback faces and asks each in turn,
46 so this costs nothing for text that the main face already has.46 so this costs nothing for text that the main face already has.
47 47
48- Emoji first: a face that has both a symbol and its emoji presentation48+ Outline faces first, and colour emoji last, because a colour emoji
49- should give the emoji one, which is what a message means by it.49+ font is usually BITMAP one fixed strike, commonly 109 or 128 pixels
50+ and a face that cannot be scaled to the size asked for is worse than
51+ no face at all. `usable?` below is what actually decides; the order
52+ only settles who wins when both would do.
53+
50 JVUI_FALLBACK_FONTS overrides, colon-separated, for a machine whose54 JVUI_FALLBACK_FONTS overrides, colon-separated, for a machine whose
51 fonts live somewhere else."55 fonts live somewhere else."
52- ["/usr/share/fonts/noto/NotoColorEmoji.ttf"56+ [;; Monochrome and scalable. Noto Emoji is the outline companion to
53- "/usr/share/fonts/truetype/noto/NotoColorEmoji.ttf"57+ ;; NotoColorEmoji and is the one that works at a UI size.
54- "/run/current-system/sw/share/X11/fonts/NotoColorEmoji.ttf"58+ "/usr/share/fonts/noto/NotoEmoji-Regular.ttf"
59+ "/usr/share/fonts/truetype/noto/NotoEmoji-Regular.ttf"
55 "/usr/share/fonts/noto/NotoSansSymbols2-Regular.ttf"60 "/usr/share/fonts/noto/NotoSansSymbols2-Regular.ttf"
56 "/usr/share/fonts/noto/NotoSansSymbols-Regular.ttf"61 "/usr/share/fonts/noto/NotoSansSymbols-Regular.ttf"
57- "/usr/share/fonts/truetype/noto/NotoSansSymbols2-Regular.ttf"])62+ "/usr/share/fonts/truetype/noto/NotoSansSymbols2-Regular.ttf"
63+ "/usr/share/fonts/truetype/noto/NotoSansSymbols-Regular.ttf"
64+ ;; Last, and only where SDL_ttf can scale it.
65+ "/usr/share/fonts/noto/NotoColorEmoji.ttf"
66+ "/usr/share/fonts/truetype/noto/NotoColorEmoji.ttf"])
58 67
59 (defn- fallback-paths []68 (defn- fallback-paths []
60 (if-let [env (System/getenv "JVUI_FALLBACK_FONTS")]69 (if-let [env (System/getenv "JVUI_FALLBACK_FONTS")]
@@ -76,12 +85,26 @@
76 ;; Fallbacks are per FACE, not per family, so each size opens its85 ;; Fallbacks are per FACE, not per family, so each size opens its
77 ;; own and they are kept for as long as the face is — SDL_ttf86 ;; own and they are kept for as long as the face is — SDL_ttf
78 ;; holds the pointer and does not copy the font.87 ;; holds the pointer and does not copy the font.
79- (let [fbs (into []88+ (let [want (sdl/font-height f)
89+ fbs (into []
80 (keep (fn [path]90 (keep (fn [path]
81 (let [g (sdl/open-font path (float size))]91 (let [g (sdl/open-font path (float size))]
82 (when-not (ffi-null? g)92 (when-not (ffi-null? g)
83- (sdl/add-fallback-font! f g)93+ ;; A face that answers a size it was
84- g))))94+ ;; not asked for cannot be used. A
95+ ;; colour emoji font is a bitmap with
96+ ;; one strike — ask for 14 and it
97+ ;; still gives 128 — and SDL_ttf draws
98+ ;; it at that, so one emoji in a
99+ ;; message makes a row a hundred and
100+ ;; twenty-eight pixels tall and pushes
101+ ;; the rest of the window out of the
102+ ;; way. Twice the face's own height is
103+ ;; the line: a symbol face runs a
104+ ;; little tall and is fine.
105+ (if (> (sdl/font-height g) (* 2 want))
106+ (do (sdl/close-font! g) nil)
107+ (do (sdl/add-fallback-font! f g) g))))))
85 (fallback-paths))]108 (fallback-paths))]
86 (swap! cache update :fallbacks (fnil into []) fbs))109 (swap! cache update :fallbacks (fnil into []) fbs))
87 (swap! cache assoc-in [:faces k] f)110 (swap! cache assoc-in [:faces k] f)
added jvui/test/jvui/font_check.clj +32 -0
new file mode 100644
@@ -0,0 +1,32 @@
1+(ns jvui.font-check
2+ "A fallback face must not change the line.
3+
4+ The headless tests measure with a stub — eight pixels a character — so
5+ they cannot see this at all. What it guards is the thing that made the
6+ compose bar a hundred and forty pixels tall: a colour emoji font is a
7+ bitmap with one fixed strike, and asked for fourteen it answers a
8+ hundred and twenty-eight. SDL_ttf then draws it at that, so one emoji
9+ in a message reshapes the window around it.
10+
11+ Needs the real fonts, which is why it is not in the headless set."
12+ (:require [jvui.sdl :as sdl] [jvui.font :as font]))
13+
14+(defn -main [& _]
15+ (sdl/ensure-loaded!)
16+ (sdl/ttf-init!)
17+ (let [c (font/open (font/find-font))
18+ h (font/line-height c 14)
19+ out (atom [])
20+ ck! (fn [n ok?] (swap! out conj [n (boolean ok?)]))]
21+ (ck! "the face has a sane line height" (< 8 h 40))
22+ ;; Each of these goes to a fallback on most systems. Whatever answers,
23+ ;; it must answer at the line the text sits on.
24+ (doseq [[what s] [["an arrow" "←"] ["a picture glyph" "🖼"] ["plain text" "Chats"]]]
25+ (let [[w gh] (font/size-of c s 14)]
26+ (ck! (str what " measures on the line")
27+ (and (pos? w) (<= gh (* 2 h))))))
28+ (doseq [[n ok?] @out] (println (if ok? "- " "FAIL ") n))
29+ (let [bad (remove second @out)]
30+ (println (if (seq bad) (str (count bad) " of " (count @out) " checks FAILED")
31+ (str "all " (count @out) " checks passed")))
32+ (when (seq bad) (System/exit 1)))))
new file mode 100644
@@ -0,0 +1,32 @@
1+(ns jvui.font-check
2+ "A fallback face must not change the line.
3+
4+ The headless tests measure with a stub — eight pixels a character — so
5+ they cannot see this at all. What it guards is the thing that made the
6+ compose bar a hundred and forty pixels tall: a colour emoji font is a
7+ bitmap with one fixed strike, and asked for fourteen it answers a
8+ hundred and twenty-eight. SDL_ttf then draws it at that, so one emoji
9+ in a message reshapes the window around it.
10+
11+ Needs the real fonts, which is why it is not in the headless set."
12+ (:require [jvui.sdl :as sdl] [jvui.font :as font]))
13+
14+(defn -main [& _]
15+ (sdl/ensure-loaded!)
16+ (sdl/ttf-init!)
17+ (let [c (font/open (font/find-font))
18+ h (font/line-height c 14)
19+ out (atom [])
20+ ck! (fn [n ok?] (swap! out conj [n (boolean ok?)]))]
21+ (ck! "the face has a sane line height" (< 8 h 40))
22+ ;; Each of these goes to a fallback on most systems. Whatever answers,
23+ ;; it must answer at the line the text sits on.
24+ (doseq [[what s] [["an arrow" "←"] ["a picture glyph" "🖼"] ["plain text" "Chats"]]]
25+ (let [[w gh] (font/size-of c s 14)]
26+ (ck! (str what " measures on the line")
27+ (and (pos? w) (<= gh (* 2 h))))))
28+ (doseq [[n ok?] @out] (println (if ok? "- " "FAIL ") n))
29+ (let [bad (remove second @out)]
30+ (println (if (seq bad) (str (count bad) " of " (count @out) " checks FAILED")
31+ (str "all " (count @out) " checks passed")))
32+ (when (seq bad) (System/exit 1)))))