nandi/jolt-nativepublic Fork 0
5145c3b
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.

Draw emoji in colour, and keep the scrollbar off the words

Two things a bitmap font forced, and one that follows from the first.

A colour emoji face cannot be a fallback font. NotoColorEmoji is bitmap
only — a single 128-pixel strike — so SDL_ttf answers 128 to every size it
is asked for, measurement as much as drawing, and one emoji in a line makes
that line a hundred and twenty-eight pixels tall. `usable?` refuses it for
that reason, which left the monochrome outline face as the only emoji
anything, and a machine without one drawing tofu.

So colour goes down a different path: rendered once at the size the face
insists on, kept as a texture, and blitted into whatever box the layout
gave the glyph. The scale happens on the GPU, where the size the face will
not do costs nothing. `emoji` and `reaction` take that path when there is a
colour face and stay exactly as they were when there is not — which is also
what a headless walk gets, having no painter and so no colour anything.
`colour-emoji?` is answered off the painter rather than off `drawing?` for
that reason: a glyph that will be a square picture has to be MEASURED as
one, three walks before it is drawn.

`reaction` measures and draws its glyph apart from its tally now. A picture
cannot be concatenated onto a number.

And the scrollbar stops sitting on the text. It is painted over the
viewport's right edge, so the last characters of every wrapped line were
under it — the conversation list where it showed worst, a card's worth of
text ending in a grey bar. `box*` grows a `:pad-right`: a strip along the
right the children are not given, taken off the content and not off the
box, so the bar is drawn inside the box's own edge. A scroll always
reserves it, showing the bar or not — reserving only when the bar appears
means every line rewraps under the reader the moment a list grows past the
window.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-09-10T10:25:08-07:00 Browse files
5145c3b parent: 28dc136
modified jvui/src/jvui/core.clj +27 -4
@@ -128,6 +128,21 @@
128128 (when (and (drawing?) tex)
129129 (paint/frame! (:painter (ui)) tex tw th rect)))
130130
131+(defn colour-emoji?
132+ "Whether `s` would be drawn as a colour picture rather than as text.
133+
134+ Answered off the painter and not off `drawing?`, so the layout passes and
135+ the paint pass agree: a glyph that will be a square picture has to be
136+ MEASURED as one, three walks before it is drawn. A headless walk has no
137+ painter and no colour anything, and answers false."
138+ [s]
139+ (boolean (when-let [p (:painter (ui))] (paint/colour-emoji? p s))))
140+
141+(defn draw-emoji!
142+ "Paint `s` as a colour picture in `rect`, if there is one."
143+ [s rect]
144+ (when (drawing?) (paint/emoji! (:painter (ui)) s rect)))
145+
131146 (defn draw-frame!
132147 "Paint feed `key`'s latest picture into `rect`, if it has one."
133148 [key rect]
@@ -366,7 +381,7 @@
366381 child on a line of its own."
367382 [opts body]
368383 (let [{:keys [dir spacing padding margin expand gravity key fill border
369- radius min-size clip? offset fixed wrap]
384+ radius min-size clip? offset fixed wrap pad-right]
370385 :or {dir :vertical expand :none gravity [0.0 0.0] clip? false}} opts
371386 spacing (double (or spacing 0.0))
372387 padding (double (or padding 0.0))
@@ -385,8 +400,13 @@
385400 [ox oy ow oh] outer
386401 box-rect [(+ ox margin) (+ oy margin)
387402 (- ow (* 2 margin)) (- oh (* 2 margin))]
403+ ;; A strip along the right the children never get, on top of the
404+ ;; padding: it is where a scrollbar goes. Only the CONTENT loses
405+ ;; it — the box keeps its rectangle, so the bar is drawn inside the
406+ ;; box's own edge rather than beyond it.
407+ pad-right (double (or pad-right 0.0))
388408 content [(+ (first box-rect) padding) (+ (second box-rect) padding)
389- (max 0.0 (- (nth box-rect 2) (* 2 padding)))
409+ (max 0.0 (- (nth box-rect 2) (* 2 padding) pad-right))
390410 (max 0.0 (- (nth box-rect 3) (* 2 padding)))]
391411 ;; Extra space to hand out along our own axis, computed from what we
392412 ;; counted last frame: whatever the fixed children took is spoken for,
@@ -419,9 +439,12 @@
419439 ;; the run of them; this swap is the only place that distinction is spelled
420440 ;; out.
421441 (let [pad2 (+ (* 2 padding) (* 2 margin))
442+ ;; The gutter counts towards the WIDTH the box needs and nothing
443+ ;; else: it is a strip the children were not given, so a box
444+ ;; wide enough for them is not wide enough for them and it.
422445 computed (if (= dir :vertical)
423- [(+ (ctr f CROSS) pad2) (+ (ctr f ALONG) pad2)]
424- [(+ (ctr f ALONG) pad2) (+ (ctr f CROSS) pad2)])
446+ [(+ (ctr f CROSS) pad2 pad-right) (+ (ctr f ALONG) pad2)]
447+ [(+ (ctr f ALONG) pad2 pad-right) (+ (ctr f CROSS) pad2)])
425448 computed (if min-size
426449 [(max (first computed) (first min-size))
427450 (max (second computed) (second min-size))]
@@ -128,6 +128,21 @@
128 (when (and (drawing?) tex)128 (when (and (drawing?) tex)
129 (paint/frame! (:painter (ui)) tex tw th rect)))129 (paint/frame! (:painter (ui)) tex tw th rect)))
130 130
131+(defn colour-emoji?
132+ "Whether `s` would be drawn as a colour picture rather than as text.
133+
134+ Answered off the painter and not off `drawing?`, so the layout passes and
135+ the paint pass agree: a glyph that will be a square picture has to be
136+ MEASURED as one, three walks before it is drawn. A headless walk has no
137+ painter and no colour anything, and answers false."
138+ [s]
139+ (boolean (when-let [p (:painter (ui))] (paint/colour-emoji? p s))))
140+
141+(defn draw-emoji!
142+ "Paint `s` as a colour picture in `rect`, if there is one."
143+ [s rect]
144+ (when (drawing?) (paint/emoji! (:painter (ui)) s rect)))
145+
131 (defn draw-frame!146 (defn draw-frame!
132 "Paint feed `key`'s latest picture into `rect`, if it has one."147 "Paint feed `key`'s latest picture into `rect`, if it has one."
133 [key rect]148 [key rect]
@@ -366,7 +381,7 @@
366 child on a line of its own."381 child on a line of its own."
367 [opts body]382 [opts body]
368 (let [{:keys [dir spacing padding margin expand gravity key fill border383 (let [{:keys [dir spacing padding margin expand gravity key fill border
369- radius min-size clip? offset fixed wrap]384+ radius min-size clip? offset fixed wrap pad-right]
370 :or {dir :vertical expand :none gravity [0.0 0.0] clip? false}} opts385 :or {dir :vertical expand :none gravity [0.0 0.0] clip? false}} opts
371 spacing (double (or spacing 0.0))386 spacing (double (or spacing 0.0))
372 padding (double (or padding 0.0))387 padding (double (or padding 0.0))
@@ -385,8 +400,13 @@
385 [ox oy ow oh] outer400 [ox oy ow oh] outer
386 box-rect [(+ ox margin) (+ oy margin)401 box-rect [(+ ox margin) (+ oy margin)
387 (- ow (* 2 margin)) (- oh (* 2 margin))]402 (- ow (* 2 margin)) (- oh (* 2 margin))]
403+ ;; A strip along the right the children never get, on top of the
404+ ;; padding: it is where a scrollbar goes. Only the CONTENT loses
405+ ;; it — the box keeps its rectangle, so the bar is drawn inside the
406+ ;; box's own edge rather than beyond it.
407+ pad-right (double (or pad-right 0.0))
388 content [(+ (first box-rect) padding) (+ (second box-rect) padding)408 content [(+ (first box-rect) padding) (+ (second box-rect) padding)
389- (max 0.0 (- (nth box-rect 2) (* 2 padding)))409+ (max 0.0 (- (nth box-rect 2) (* 2 padding) pad-right))
390 (max 0.0 (- (nth box-rect 3) (* 2 padding)))]410 (max 0.0 (- (nth box-rect 3) (* 2 padding)))]
391 ;; Extra space to hand out along our own axis, computed from what we411 ;; Extra space to hand out along our own axis, computed from what we
392 ;; counted last frame: whatever the fixed children took is spoken for,412 ;; counted last frame: whatever the fixed children took is spoken for,
@@ -419,9 +439,12 @@
419 ;; the run of them; this swap is the only place that distinction is spelled439 ;; the run of them; this swap is the only place that distinction is spelled
420 ;; out.440 ;; out.
421 (let [pad2 (+ (* 2 padding) (* 2 margin))441 (let [pad2 (+ (* 2 padding) (* 2 margin))
442+ ;; The gutter counts towards the WIDTH the box needs and nothing
443+ ;; else: it is a strip the children were not given, so a box
444+ ;; wide enough for them is not wide enough for them and it.
422 computed (if (= dir :vertical)445 computed (if (= dir :vertical)
423- [(+ (ctr f CROSS) pad2) (+ (ctr f ALONG) pad2)]446+ [(+ (ctr f CROSS) pad2 pad-right) (+ (ctr f ALONG) pad2)]
424- [(+ (ctr f ALONG) pad2) (+ (ctr f CROSS) pad2)])447+ [(+ (ctr f ALONG) pad2 pad-right) (+ (ctr f CROSS) pad2)])
425 computed (if min-size448 computed (if min-size
426 [(max (first computed) (first min-size))449 [(max (first computed) (first min-size))
427 (max (second computed) (second min-size))]450 (max (second computed) (second min-size))]
modified jvui/src/jvui/font.clj +94 -0
@@ -70,11 +70,103 @@
7070 (remove clojure.string/blank? (clojure.string/split env #":"))
7171 (filter #(.exists (java.io.File. %)) fallbacks)))
7272
73+(def ^:private colour-faces
74+ "Colour emoji faces, for the picture path below rather than as fallbacks.
75+
76+ JVUI_COLOUR_EMOJI_FONT overrides, and an empty value turns colour off —
77+ which is the way to get the monochrome fallback back on a machine that
78+ has both."
79+ ["/usr/share/fonts/noto/NotoColorEmoji.ttf"
80+ "/usr/share/fonts/truetype/noto/NotoColorEmoji.ttf"
81+ "/usr/share/fonts/noto/NotoColorEmoji-Regular.ttf"
82+ "/System/Library/Fonts/Apple Color Emoji.ttc"])
83+
84+(defn- colour-path []
85+ (if-let [env (System/getenv "JVUI_COLOUR_EMOJI_FONT")]
86+ (when (seq env) env)
87+ (first (filter #(.exists (java.io.File. %)) colour-faces))))
88+
7389 (defn open
7490 "A font cache over `path`. Sizes are opened lazily and kept."
7591 [path]
7692 (atom {:path path :faces {} :sizes {} :textures {}}))
7793
94+;; --- colour emoji ------------------------------------------------------------
95+;;
96+;; Not through the fallback list, and it cannot be: a colour emoji face is a
97+;; BITMAP with one strike — NotoColorEmoji has a single 128-pixel one — so
98+;; SDL_ttf answers 128 to every size it is asked for, measurement as much as
99+;; drawing. Added as a fallback it makes any line holding an emoji a hundred
100+;; and twenty-eight pixels tall, which is what `usable?` above refuses.
101+;;
102+;; So it is drawn as a PICTURE instead: rendered once at the size it insists
103+;; on, kept as a texture, and blitted into whatever box the layout gave the
104+;; glyph. The scale happens on the GPU at draw time, where the size the face
105+;; will not do costs nothing. The monochrome fallback stays as it is and
106+;; answers everything this has no glyph for.
107+
108+(defn- colour-face
109+ "The colour emoji face, opened once, or nil where there is none.
110+
111+ `:colour` is a three-state cell: absent means not looked for, false means
112+ looked for and not found. Without the false a machine with no colour face
113+ re-opens nothing but re-walks the candidate list on every glyph."
114+ [cache]
115+ (let [v (:colour @cache)]
116+ (if (some? v)
117+ (or v nil)
118+ (let [path (colour-path)
119+ f (when path (sdl/open-font path (float 32)))
120+ f (when-not (ffi-null? f) f)]
121+ (swap! cache assoc :colour (or f false))
122+ f))))
123+
124+(defn- ignorable?
125+ "Codepoints an emoji carries that no face is expected to have a glyph for:
126+ the variation selectors that ask for emoji presentation, the zero-width
127+ joiner that binds a sequence, and the tag characters flags are spelled
128+ with."
129+ [cp]
130+ (or (= cp 0x200D) (<= 0xFE00 cp 0xFE0F) (<= 0xE0020 cp 0xE007F)))
131+
132+(defn colour-glyph?
133+ "Whether the colour face can draw `s` — every codepoint in it that is a
134+ glyph at all.
135+
136+ Every one, not the first: taking the first would claim a family the face
137+ has only the man of, and draw it as a man."
138+ [cache s]
139+ (boolean
140+ (when-let [f (and (seq s) (colour-face cache))]
141+ ;; `seq` over the codepoints, not over UTF-16: a jolt string is
142+ ;; Scheme's, whose characters are whole scalars, so there are no
143+ ;; surrogate halves here to pair up. frq.glyphs walks emoji the same
144+ ;; way.
145+ (let [cps (remove ignorable? (map int (seq s)))]
146+ (and (seq cps) (every? #(sdl/font-has-glyph? f (int %)) cps))))))
147+
148+(defn colour-texture
149+ "An SDL texture of `s` in colour, and its [w h], cached. nil where the
150+ colour face cannot draw it.
151+
152+ One texture per string rather than per string-and-size, because the face
153+ has one size: the caller scales it into the rectangle the layout gave."
154+ [cache renderer s]
155+ (when (colour-glyph? cache s)
156+ (or (get-in @cache [:colour-textures s])
157+ (let [surf (sdl/render-blended (colour-face cache) s)]
158+ (when-not (ffi-null? surf)
159+ (let [tex (sdl/texture-from-surface renderer surf)
160+ wh (sdl/surface-size surf)]
161+ (sdl/destroy-surface! surf)
162+ (sdl/texture-blend-mode! tex sdl/BLEND)
163+ ;; Linear, because this texture is only ever drawn scaled DOWN
164+ ;; — 128 pixels into a sixteen-pixel line — and nearest at that
165+ ;; ratio is a handful of the original pixels and looks it.
166+ (sdl/texture-scale-mode! tex sdl/SCALE-LINEAR)
167+ (swap! cache assoc-in [:colour-textures s] [tex wh])
168+ [tex wh]))))))
169+
78170 (defn- face
79171 [cache size]
80172 (let [k (int size)]
@@ -140,5 +232,7 @@
140232 (defn close!
141233 [cache]
142234 (doseq [[_ [tex _]] (:textures @cache)] (sdl/destroy-texture! tex))
235+ (doseq [[_ [tex _]] (:colour-textures @cache)] (sdl/destroy-texture! tex))
143236 (doseq [[_ f] (:faces @cache)] (sdl/close-font! f))
237+ (when-let [f (:colour @cache)] (sdl/close-font! f))
144238 (reset! cache {:path (:path @cache) :faces {} :sizes {} :textures {}}))
@@ -70,11 +70,103 @@
70 (remove clojure.string/blank? (clojure.string/split env #":"))70 (remove clojure.string/blank? (clojure.string/split env #":"))
71 (filter #(.exists (java.io.File. %)) fallbacks)))71 (filter #(.exists (java.io.File. %)) fallbacks)))
72 72
73+(def ^:private colour-faces
74+ "Colour emoji faces, for the picture path below rather than as fallbacks.
75+
76+ JVUI_COLOUR_EMOJI_FONT overrides, and an empty value turns colour off —
77+ which is the way to get the monochrome fallback back on a machine that
78+ has both."
79+ ["/usr/share/fonts/noto/NotoColorEmoji.ttf"
80+ "/usr/share/fonts/truetype/noto/NotoColorEmoji.ttf"
81+ "/usr/share/fonts/noto/NotoColorEmoji-Regular.ttf"
82+ "/System/Library/Fonts/Apple Color Emoji.ttc"])
83+
84+(defn- colour-path []
85+ (if-let [env (System/getenv "JVUI_COLOUR_EMOJI_FONT")]
86+ (when (seq env) env)
87+ (first (filter #(.exists (java.io.File. %)) colour-faces))))
88+
73 (defn open89 (defn open
74 "A font cache over `path`. Sizes are opened lazily and kept."90 "A font cache over `path`. Sizes are opened lazily and kept."
75 [path]91 [path]
76 (atom {:path path :faces {} :sizes {} :textures {}}))92 (atom {:path path :faces {} :sizes {} :textures {}}))
77 93
94+;; --- colour emoji ------------------------------------------------------------
95+;;
96+;; Not through the fallback list, and it cannot be: a colour emoji face is a
97+;; BITMAP with one strike — NotoColorEmoji has a single 128-pixel one — so
98+;; SDL_ttf answers 128 to every size it is asked for, measurement as much as
99+;; drawing. Added as a fallback it makes any line holding an emoji a hundred
100+;; and twenty-eight pixels tall, which is what `usable?` above refuses.
101+;;
102+;; So it is drawn as a PICTURE instead: rendered once at the size it insists
103+;; on, kept as a texture, and blitted into whatever box the layout gave the
104+;; glyph. The scale happens on the GPU at draw time, where the size the face
105+;; will not do costs nothing. The monochrome fallback stays as it is and
106+;; answers everything this has no glyph for.
107+
108+(defn- colour-face
109+ "The colour emoji face, opened once, or nil where there is none.
110+
111+ `:colour` is a three-state cell: absent means not looked for, false means
112+ looked for and not found. Without the false a machine with no colour face
113+ re-opens nothing but re-walks the candidate list on every glyph."
114+ [cache]
115+ (let [v (:colour @cache)]
116+ (if (some? v)
117+ (or v nil)
118+ (let [path (colour-path)
119+ f (when path (sdl/open-font path (float 32)))
120+ f (when-not (ffi-null? f) f)]
121+ (swap! cache assoc :colour (or f false))
122+ f))))
123+
124+(defn- ignorable?
125+ "Codepoints an emoji carries that no face is expected to have a glyph for:
126+ the variation selectors that ask for emoji presentation, the zero-width
127+ joiner that binds a sequence, and the tag characters flags are spelled
128+ with."
129+ [cp]
130+ (or (= cp 0x200D) (<= 0xFE00 cp 0xFE0F) (<= 0xE0020 cp 0xE007F)))
131+
132+(defn colour-glyph?
133+ "Whether the colour face can draw `s` — every codepoint in it that is a
134+ glyph at all.
135+
136+ Every one, not the first: taking the first would claim a family the face
137+ has only the man of, and draw it as a man."
138+ [cache s]
139+ (boolean
140+ (when-let [f (and (seq s) (colour-face cache))]
141+ ;; `seq` over the codepoints, not over UTF-16: a jolt string is
142+ ;; Scheme's, whose characters are whole scalars, so there are no
143+ ;; surrogate halves here to pair up. frq.glyphs walks emoji the same
144+ ;; way.
145+ (let [cps (remove ignorable? (map int (seq s)))]
146+ (and (seq cps) (every? #(sdl/font-has-glyph? f (int %)) cps))))))
147+
148+(defn colour-texture
149+ "An SDL texture of `s` in colour, and its [w h], cached. nil where the
150+ colour face cannot draw it.
151+
152+ One texture per string rather than per string-and-size, because the face
153+ has one size: the caller scales it into the rectangle the layout gave."
154+ [cache renderer s]
155+ (when (colour-glyph? cache s)
156+ (or (get-in @cache [:colour-textures s])
157+ (let [surf (sdl/render-blended (colour-face cache) s)]
158+ (when-not (ffi-null? surf)
159+ (let [tex (sdl/texture-from-surface renderer surf)
160+ wh (sdl/surface-size surf)]
161+ (sdl/destroy-surface! surf)
162+ (sdl/texture-blend-mode! tex sdl/BLEND)
163+ ;; Linear, because this texture is only ever drawn scaled DOWN
164+ ;; — 128 pixels into a sixteen-pixel line — and nearest at that
165+ ;; ratio is a handful of the original pixels and looks it.
166+ (sdl/texture-scale-mode! tex sdl/SCALE-LINEAR)
167+ (swap! cache assoc-in [:colour-textures s] [tex wh])
168+ [tex wh]))))))
169+
78 (defn- face170 (defn- face
79 [cache size]171 [cache size]
80 (let [k (int size)]172 (let [k (int size)]
@@ -140,5 +232,7 @@
140 (defn close!232 (defn close!
141 [cache]233 [cache]
142 (doseq [[_ [tex _]] (:textures @cache)] (sdl/destroy-texture! tex))234 (doseq [[_ [tex _]] (:textures @cache)] (sdl/destroy-texture! tex))
235+ (doseq [[_ [tex _]] (:colour-textures @cache)] (sdl/destroy-texture! tex))
143 (doseq [[_ f] (:faces @cache)] (sdl/close-font! f))236 (doseq [[_ f] (:faces @cache)] (sdl/close-font! f))
237+ (when-let [f (:colour @cache)] (sdl/close-font! f))
144 (reset! cache {:path (:path @cache) :faces {} :sizes {} :textures {}}))238 (reset! cache {:path (:path @cache) :faces {} :sizes {} :textures {}}))
modified jvui/src/jvui/paint.clj +14 -0
@@ -150,6 +150,20 @@
150150 (flush! p)
151151 (sdl/blit! r tex nil [(+ x (/ (- w dw) 2.0)) (+ y (/ (- h dh) 2.0)) dw dh])))
152152
153+(defn colour-emoji?
154+ "Whether `s` has a colour picture to be drawn as."
155+ [p s]
156+ (font/colour-glyph? (:fonts @p) s))
157+
158+(defn emoji!
159+ "Draw `s` as a colour picture in `rect`. Answers whether it did — a caller
160+ that gets false draws it as text instead."
161+ [p s rect]
162+ (let [{:keys [r fonts]} @p]
163+ (if-let [[tex [tw th]] (font/colour-texture fonts r s)]
164+ (do (frame! p tex tw th rect) true)
165+ false)))
166+
153167 (defn line!
154168 "A `width`-thick line. Axis-aligned lines are a rectangle; the diagonal case
155169 which in this toolkit is a checkbox tick is a few offset hairlines,
@@ -150,6 +150,20 @@
150 (flush! p)150 (flush! p)
151 (sdl/blit! r tex nil [(+ x (/ (- w dw) 2.0)) (+ y (/ (- h dh) 2.0)) dw dh])))151 (sdl/blit! r tex nil [(+ x (/ (- w dw) 2.0)) (+ y (/ (- h dh) 2.0)) dw dh])))
152 152
153+(defn colour-emoji?
154+ "Whether `s` has a colour picture to be drawn as."
155+ [p s]
156+ (font/colour-glyph? (:fonts @p) s))
157+
158+(defn emoji!
159+ "Draw `s` as a colour picture in `rect`. Answers whether it did — a caller
160+ that gets false draws it as text instead."
161+ [p s rect]
162+ (let [{:keys [r fonts]} @p]
163+ (if-let [[tex [tw th]] (font/colour-texture fonts r s)]
164+ (do (frame! p tex tw th rect) true)
165+ false)))
166+
153 (defn line!167 (defn line!
154 "A `width`-thick line. Axis-aligned lines are a rectangle; the diagonal case168 "A `width`-thick line. Axis-aligned lines are a rectangle; the diagonal case
155 which in this toolkit is a checkbox tick is a few offset hairlines,169 which in this toolkit is a checkbox tick is a few offset hairlines,
modified jvui/src/jvui/sdl.clj +1 -0
@@ -70,6 +70,7 @@
7070 ;; A face to try for characters the main one has no glyph for. SDL_ttf keeps
7171 ;; a list of them and asks each in turn.
7272 (ffi/defcfn add-fallback-font! "TTF_AddFallbackFont" [:pointer :pointer] :bool)
73+(ffi/defcfn font-has-glyph? "TTF_FontHasGlyph" [:pointer :uint] :bool)
7374 (ffi/defcfn ^:private raw-string-size "TTF_GetStringSize"
7475 [:pointer :string :ulong :pointer :pointer] :bool)
7576 (ffi/defcfn ^:private raw-render-blended "TTF_RenderText_Blended"
@@ -70,6 +70,7 @@
70 ;; A face to try for characters the main one has no glyph for. SDL_ttf keeps70 ;; A face to try for characters the main one has no glyph for. SDL_ttf keeps
71 ;; a list of them and asks each in turn.71 ;; a list of them and asks each in turn.
72 (ffi/defcfn add-fallback-font! "TTF_AddFallbackFont" [:pointer :pointer] :bool)72 (ffi/defcfn add-fallback-font! "TTF_AddFallbackFont" [:pointer :pointer] :bool)
73+(ffi/defcfn font-has-glyph? "TTF_FontHasGlyph" [:pointer :uint] :bool)
73 (ffi/defcfn ^:private raw-string-size "TTF_GetStringSize"74 (ffi/defcfn ^:private raw-string-size "TTF_GetStringSize"
74 [:pointer :string :ulong :pointer :pointer] :bool)75 [:pointer :string :ulong :pointer :pointer] :bool)
75 (ffi/defcfn ^:private raw-render-blended "TTF_RenderText_Blended"76 (ffi/defcfn ^:private raw-render-blended "TTF_RenderText_Blended"
modified jvui/src/jvui/widgets.clj +48 -14
@@ -473,8 +473,18 @@
473473 jump? maxoff
474474 stick? maxoff
475475 :else wheeled)
476+ ;; The gutter the bar is drawn in, taken off the content whether or
477+ ;; not there is anything to scroll yet. Always, because the
478+ ;; alternative is that a list reflows the moment it grows past the
479+ ;; window — every line rewrapping under the reader as the message
480+ ;; that overflowed arrives. And taken at all because the bar is
481+ ;; painted over the viewport's right edge: without it the last
482+ ;; characters of every wrapped line sit under the bar, which is
483+ ;; where "the sidebar truncates" comes from.
484+ gutter (double (c/th :scrollbar))
476485 r (c/box* (merge {:dir :vertical
477486 :expand :both
487+ :pad-right gutter
478488 ;; An explicit :height IS a demand and is
479489 ;; reported as one. Without it the viewport
480490 ;; fills what is left and asks for nothing —
@@ -623,14 +633,23 @@
623633 "One emoji, drawn as a character and sized to sit level with the words
624634 either side.
625635
626- Through the text font, which is where this differs from libvidya: that
627- draws from an image pack of its own, and this asks SDL_ttf for the glyph.
628- On a system whose UI font has no emoji coverage — most of them — what
629- appears is the missing-glyph box. The fix is a font with the coverage,
630- not code here, which is why JVUI_FONT exists."
636+ In colour where there is a colour face to draw it from, and as text
637+ where there is not.
638+
639+ The two paths differ in more than the picture: a colour emoji face is a
640+ bitmap with one 128-pixel strike, so it cannot be a fallback font and is
641+ drawn as an image scaled into the line which means the glyph occupies a
642+ SQUARE of the size asked for, decided here rather than measured. The text
643+ path stays exactly what it was, a label in the UI font, and is what a
644+ machine with no colour face gets."
631645 ([s] (emoji s nil))
632646 ([s size]
633- (label s {:size (or size (c/th :font-size))})))
647+ (let [sz (double (or size (c/th :font-size)))]
648+ (if (c/colour-emoji? s)
649+ (let [rect (c/leaf [sz sz] :none [0.5 0.5])]
650+ (c/draw-emoji! s rect)
651+ rect)
652+ (label s {:size sz})))))
634653
635654 (defn avatar
636655 "A round picture for somebody, or their initial on a colour if there is
@@ -660,16 +679,25 @@
660679 "A tally wearing a pill: an emoji, how many people, and whether you are one
661680 of them.
662681
663- The same glyph `emoji` draws, but a reaction is not a character — it
664- answers the pointer and it is a count. `mine?` is bordered rather than
665- filled differently, because the pill has to stay readable at the size a
666- line of them ends up."
682+ The same glyph `emoji` draws, and the same two ways of drawing it: a
683+ colour face gives a square picture, and everything else is text. So the
684+ glyph and the tally are measured and drawn apart rather than as one
685+ string a picture cannot be concatenated onto a number.
686+
687+ A reaction is not a character otherwise either: it answers the pointer
688+ and it is a count. `mine?` is bordered rather than filled differently,
689+ because the pill has to stay readable at the size a line of them ends
690+ up."
667691 ([glyph] (reaction glyph {}))
668692 ([glyph {:keys [count mine? size key]
669693 :or {count 0 mine? false}}]
670- (let [sz (or size (c/th :font-size))
671- txt (if (pos? count) (str glyph " " count) (str glyph))
672- [tw th*] (c/measure txt sz)
694+ (let [sz (double (or size (c/th :font-size)))
695+ colour? (c/colour-emoji? (str glyph))
696+ tally (when (pos? count) (str " " count))
697+ [gw gh] (if colour? [sz sz] (c/measure (str glyph) sz))
698+ [cw ch] (if tally (c/measure tally sz) [0.0 0.0])
699+ tw (+ (double gw) (double cw))
700+ th* (max (double gh) (double ch))
673701 pad 6.0
674702 rect (c/leaf [(+ tw (* 2 pad)) (+ th* 4.0)] :none [0.0 0.5])
675703 id (c/next-id key)
@@ -677,5 +705,11 @@
677705 (c/fill! rect (if mine? (c/th :press) (c/th :surface-alt))
678706 (/ h 2.0)
679707 (when mine? (c/th :accent)) (if mine? 1.0 0.0))
680- (c/draw-text! txt (+ x pad) (+ y (/ (- h th*) 2.0)) sz (c/th :text))
708+ (if colour?
709+ (c/draw-emoji! (str glyph) [(+ x pad) (+ y (/ (- h sz) 2.0)) sz sz])
710+ (c/draw-text! (str glyph) (+ x pad) (+ y (/ (- h (double gh)) 2.0))
711+ sz (c/th :text)))
712+ (when tally
713+ (c/draw-text! tally (+ x pad (double gw)) (+ y (/ (- h (double ch)) 2.0))
714+ sz (c/th :text)))
681715 (:clicked? (c/interact! id rect)))))
@@ -473,8 +473,18 @@
473 jump? maxoff473 jump? maxoff
474 stick? maxoff474 stick? maxoff
475 :else wheeled)475 :else wheeled)
476+ ;; The gutter the bar is drawn in, taken off the content whether or
477+ ;; not there is anything to scroll yet. Always, because the
478+ ;; alternative is that a list reflows the moment it grows past the
479+ ;; window — every line rewrapping under the reader as the message
480+ ;; that overflowed arrives. And taken at all because the bar is
481+ ;; painted over the viewport's right edge: without it the last
482+ ;; characters of every wrapped line sit under the bar, which is
483+ ;; where "the sidebar truncates" comes from.
484+ gutter (double (c/th :scrollbar))
476 r (c/box* (merge {:dir :vertical485 r (c/box* (merge {:dir :vertical
477 :expand :both486 :expand :both
487+ :pad-right gutter
478 ;; An explicit :height IS a demand and is488 ;; An explicit :height IS a demand and is
479 ;; reported as one. Without it the viewport489 ;; reported as one. Without it the viewport
480 ;; fills what is left and asks for nothing —490 ;; fills what is left and asks for nothing —
@@ -623,14 +633,23 @@
623 "One emoji, drawn as a character and sized to sit level with the words633 "One emoji, drawn as a character and sized to sit level with the words
624 either side.634 either side.
625 635
626- Through the text font, which is where this differs from libvidya: that636+ In colour where there is a colour face to draw it from, and as text
627- draws from an image pack of its own, and this asks SDL_ttf for the glyph.637+ where there is not.
628- On a system whose UI font has no emoji coverage — most of them — what638+
629- appears is the missing-glyph box. The fix is a font with the coverage,639+ The two paths differ in more than the picture: a colour emoji face is a
630- not code here, which is why JVUI_FONT exists."640+ bitmap with one 128-pixel strike, so it cannot be a fallback font and is
641+ drawn as an image scaled into the line which means the glyph occupies a
642+ SQUARE of the size asked for, decided here rather than measured. The text
643+ path stays exactly what it was, a label in the UI font, and is what a
644+ machine with no colour face gets."
631 ([s] (emoji s nil))645 ([s] (emoji s nil))
632 ([s size]646 ([s size]
633- (label s {:size (or size (c/th :font-size))})))647+ (let [sz (double (or size (c/th :font-size)))]
648+ (if (c/colour-emoji? s)
649+ (let [rect (c/leaf [sz sz] :none [0.5 0.5])]
650+ (c/draw-emoji! s rect)
651+ rect)
652+ (label s {:size sz})))))
634 653
635 (defn avatar654 (defn avatar
636 "A round picture for somebody, or their initial on a colour if there is655 "A round picture for somebody, or their initial on a colour if there is
@@ -660,16 +679,25 @@
660 "A tally wearing a pill: an emoji, how many people, and whether you are one679 "A tally wearing a pill: an emoji, how many people, and whether you are one
661 of them.680 of them.
662 681
663- The same glyph `emoji` draws, but a reaction is not a character — it682+ The same glyph `emoji` draws, and the same two ways of drawing it: a
664- answers the pointer and it is a count. `mine?` is bordered rather than683+ colour face gives a square picture, and everything else is text. So the
665- filled differently, because the pill has to stay readable at the size a684+ glyph and the tally are measured and drawn apart rather than as one
666- line of them ends up."685+ string a picture cannot be concatenated onto a number.
686+
687+ A reaction is not a character otherwise either: it answers the pointer
688+ and it is a count. `mine?` is bordered rather than filled differently,
689+ because the pill has to stay readable at the size a line of them ends
690+ up."
667 ([glyph] (reaction glyph {}))691 ([glyph] (reaction glyph {}))
668 ([glyph {:keys [count mine? size key]692 ([glyph {:keys [count mine? size key]
669 :or {count 0 mine? false}}]693 :or {count 0 mine? false}}]
670- (let [sz (or size (c/th :font-size))694+ (let [sz (double (or size (c/th :font-size)))
671- txt (if (pos? count) (str glyph " " count) (str glyph))695+ colour? (c/colour-emoji? (str glyph))
672- [tw th*] (c/measure txt sz)696+ tally (when (pos? count) (str " " count))
697+ [gw gh] (if colour? [sz sz] (c/measure (str glyph) sz))
698+ [cw ch] (if tally (c/measure tally sz) [0.0 0.0])
699+ tw (+ (double gw) (double cw))
700+ th* (max (double gh) (double ch))
673 pad 6.0701 pad 6.0
674 rect (c/leaf [(+ tw (* 2 pad)) (+ th* 4.0)] :none [0.0 0.5])702 rect (c/leaf [(+ tw (* 2 pad)) (+ th* 4.0)] :none [0.0 0.5])
675 id (c/next-id key)703 id (c/next-id key)
@@ -677,5 +705,11 @@
677 (c/fill! rect (if mine? (c/th :press) (c/th :surface-alt))705 (c/fill! rect (if mine? (c/th :press) (c/th :surface-alt))
678 (/ h 2.0)706 (/ h 2.0)
679 (when mine? (c/th :accent)) (if mine? 1.0 0.0))707 (when mine? (c/th :accent)) (if mine? 1.0 0.0))
680- (c/draw-text! txt (+ x pad) (+ y (/ (- h th*) 2.0)) sz (c/th :text))708+ (if colour?
709+ (c/draw-emoji! (str glyph) [(+ x pad) (+ y (/ (- h sz) 2.0)) sz sz])
710+ (c/draw-text! (str glyph) (+ x pad) (+ y (/ (- h (double gh)) 2.0))
711+ sz (c/th :text)))
712+ (when tally
713+ (c/draw-text! tally (+ x pad (double gw)) (+ y (/ (- h (double ch)) 2.0))
714+ sz (c/th :text)))
681 (:clicked? (c/interact! id rect)))))715 (:clicked? (c/interact! id rect)))))