Fill the window's height, and find a glyph the UI font has not got
Two things a screenshot showed. THE GLYPHS. A UI font covers Latin and stops, and frq's chrome is icons while its messages carry emoji — every one of them was the missing-glyph box. SDL_ttf keeps a list of fallback faces and asks each in turn, so Noto Color Emoji and Noto Sans Symbols are added behind the main face and text the main face already has costs nothing. Per FACE and not per family, so each size opens its own and holds them: SDL_ttf keeps the pointer rather than copying the font. THE HEIGHT, which took three wrong answers to get right and each one is worth keeping. A row is only as tall as what is in it, and frq marks the PANES with :fill-height rather than the row holding them — egui hands a horizontal layout the available height and the panes fill that, so there is nothing there to mark. Here the row has to be told, and its own children are what know: a row holding something that wants the height wants the height. Then the viewport ASKED for the height instead of filling it — a minimum of the whole column — and the column had nothing left for the separator and the compose bar, which went off the bottom of the window. It reports nothing now and takes what is left. An explicit :height is still a demand, which is what the viewport test was already saying. And its :expand was being overridden by the container default, :cross. A viewport that fills only the width asks its column for no height, is given none, and shows nothing at all — an empty backlog with a scrollbar beside it, which is exactly what the window looked like. It is the one container that always fills both ways, so it says so itself. Every one of those was found by tracing the box chain rather than by reading it: the numbers say which box stopped passing the slack on, and all three times it was not the one I would have guessed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
7d907d0 parent: 5fcbb42 modified
glimmer-backends/glimmer-jvui/src/glimmer_jvui/core.clj +29 -6 | @@ -70,9 +70,27 @@ | ||
| 70 | 70 | |
| 71 | 71 | (defn- num [v default] (if (number? v) (double v) default)) |
| 72 | 72 | |
| 73 | +(defn- fills-height? | |
| 74 | + "Does any child of `n` ask to fill the height? | |
| 75 | + | |
| 76 | + A row is only as tall as what is in it, and frq marks the PANES with | |
| 77 | + :fill-height rather than the row that holds them — egui gives a | |
| 78 | + horizontal layout the available height and the panes fill that, so | |
| 79 | + there is nothing there to mark. Here the row has to be told, and its | |
| 80 | + own children are what know: a row holding something that wants the | |
| 81 | + height wants the height. | |
| 82 | + | |
| 83 | + Asked of the tree rather than inferred from the layout, because the | |
| 84 | + layout answers a frame too late — a row that learns it should be tall | |
| 85 | + from what happened last frame is a row that is the wrong height on | |
| 86 | + the frame anybody looks at." | |
| 87 | + [n] | |
| 88 | + (boolean (some #(:fill-height (:props (deref %))) (:children (deref n))))) | |
| 89 | + | |
| 73 | 90 | (defn- box-opts |
| 74 | 91 | "The container options shared by every container tag." |
| 75 | - [props key] | |
| 92 | + ([props key] (box-opts props key false)) | |
| 93 | + ([props key fill-height?] | |
| 76 | 94 | (cond-> {:key key |
| 77 | 95 | :dir (if (= :horizontal (:orientation props)) :horizontal :vertical) |
| 78 | 96 | ;; A CONTAINER fills its parent's cross axis by default. Without |
| @@ -97,7 +115,7 @@ | ||
| 97 | 115 | ;; row it does not fill the width of is not what anyone means by it. |
| 98 | 116 | ;; The panes that do NOT ask for it stay :cross and keep their own |
| 99 | 117 | ;; size, which is what leaves the slack to be taken. |
| 100 | - (:fill-height props) (assoc :expand :both) | |
| 118 | + (or (:fill-height props) fill-height?) (assoc :expand :both) | |
| 101 | 119 | ;; A minimum, not a size: the messages column asks for one only while |
| 102 | 120 | ;; the people panel is beside it. |
| 103 | 121 | (:width-request props) |
| @@ -113,7 +131,7 @@ | ||
| 113 | 131 | (:align props) (assoc :gravity (case (:align props) |
| 114 | 132 | (:center "center") [0.0 0.5] |
| 115 | 133 | (:end "end") [0.0 1.0] |
| 116 | - [0.0 0.0])))) | |
| 134 | + [0.0 0.0]))))) | |
| 117 | 135 | |
| 118 | 136 | (defn- fire! [n k & args] |
| 119 | 137 | (when-let [f (get (:props @n) k)] (apply f args))) |
| @@ -158,7 +176,11 @@ | ||
| 158 | 176 | ;; :height is a prop frq writes and this used to drop on the floor — |
| 159 | 177 | ;; the chat did not follow new messages, and switching channels |
| 160 | 178 | ;; carried the previous one's scroll across. |
| 161 | - :scroll (w/scroll* (cond-> (box-opts props key) | |
| 179 | + ;; :expand is forced rather than left to box-opts, whose default is | |
| 180 | + ;; :cross — and a viewport that fills only the width asks its column | |
| 181 | + ;; for no height, is given none, and shows nothing at all. It is the | |
| 182 | + ;; one container that always fills both ways. | |
| 183 | + :scroll (w/scroll* (cond-> (assoc (box-opts props key) :expand :both) | |
| 162 | 184 | (:height props) |
| 163 | 185 | (assoc :height (num (:height props) 200.0)) |
| 164 | 186 | (:reserve props) |
| @@ -185,10 +207,11 @@ | ||
| 185 | 207 | (fire! n :on-change (if at-end? "end" "away"))))) |
| 186 | 208 | (emit-children! n)) |
| 187 | 209 | |
| 188 | - :hbox (c/box* (assoc (box-opts props key) :dir :horizontal) | |
| 210 | + :hbox (c/box* (assoc (box-opts props key (fills-height? n)) :dir :horizontal) | |
| 189 | 211 | (emit-children! n)) |
| 190 | 212 | |
| 191 | - (:vbox :box) (c/box* (box-opts props key) (emit-children! n)) | |
| 213 | + (:vbox :box) (c/box* (box-opts props key (fills-height? n)) | |
| 214 | + (emit-children! n)) | |
| 192 | 215 | |
| 193 | 216 | ;; ONE tag for both kinds of picture: `:feed` is live pixels pushed |
| 194 | 217 | ;; in under a name and re-uploaded as they arrive, `:src` is a file |
| @@ -70,9 +70,27 @@ | |||
| 70 | 70 | ||
| 71 | (defn- num [v default] (if (number? v) (double v) default)) | 71 | (defn- num [v default] (if (number? v) (double v) default)) |
| 72 | 72 | ||
| 73 | +(defn- fills-height? | ||
| 74 | + "Does any child of `n` ask to fill the height? | ||
| 75 | + | ||
| 76 | + A row is only as tall as what is in it, and frq marks the PANES with | ||
| 77 | + :fill-height rather than the row that holds them — egui gives a | ||
| 78 | + horizontal layout the available height and the panes fill that, so | ||
| 79 | + there is nothing there to mark. Here the row has to be told, and its | ||
| 80 | + own children are what know: a row holding something that wants the | ||
| 81 | + height wants the height. | ||
| 82 | + | ||
| 83 | + Asked of the tree rather than inferred from the layout, because the | ||
| 84 | + layout answers a frame too late — a row that learns it should be tall | ||
| 85 | + from what happened last frame is a row that is the wrong height on | ||
| 86 | + the frame anybody looks at." | ||
| 87 | + [n] | ||
| 88 | + (boolean (some #(:fill-height (:props (deref %))) (:children (deref n))))) | ||
| 89 | + | ||
| 73 | (defn- box-opts | 90 | (defn- box-opts |
| 74 | "The container options shared by every container tag." | 91 | "The container options shared by every container tag." |
| 75 | - [props key] | 92 | + ([props key] (box-opts props key false)) |
| 93 | + ([props key fill-height?] | ||
| 76 | (cond-> {:key key | 94 | (cond-> {:key key |
| 77 | :dir (if (= :horizontal (:orientation props)) :horizontal :vertical) | 95 | :dir (if (= :horizontal (:orientation props)) :horizontal :vertical) |
| 78 | ;; A CONTAINER fills its parent's cross axis by default. Without | 96 | ;; A CONTAINER fills its parent's cross axis by default. Without |
| @@ -97,7 +115,7 @@ | |||
| 97 | ;; row it does not fill the width of is not what anyone means by it. | 115 | ;; row it does not fill the width of is not what anyone means by it. |
| 98 | ;; The panes that do NOT ask for it stay :cross and keep their own | 116 | ;; The panes that do NOT ask for it stay :cross and keep their own |
| 99 | ;; size, which is what leaves the slack to be taken. | 117 | ;; size, which is what leaves the slack to be taken. |
| 100 | - (:fill-height props) (assoc :expand :both) | 118 | + (or (:fill-height props) fill-height?) (assoc :expand :both) |
| 101 | ;; A minimum, not a size: the messages column asks for one only while | 119 | ;; A minimum, not a size: the messages column asks for one only while |
| 102 | ;; the people panel is beside it. | 120 | ;; the people panel is beside it. |
| 103 | (:width-request props) | 121 | (:width-request props) |
| @@ -113,7 +131,7 @@ | |||
| 113 | (:align props) (assoc :gravity (case (:align props) | 131 | (:align props) (assoc :gravity (case (:align props) |
| 114 | (:center "center") [0.0 0.5] | 132 | (:center "center") [0.0 0.5] |
| 115 | (:end "end") [0.0 1.0] | 133 | (:end "end") [0.0 1.0] |
| 116 | - [0.0 0.0])))) | 134 | + [0.0 0.0]))))) |
| 117 | 135 | ||
| 118 | (defn- fire! [n k & args] | 136 | (defn- fire! [n k & args] |
| 119 | (when-let [f (get (:props @n) k)] (apply f args))) | 137 | (when-let [f (get (:props @n) k)] (apply f args))) |
| @@ -158,7 +176,11 @@ | |||
| 158 | ;; :height is a prop frq writes and this used to drop on the floor — | 176 | ;; :height is a prop frq writes and this used to drop on the floor — |
| 159 | ;; the chat did not follow new messages, and switching channels | 177 | ;; the chat did not follow new messages, and switching channels |
| 160 | ;; carried the previous one's scroll across. | 178 | ;; carried the previous one's scroll across. |
| 161 | - :scroll (w/scroll* (cond-> (box-opts props key) | 179 | + ;; :expand is forced rather than left to box-opts, whose default is |
| 180 | + ;; :cross — and a viewport that fills only the width asks its column | ||
| 181 | + ;; for no height, is given none, and shows nothing at all. It is the | ||
| 182 | + ;; one container that always fills both ways. | ||
| 183 | + :scroll (w/scroll* (cond-> (assoc (box-opts props key) :expand :both) | ||
| 162 | (:height props) | 184 | (:height props) |
| 163 | (assoc :height (num (:height props) 200.0)) | 185 | (assoc :height (num (:height props) 200.0)) |
| 164 | (:reserve props) | 186 | (:reserve props) |
| @@ -185,10 +207,11 @@ | |||
| 185 | (fire! n :on-change (if at-end? "end" "away"))))) | 207 | (fire! n :on-change (if at-end? "end" "away"))))) |
| 186 | (emit-children! n)) | 208 | (emit-children! n)) |
| 187 | 209 | ||
| 188 | - :hbox (c/box* (assoc (box-opts props key) :dir :horizontal) | 210 | + :hbox (c/box* (assoc (box-opts props key (fills-height? n)) :dir :horizontal) |
| 189 | (emit-children! n)) | 211 | (emit-children! n)) |
| 190 | 212 | ||
| 191 | - (:vbox :box) (c/box* (box-opts props key) (emit-children! n)) | 213 | + (:vbox :box) (c/box* (box-opts props key (fills-height? n)) |
| 214 | + (emit-children! n)) | ||
| 192 | 215 | ||
| 193 | ;; ONE tag for both kinds of picture: `:feed` is live pixels pushed | 216 | ;; ONE tag for both kinds of picture: `:feed` is live pixels pushed |
| 194 | ;; in under a name and re-uploaded as they arrive, `:src` is a file | 217 | ;; in under a name and re-uploaded as they arrive, `:src` is a file |
modified
jvui/src/jvui/font.clj +38 -1 | @@ -13,7 +13,8 @@ | ||
| 13 | 13 | with a colour-mod at draw time, so one cached texture serves a label in |
| 14 | 14 | every colour it is ever drawn in, and a static page uploads nothing at all |
| 15 | 15 | after its first frame." |
| 16 | - (:require [jvui.sdl :as sdl])) | |
| 16 | + (:require [clojure.string] | |
| 17 | + [jvui.sdl :as sdl])) | |
| 17 | 18 | |
| 18 | 19 | (def ^:private candidates |
| 19 | 20 | ["/usr/share/fonts/noto/NotoSans-Regular.ttf" |
| @@ -35,6 +36,31 @@ | ||
| 35 | 36 | |
| 36 | 37 | (defn- ffi-null? [p] (or (nil? p) (and (number? p) (zero? p)))) |
| 37 | 38 | |
| 39 | +(def ^:private fallbacks | |
| 40 | + "Faces to try for what the UI font has no glyph for. | |
| 41 | + | |
| 42 | + A UI font covers Latin and stops. frq's chrome is icons and its | |
| 43 | + messages carry emoji, and every one of those was coming out as the | |
| 44 | + missing-glyph box — which is what the reader sees as \"the glyphs are | |
| 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. | |
| 47 | + | |
| 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. | |
| 50 | + JVUI_FALLBACK_FONTS overrides, colon-separated, for a machine whose | |
| 51 | + 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" | |
| 55 | + "/usr/share/fonts/noto/NotoSansSymbols2-Regular.ttf" | |
| 56 | + "/usr/share/fonts/noto/NotoSansSymbols-Regular.ttf" | |
| 57 | + "/usr/share/fonts/truetype/noto/NotoSansSymbols2-Regular.ttf"]) | |
| 58 | + | |
| 59 | +(defn- fallback-paths [] | |
| 60 | + (if-let [env (System/getenv "JVUI_FALLBACK_FONTS")] | |
| 61 | + (remove clojure.string/blank? (clojure.string/split env #":")) | |
| 62 | + (filter #(.exists (java.io.File. %)) fallbacks))) | |
| 63 | + | |
| 38 | 64 | (defn open |
| 39 | 65 | "A font cache over `path`. Sizes are opened lazily and kept." |
| 40 | 66 | [path] |
| @@ -47,6 +73,17 @@ | ||
| 47 | 73 | (let [f (sdl/open-font (:path @cache) (float size))] |
| 48 | 74 | (when (or (nil? f) (and (number? f) (zero? f))) |
| 49 | 75 | (throw (ex-info (str "TTF_OpenFont " (:path @cache) ": " (sdl/error)) {}))) |
| 76 | + ;; 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_ttf | |
| 78 | + ;; holds the pointer and does not copy the font. | |
| 79 | + (let [fbs (into [] | |
| 80 | + (keep (fn [path] | |
| 81 | + (let [g (sdl/open-font path (float size))] | |
| 82 | + (when-not (ffi-null? g) | |
| 83 | + (sdl/add-fallback-font! f g) | |
| 84 | + g)))) | |
| 85 | + (fallback-paths))] | |
| 86 | + (swap! cache update :fallbacks (fnil into []) fbs)) | |
| 50 | 87 | (swap! cache assoc-in [:faces k] f) |
| 51 | 88 | f)))) |
| 52 | 89 | |
| @@ -13,7 +13,8 @@ | |||
| 13 | with a colour-mod at draw time, so one cached texture serves a label in | 13 | with a colour-mod at draw time, so one cached texture serves a label in |
| 14 | every colour it is ever drawn in, and a static page uploads nothing at all | 14 | every colour it is ever drawn in, and a static page uploads nothing at all |
| 15 | after its first frame." | 15 | after its first frame." |
| 16 | - (:require [jvui.sdl :as sdl])) | 16 | + (:require [clojure.string] |
| 17 | + [jvui.sdl :as sdl])) | ||
| 17 | 18 | ||
| 18 | (def ^:private candidates | 19 | (def ^:private candidates |
| 19 | ["/usr/share/fonts/noto/NotoSans-Regular.ttf" | 20 | ["/usr/share/fonts/noto/NotoSans-Regular.ttf" |
| @@ -35,6 +36,31 @@ | |||
| 35 | 36 | ||
| 36 | (defn- ffi-null? [p] (or (nil? p) (and (number? p) (zero? p)))) | 37 | (defn- ffi-null? [p] (or (nil? p) (and (number? p) (zero? p)))) |
| 37 | 38 | ||
| 39 | +(def ^:private fallbacks | ||
| 40 | + "Faces to try for what the UI font has no glyph for. | ||
| 41 | + | ||
| 42 | + A UI font covers Latin and stops. frq's chrome is icons and its | ||
| 43 | + messages carry emoji, and every one of those was coming out as the | ||
| 44 | + missing-glyph box — which is what the reader sees as \"the glyphs are | ||
| 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. | ||
| 47 | + | ||
| 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. | ||
| 50 | + JVUI_FALLBACK_FONTS overrides, colon-separated, for a machine whose | ||
| 51 | + 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" | ||
| 55 | + "/usr/share/fonts/noto/NotoSansSymbols2-Regular.ttf" | ||
| 56 | + "/usr/share/fonts/noto/NotoSansSymbols-Regular.ttf" | ||
| 57 | + "/usr/share/fonts/truetype/noto/NotoSansSymbols2-Regular.ttf"]) | ||
| 58 | + | ||
| 59 | +(defn- fallback-paths [] | ||
| 60 | + (if-let [env (System/getenv "JVUI_FALLBACK_FONTS")] | ||
| 61 | + (remove clojure.string/blank? (clojure.string/split env #":")) | ||
| 62 | + (filter #(.exists (java.io.File. %)) fallbacks))) | ||
| 63 | + | ||
| 38 | (defn open | 64 | (defn open |
| 39 | "A font cache over `path`. Sizes are opened lazily and kept." | 65 | "A font cache over `path`. Sizes are opened lazily and kept." |
| 40 | [path] | 66 | [path] |
| @@ -47,6 +73,17 @@ | |||
| 47 | (let [f (sdl/open-font (:path @cache) (float size))] | 73 | (let [f (sdl/open-font (:path @cache) (float size))] |
| 48 | (when (or (nil? f) (and (number? f) (zero? f))) | 74 | (when (or (nil? f) (and (number? f) (zero? f))) |
| 49 | (throw (ex-info (str "TTF_OpenFont " (:path @cache) ": " (sdl/error)) {}))) | 75 | (throw (ex-info (str "TTF_OpenFont " (:path @cache) ": " (sdl/error)) {}))) |
| 76 | + ;; 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_ttf | ||
| 78 | + ;; holds the pointer and does not copy the font. | ||
| 79 | + (let [fbs (into [] | ||
| 80 | + (keep (fn [path] | ||
| 81 | + (let [g (sdl/open-font path (float size))] | ||
| 82 | + (when-not (ffi-null? g) | ||
| 83 | + (sdl/add-fallback-font! f g) | ||
| 84 | + g)))) | ||
| 85 | + (fallback-paths))] | ||
| 86 | + (swap! cache update :fallbacks (fnil into []) fbs)) | ||
| 50 | (swap! cache assoc-in [:faces k] f) | 87 | (swap! cache assoc-in [:faces k] f) |
| 51 | f)))) | 88 | f)))) |
| 52 | 89 | ||
modified
jvui/src/jvui/sdl.clj +3 -0 | @@ -67,6 +67,9 @@ | ||
| 67 | 67 | (ffi/defcfn open-font "TTF_OpenFont" [:string :float] :pointer) |
| 68 | 68 | (ffi/defcfn close-font! "TTF_CloseFont" [:pointer] :void) |
| 69 | 69 | (ffi/defcfn font-height "TTF_GetFontHeight" [:pointer] :int) |
| 70 | +;; 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. | |
| 72 | +(ffi/defcfn add-fallback-font! "TTF_AddFallbackFont" [:pointer :pointer] :bool) | |
| 70 | 73 | (ffi/defcfn ^:private raw-string-size "TTF_GetStringSize" |
| 71 | 74 | [:pointer :string :ulong :pointer :pointer] :bool) |
| 72 | 75 | (ffi/defcfn ^:private raw-render-blended "TTF_RenderText_Blended" |
| @@ -67,6 +67,9 @@ | |||
| 67 | (ffi/defcfn open-font "TTF_OpenFont" [:string :float] :pointer) | 67 | (ffi/defcfn open-font "TTF_OpenFont" [:string :float] :pointer) |
| 68 | (ffi/defcfn close-font! "TTF_CloseFont" [:pointer] :void) | 68 | (ffi/defcfn close-font! "TTF_CloseFont" [:pointer] :void) |
| 69 | (ffi/defcfn font-height "TTF_GetFontHeight" [:pointer] :int) | 69 | (ffi/defcfn font-height "TTF_GetFontHeight" [:pointer] :int) |
| 70 | +;; 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. | ||
| 72 | +(ffi/defcfn add-fallback-font! "TTF_AddFallbackFont" [:pointer :pointer] :bool) | ||
| 70 | (ffi/defcfn ^:private raw-string-size "TTF_GetStringSize" | 73 | (ffi/defcfn ^:private raw-string-size "TTF_GetStringSize" |
| 71 | [:pointer :string :ulong :pointer :pointer] :bool) | 74 | [:pointer :string :ulong :pointer :pointer] :bool) |
| 72 | (ffi/defcfn ^:private raw-render-blended "TTF_RenderText_Blended" | 75 | (ffi/defcfn ^:private raw-render-blended "TTF_RenderText_Blended" |
modified
jvui/src/jvui/widgets.clj +22 -13 | @@ -426,20 +426,24 @@ | ||
| 426 | 426 | leaves the end" |
| 427 | 427 | [opts body] |
| 428 | 428 | (let [{:keys [scroll-key reserve stick-to-bottom scroll-to-bottom on-at-end]} opts |
| 429 | - avail (c/avail-height) | |
| 430 | - h (double (cond | |
| 431 | - (:height opts) (:height opts) | |
| 432 | - ;; With no height of its own a viewport takes what the | |
| 433 | - ;; column has left, less whatever must stay behind for | |
| 434 | - ;; the compose bar under it. Two hundred is only the | |
| 435 | - ;; answer before anything has a size — on the first | |
| 436 | - ;; frame, when the column does not know its own height | |
| 437 | - ;; yet. | |
| 438 | - (pos? avail) (max 0.0 (- avail (double (or reserve 0.0)))) | |
| 439 | - :else 200.0)) | |
| 440 | 429 | id (c/next-id (:key opts)) |
| 441 | 430 | area (or scroll-key id) |
| 442 | 431 | prev (c/data id) |
| 432 | + ;; A viewport with no height of its own FILLS what is left rather | |
| 433 | + ;; than ASKING for it. Asking is what it did first — a minimum of | |
| 434 | + ;; the whole column — and then the column had nothing left for | |
| 435 | + ;; the separator and compose bar under it, which went off the | |
| 436 | + ;; bottom of the window. | |
| 437 | + ;; | |
| 438 | + ;; So the height is the one it was GIVEN last frame, and it | |
| 439 | + ;; reports a minimum of nothing. Two hundred is the answer on the | |
| 440 | + ;; first frame only, before it has been given anything. | |
| 441 | + given (nth (:rect prev) 3 nil) | |
| 442 | + h (double (cond | |
| 443 | + (:height opts) (:height opts) | |
| 444 | + (and given (pos? (double given))) | |
| 445 | + (max 0.0 (- (double given) (double (or reserve 0.0)))) | |
| 446 | + :else 200.0)) | |
| 443 | 447 | content-h (second (or (:content-min prev) [0.0 0.0])) |
| 444 | 448 | view (or (:rect prev) [0.0 0.0 0.0 0.0]) |
| 445 | 449 | maxoff (max 0.0 (- content-h h)) |
| @@ -470,8 +474,13 @@ | ||
| 470 | 474 | stick? maxoff |
| 471 | 475 | :else wheeled) |
| 472 | 476 | r (c/box* (merge {:dir :vertical |
| 473 | - :expand :horizontal | |
| 474 | - :min-size [0.0 h] | |
| 477 | + :expand :both | |
| 478 | + ;; An explicit :height IS a demand and is | |
| 479 | + ;; reported as one. Without it the viewport | |
| 480 | + ;; fills what is left and asks for nothing — | |
| 481 | + ;; asking is what left the compose bar off the | |
| 482 | + ;; bottom of the window. | |
| 483 | + :min-size [0.0 (if (:height opts) h 0.0)] | |
| 475 | 484 | :fixed true |
| 476 | 485 | :clip? true |
| 477 | 486 | :spacing (c/th :spacing) |
| @@ -426,20 +426,24 @@ | |||
| 426 | leaves the end" | 426 | leaves the end" |
| 427 | [opts body] | 427 | [opts body] |
| 428 | (let [{:keys [scroll-key reserve stick-to-bottom scroll-to-bottom on-at-end]} opts | 428 | (let [{:keys [scroll-key reserve stick-to-bottom scroll-to-bottom on-at-end]} opts |
| 429 | - avail (c/avail-height) | ||
| 430 | - h (double (cond | ||
| 431 | - (:height opts) (:height opts) | ||
| 432 | - ;; With no height of its own a viewport takes what the | ||
| 433 | - ;; column has left, less whatever must stay behind for | ||
| 434 | - ;; the compose bar under it. Two hundred is only the | ||
| 435 | - ;; answer before anything has a size — on the first | ||
| 436 | - ;; frame, when the column does not know its own height | ||
| 437 | - ;; yet. | ||
| 438 | - (pos? avail) (max 0.0 (- avail (double (or reserve 0.0)))) | ||
| 439 | - :else 200.0)) | ||
| 440 | id (c/next-id (:key opts)) | 429 | id (c/next-id (:key opts)) |
| 441 | area (or scroll-key id) | 430 | area (or scroll-key id) |
| 442 | prev (c/data id) | 431 | prev (c/data id) |
| 432 | + ;; A viewport with no height of its own FILLS what is left rather | ||
| 433 | + ;; than ASKING for it. Asking is what it did first — a minimum of | ||
| 434 | + ;; the whole column — and then the column had nothing left for | ||
| 435 | + ;; the separator and compose bar under it, which went off the | ||
| 436 | + ;; bottom of the window. | ||
| 437 | + ;; | ||
| 438 | + ;; So the height is the one it was GIVEN last frame, and it | ||
| 439 | + ;; reports a minimum of nothing. Two hundred is the answer on the | ||
| 440 | + ;; first frame only, before it has been given anything. | ||
| 441 | + given (nth (:rect prev) 3 nil) | ||
| 442 | + h (double (cond | ||
| 443 | + (:height opts) (:height opts) | ||
| 444 | + (and given (pos? (double given))) | ||
| 445 | + (max 0.0 (- (double given) (double (or reserve 0.0)))) | ||
| 446 | + :else 200.0)) | ||
| 443 | content-h (second (or (:content-min prev) [0.0 0.0])) | 447 | content-h (second (or (:content-min prev) [0.0 0.0])) |
| 444 | view (or (:rect prev) [0.0 0.0 0.0 0.0]) | 448 | view (or (:rect prev) [0.0 0.0 0.0 0.0]) |
| 445 | maxoff (max 0.0 (- content-h h)) | 449 | maxoff (max 0.0 (- content-h h)) |
| @@ -470,8 +474,13 @@ | |||
| 470 | stick? maxoff | 474 | stick? maxoff |
| 471 | :else wheeled) | 475 | :else wheeled) |
| 472 | r (c/box* (merge {:dir :vertical | 476 | r (c/box* (merge {:dir :vertical |
| 473 | - :expand :horizontal | 477 | + :expand :both |
| 474 | - :min-size [0.0 h] | 478 | + ;; An explicit :height IS a demand and is |
| 479 | + ;; reported as one. Without it the viewport | ||
| 480 | + ;; fills what is left and asks for nothing — | ||
| 481 | + ;; asking is what left the compose bar off the | ||
| 482 | + ;; bottom of the window. | ||
| 483 | + :min-size [0.0 (if (:height opts) h 0.0)] | ||
| 475 | :fixed true | 484 | :fixed true |
| 476 | :clip? true | 485 | :clip? true |
| 477 | :spacing (c/th :spacing) | 486 | :spacing (c/th :spacing) |