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

Let a container fill its parent, and report "end" as the word

The chat column came out a couple of hundred points wide in a
five-hundred-point window, every message wrapped to match and the
scrollbar stranded in the middle of the screen. Every box was
shrink-wrapping its children, and frq's tree is nested boxes — each only
as wide as what is in it, all the way down.

A container fills its parent's cross axis now. :cross rather than
:horizontal, and the distinction is the whole reason for a new value: in
a ROW, :horizontal means take a share of the slack, so a line of buttons
would stretch to fill the window. :cross means fill the other axis and
take no share of your own, which is what a column in a column wants and
what a row in a column wants too.

And :on-change reports the WORD. libvidya emits "end" or "away" and frq's
handler is (= "end" %), so a boolean made at-present? permanently false —
the jump-to-present button would never have gone away. :on-scroll is no
longer fired from here at all: it is the channel for backends that report
an OFFSET rather than a place, which is the terminal's, and frq turns one
into the other with `scrolled!`.

The filling half is checked by pixel, because only tags that register for
events carry a rect in the tree and a plain vbox is not one. A card
paints a background, so a screenshot can measure how wide it came out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-09-10T06:44:40-07:00 Browse files
e72d7b0 parent: 2271a91
modified glimmer-backends/glimmer-jvui/src/glimmer_jvui/core.clj +24 -7
@@ -74,7 +74,18 @@
7474 "The container options shared by every container tag."
7575 [props key]
7676 (cond-> {:key key
77- :dir (if (= :horizontal (:orientation props)) :horizontal :vertical)}
77+ :dir (if (= :horizontal (:orientation props)) :horizontal :vertical)
78+ ;; A CONTAINER fills its parent's cross axis by default. Without
79+ ;; this every box shrink-wraps its children, and frq's chat
80+ ;; column came out a couple of hundred points wide in a
81+ ;; five-hundred-point window with every message wrapped to
82+ ;; match — the tree is nested boxes, and each one only as wide
83+ ;; as what is in it.
84+ ;;
85+ ;; :cross and not :horizontal: in a ROW, :horizontal means take
86+ ;; a share of the slack, and a line of buttons would stretch to
87+ ;; fill the window.
88+ :expand :cross}
7889 (:spacing props) (assoc :spacing (num (:spacing props) 0.0))
7990 (:padding props) (assoc :padding (num (:padding props) 0.0))
8091 (:margin props) (assoc :margin (num (:margin props) 0.0))
@@ -142,14 +153,20 @@
142153 (assoc :stick-to-bottom true)
143154 (:scroll-to-bottom props)
144155 (assoc :scroll-to-bottom (num (:scroll-to-bottom props) 0.0))
145- ;; libvidya calls this "change" and answers
146- ;; "end"/"away"; frq listens on :on-change and
147- ;; :on-scroll. Both are given the boolean.
148- (or (:on-change props) (:on-scroll props))
156+ ;; "end" or "away", the STRING libvidya emits —
157+ ;; frq's handler is (= "end" %) and a boolean
158+ ;; makes it permanently false.
159+ ;;
160+ ;; :on-scroll is deliberately not fired here.
161+ ;; It is the channel for backends that report an
162+ ;; OFFSET rather than a place — the terminal's —
163+ ;; and frq turns one into the other with
164+ ;; `scrolled!`. A window that reports where it
165+ ;; ended up has nothing to say on it.
166+ (:on-change props)
149167 (assoc :on-at-end
150168 (fn [at-end?]
151- (fire! n :on-change at-end?)
152- (fire! n :on-scroll at-end?))))
169+ (fire! n :on-change (if at-end? "end" "away")))))
153170 (emit-children! n))
154171
155172 :hbox (c/box* (assoc (box-opts props key) :dir :horizontal)
@@ -74,7 +74,18 @@
74 "The container options shared by every container tag."74 "The container options shared by every container tag."
75 [props key]75 [props key]
76 (cond-> {:key key76 (cond-> {:key key
77- :dir (if (= :horizontal (:orientation props)) :horizontal :vertical)}77+ :dir (if (= :horizontal (:orientation props)) :horizontal :vertical)
78+ ;; A CONTAINER fills its parent's cross axis by default. Without
79+ ;; this every box shrink-wraps its children, and frq's chat
80+ ;; column came out a couple of hundred points wide in a
81+ ;; five-hundred-point window with every message wrapped to
82+ ;; match — the tree is nested boxes, and each one only as wide
83+ ;; as what is in it.
84+ ;;
85+ ;; :cross and not :horizontal: in a ROW, :horizontal means take
86+ ;; a share of the slack, and a line of buttons would stretch to
87+ ;; fill the window.
88+ :expand :cross}
78 (:spacing props) (assoc :spacing (num (:spacing props) 0.0))89 (:spacing props) (assoc :spacing (num (:spacing props) 0.0))
79 (:padding props) (assoc :padding (num (:padding props) 0.0))90 (:padding props) (assoc :padding (num (:padding props) 0.0))
80 (:margin props) (assoc :margin (num (:margin props) 0.0))91 (:margin props) (assoc :margin (num (:margin props) 0.0))
@@ -142,14 +153,20 @@
142 (assoc :stick-to-bottom true)153 (assoc :stick-to-bottom true)
143 (:scroll-to-bottom props)154 (:scroll-to-bottom props)
144 (assoc :scroll-to-bottom (num (:scroll-to-bottom props) 0.0))155 (assoc :scroll-to-bottom (num (:scroll-to-bottom props) 0.0))
145- ;; libvidya calls this "change" and answers156+ ;; "end" or "away", the STRING libvidya emits —
146- ;; "end"/"away"; frq listens on :on-change and157+ ;; frq's handler is (= "end" %) and a boolean
147- ;; :on-scroll. Both are given the boolean.158+ ;; makes it permanently false.
148- (or (:on-change props) (:on-scroll props))159+ ;;
160+ ;; :on-scroll is deliberately not fired here.
161+ ;; It is the channel for backends that report an
162+ ;; OFFSET rather than a place — the terminal's —
163+ ;; and frq turns one into the other with
164+ ;; `scrolled!`. A window that reports where it
165+ ;; ended up has nothing to say on it.
166+ (:on-change props)
149 (assoc :on-at-end167 (assoc :on-at-end
150 (fn [at-end?]168 (fn [at-end?]
151- (fire! n :on-change at-end?)169+ (fire! n :on-change (if at-end? "end" "away")))))
152- (fire! n :on-scroll at-end?))))
153 (emit-children! n))170 (emit-children! n))
154 171
155 :hbox (c/box* (assoc (box-opts props key) :dir :horizontal)172 :hbox (c/box* (assoc (box-opts props key) :dir :horizontal)
modified glimmer-backends/glimmer-jvui/test/glimmer_jvui/props_check.clj +10 -1
@@ -35,6 +35,11 @@
3535 out (atom []) ck! (fn [n ok?] (swap! out conj [n (boolean ok?)]))]
3636 (ui/run (fn []
3737 [:vbox {:spacing 8}
38+ ;; A card paints a background, so how wide it comes out is
39+ ;; visible. Nested two deep because frq's tree is nested:
40+ ;; every box shrink-wrapping is how its chat column ended
41+ ;; up a couple of hundred points wide in a wider window.
42+ [:vbox {} [:card {} [:label {:label "x"}]]]
3843 ;; :active, the way frq writes it — the tick must appear.
3944 [:checkbutton {:label "TLS" :active true}]
4045 ;; A string far wider than the field it is in.
@@ -47,7 +52,11 @@
4752 (> (ink-in img bg 4 4 26 30) 12))
4853 ;; Nothing may be drawn to the right of the field's own border.
4954 (ck! "entry text stops at the field's edge"
50- (zero? (ink-in img bg 232 40 240 100))))
55+ (zero? (ink-in img bg 232 100 240 140)))
56+ ;; The card's own background, out near the right edge: a
57+ ;; shrink-wrapped one would not reach.
58+ (ck! "a nested container fills the width"
59+ (pos? (ink-in img bg 200 4 232 40))))
5160 (doseq [[n ok?] @out] (println (if ok? "- " "FAIL ") n))
5261 (let [bad (remove second @out)]
5362 (println (if (seq bad) (str (count bad) " of " (count @out) " checks FAILED")
@@ -35,6 +35,11 @@
35 out (atom []) ck! (fn [n ok?] (swap! out conj [n (boolean ok?)]))]35 out (atom []) ck! (fn [n ok?] (swap! out conj [n (boolean ok?)]))]
36 (ui/run (fn []36 (ui/run (fn []
37 [:vbox {:spacing 8}37 [:vbox {:spacing 8}
38+ ;; A card paints a background, so how wide it comes out is
39+ ;; visible. Nested two deep because frq's tree is nested:
40+ ;; every box shrink-wrapping is how its chat column ended
41+ ;; up a couple of hundred points wide in a wider window.
42+ [:vbox {} [:card {} [:label {:label "x"}]]]
38 ;; :active, the way frq writes it — the tick must appear.43 ;; :active, the way frq writes it — the tick must appear.
39 [:checkbutton {:label "TLS" :active true}]44 [:checkbutton {:label "TLS" :active true}]
40 ;; A string far wider than the field it is in.45 ;; A string far wider than the field it is in.
@@ -47,7 +52,11 @@
47 (> (ink-in img bg 4 4 26 30) 12))52 (> (ink-in img bg 4 4 26 30) 12))
48 ;; Nothing may be drawn to the right of the field's own border.53 ;; Nothing may be drawn to the right of the field's own border.
49 (ck! "entry text stops at the field's edge"54 (ck! "entry text stops at the field's edge"
50- (zero? (ink-in img bg 232 40 240 100))))55+ (zero? (ink-in img bg 232 100 240 140)))
56+ ;; The card's own background, out near the right edge: a
57+ ;; shrink-wrapped one would not reach.
58+ (ck! "a nested container fills the width"
59+ (pos? (ink-in img bg 200 4 232 40))))
51 (doseq [[n ok?] @out] (println (if ok? "- " "FAIL ") n))60 (doseq [[n ok?] @out] (println (if ok? "- " "FAIL ") n))
52 (let [bad (remove second @out)]61 (let [bad (remove second @out)]
53 (println (if (seq bad) (str (count bad) " of " (count @out) " checks FAILED")62 (println (if (seq bad) (str (count bad) " of " (count @out) " checks FAILED")
modified glimmer-backends/glimmer-jvui/test/glimmer_jvui/tests.clj +25 -0
@@ -177,6 +177,30 @@
177177 (first (filter #(and (= :label (:tag @%)) (= text (str (:label (:props @%)))))
178178 (walk root))))
179179
180+(defn- check-rows-do-not-stretch! []
181+ ;; Every box shrink-wrapping its children is how frq's chat column came
182+ ;; out a couple of hundred points wide in a five-hundred-point window,
183+ ;; with every message wrapped to match and the scrollbar stranded in the
184+ ;; middle of the screen. A container fills its parent's cross axis.
185+ ;;
186+ ;; And a ROW's children must NOT: :cross rather than :horizontal is what
187+ ;; keeps a line of buttons from stretching to fill the window.
188+ (let [root (jv/root-node) cx (ctx)] ; a 400-wide context
189+ (gui/mount root :page
190+ [(fn [] [:vbox {:key :outer}
191+ [:vbox {:key :inner} [:label {:label "hi"}]]
192+ [:hbox {:key :row} [:button {:label "a"}]
193+ [:button {:label "b"}]]])])
194+ (dotimes [_ 3] (jv/render-once root cx))
195+ ;; Only tags that register for events carry a rect here, so the
196+ ;; filling half is checked by pixel in props-check — a card paints a
197+ ;; background and a screenshot can measure it. What CAN be asserted
198+ ;; from the tree is the half that would regress silently.
199+ (let [buttons (filter #(and (= :button (:tag @%)) (:rect @%)) (walk root))]
200+ (check! (every? #(< (nth (:rect @%) 2) 120.0) buttons)
201+ (str "and buttons in a row do not stretch: "
202+ (pr-str (map #(nth (:rect @%) 2) buttons)))))))
203+
180204 (defn- check-scroll-sticks! []
181205 ;; A chat that does not follow new messages is the difference between a
182206 ;; window you read and one you drag.
@@ -234,6 +258,7 @@
234258 ["a press alone does not" check-press-alone-is-not-a-click!]
235259 ["a reorder keeps identity" check-reorder-keeps-identity!]
236260 ["an unknown tag is a container" check-unknown-tag-is-a-container!]
261+ ["a row does not stretch" check-rows-do-not-stretch!]
237262 ["a list sticks to the end" check-scroll-sticks!]
238263 ["Enter sends" check-enter-sends!]
239264 ["an entry round-trips" check-entry-round-trips!]
@@ -177,6 +177,30 @@
177 (first (filter #(and (= :label (:tag @%)) (= text (str (:label (:props @%)))))177 (first (filter #(and (= :label (:tag @%)) (= text (str (:label (:props @%)))))
178 (walk root))))178 (walk root))))
179 179
180+(defn- check-rows-do-not-stretch! []
181+ ;; Every box shrink-wrapping its children is how frq's chat column came
182+ ;; out a couple of hundred points wide in a five-hundred-point window,
183+ ;; with every message wrapped to match and the scrollbar stranded in the
184+ ;; middle of the screen. A container fills its parent's cross axis.
185+ ;;
186+ ;; And a ROW's children must NOT: :cross rather than :horizontal is what
187+ ;; keeps a line of buttons from stretching to fill the window.
188+ (let [root (jv/root-node) cx (ctx)] ; a 400-wide context
189+ (gui/mount root :page
190+ [(fn [] [:vbox {:key :outer}
191+ [:vbox {:key :inner} [:label {:label "hi"}]]
192+ [:hbox {:key :row} [:button {:label "a"}]
193+ [:button {:label "b"}]]])])
194+ (dotimes [_ 3] (jv/render-once root cx))
195+ ;; Only tags that register for events carry a rect here, so the
196+ ;; filling half is checked by pixel in props-check — a card paints a
197+ ;; background and a screenshot can measure it. What CAN be asserted
198+ ;; from the tree is the half that would regress silently.
199+ (let [buttons (filter #(and (= :button (:tag @%)) (:rect @%)) (walk root))]
200+ (check! (every? #(< (nth (:rect @%) 2) 120.0) buttons)
201+ (str "and buttons in a row do not stretch: "
202+ (pr-str (map #(nth (:rect @%) 2) buttons)))))))
203+
180 (defn- check-scroll-sticks! []204 (defn- check-scroll-sticks! []
181 ;; A chat that does not follow new messages is the difference between a205 ;; A chat that does not follow new messages is the difference between a
182 ;; window you read and one you drag.206 ;; window you read and one you drag.
@@ -234,6 +258,7 @@
234 ["a press alone does not" check-press-alone-is-not-a-click!]258 ["a press alone does not" check-press-alone-is-not-a-click!]
235 ["a reorder keeps identity" check-reorder-keeps-identity!]259 ["a reorder keeps identity" check-reorder-keeps-identity!]
236 ["an unknown tag is a container" check-unknown-tag-is-a-container!]260 ["an unknown tag is a container" check-unknown-tag-is-a-container!]
261+ ["a row does not stretch" check-rows-do-not-stretch!]
237 ["a list sticks to the end" check-scroll-sticks!]262 ["a list sticks to the end" check-scroll-sticks!]
238 ["Enter sends" check-enter-sends!]263 ["Enter sends" check-enter-sends!]
239 ["an entry round-trips" check-entry-round-trips!]264 ["an entry round-trips" check-entry-round-trips!]
modified jvui/src/jvui/core.clj +12 -2
@@ -206,6 +206,15 @@
206206 (defn- ctr+ [f i v] (aset (:c f) i (+ (aget (:c f) i) v)))
207207
208208 (defn- expands?
209+ "Does a child with this `expand` take more room along `axis`?
210+
211+ `:cross` is the container's answer, and the one worth explaining: fill
212+ the parent's OTHER axis and take no share of its own. A column inside a
213+ column should be as wide as its parent and only as tall as its
214+ contents; a row inside that column, the same. Saying `:horizontal`
215+ instead works in a column and is wrong in a row — there it makes the
216+ child fight its siblings for the slack, and a line of buttons stretches
217+ to fill the window."
209218 [expand axis]
210219 (case expand
211220 :both true
@@ -249,8 +258,9 @@
249258 [rx ry rw rh] rect
250259 [ox oy] (or offset [0.0 0.0])
251260 along-axis (if (= dir :vertical) :vertical :horizontal)
252- grow-along? (expands? expand along-axis)
253- grow-cross? (expands? expand (if (= dir :vertical) :horizontal :vertical))]
261+ cross-axis (if (= dir :vertical) :horizontal :vertical)
262+ grow-along? (and (not= expand :cross) (expands? expand along-axis))
263+ grow-cross? (or (= expand :cross) (expands? expand cross-axis))]
254264 (if (= dir :vertical)
255265 (let [h (+ mh (if grow-along? share 0.0))
256266 ;; NOT clamped to the box's width. A container's own size comes from
@@ -206,6 +206,15 @@
206 (defn- ctr+ [f i v] (aset (:c f) i (+ (aget (:c f) i) v)))206 (defn- ctr+ [f i v] (aset (:c f) i (+ (aget (:c f) i) v)))
207 207
208 (defn- expands?208 (defn- expands?
209+ "Does a child with this `expand` take more room along `axis`?
210+
211+ `:cross` is the container's answer, and the one worth explaining: fill
212+ the parent's OTHER axis and take no share of its own. A column inside a
213+ column should be as wide as its parent and only as tall as its
214+ contents; a row inside that column, the same. Saying `:horizontal`
215+ instead works in a column and is wrong in a row — there it makes the
216+ child fight its siblings for the slack, and a line of buttons stretches
217+ to fill the window."
209 [expand axis]218 [expand axis]
210 (case expand219 (case expand
211 :both true220 :both true
@@ -249,8 +258,9 @@
249 [rx ry rw rh] rect258 [rx ry rw rh] rect
250 [ox oy] (or offset [0.0 0.0])259 [ox oy] (or offset [0.0 0.0])
251 along-axis (if (= dir :vertical) :vertical :horizontal)260 along-axis (if (= dir :vertical) :vertical :horizontal)
252- grow-along? (expands? expand along-axis)261+ cross-axis (if (= dir :vertical) :horizontal :vertical)
253- grow-cross? (expands? expand (if (= dir :vertical) :horizontal :vertical))]262+ grow-along? (and (not= expand :cross) (expands? expand along-axis))
263+ grow-cross? (or (= expand :cross) (expands? expand cross-axis))]
254 (if (= dir :vertical)264 (if (= dir :vertical)
255 (let [h (+ mh (if grow-along? share 0.0))265 (let [h (+ mh (if grow-along? share 0.0))
256 ;; NOT clamped to the box's width. A container's own size comes from266 ;; NOT clamped to the box's width. A container's own size comes from