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

Measure a gap down the page in rows, and let a thin one be nothing

A cell is about eight points across and sixteen down, and one divisor for both
spends twice as much of the screen on vertical air as the design asked for.
Vertical air is the whole budget: a conversation is measured in how many
messages fit, and frq's fit two.

So there are two scales now, and which one a prop takes is the axis it
measures. `:spacing` and `:gap` ask the box: the distance between the children
of a column is rows, the same number between the children of a row is columns,
and nothing in the name says which.

And a half rounds down. That is the case that decides how a screen reads,
because it is nearly every case: frq spaces its columns 8 points apart against
a 16-point row, so every gap in the tree is exactly one half. Rounded up, the
chat screen spent nine rows on the gaps between its ten children — and most of
those children are empty wrappers holding a place for something not on screen,
each costing a blank line it was never meant to have. Rounded down, a gap that
thin is what it looks like at this size: nothing.

Thirteen messages now, where there were two.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-09-01T23:55:09-07:00 Browse files
c4f56b0 parent: 68910bd
modified jolt/glimmer-tui/src/glimmer_tui/core.jolt +75 -31
@@ -48,28 +48,46 @@
4848 ;; twelve blank rows and a column wider than the screen. The reconciler is not
4949 ;; the place to fix that and neither is the app: the numbers are right, and it
5050 ;; is the unit under them that changed. So the backend divides on the way
51-;; across, and a tree written for cells leaves the scale at 1.
52-(defonce ^:private scale (atom 1))
53-
54-;; The props that are a distance rather than a count, a flag or a name. A key
55-;; this list does not know crosses unscaled, which is the right way round: a
56-;; number that turns out to be a length paints a little large, where a scaled
57-;; `:value` or `:selected` would be silently wrong.
58-(def ^:private spatial-props
59- #{:margin :margin-top :margin-bottom :margin-left :margin-right
60- :padding :padding-top :padding-bottom :padding-left :padding-right
61- :spacing :gap :size :reserve
62- :width-request :height-request :max-width :min-width :max-height})
51+;; across, and a tree written for cells leaves both scales at 1.
52+;;
53+;; Two scales, because a cell is not square. It is about eight points across
54+;; and sixteen down, so one divisor for both spends twice as much of the screen
55+;; on vertical air as the design asked for and vertical air is the whole
56+;; budget: a chat backlog is measured in how many messages fit. A `:spacing 8`
57+;; between every pair of rows is half a row, which is to say none; the same 8
58+;; between two buttons is a column, which is the space that keeps them apart.
59+(defonce ^:private col-scale (atom 1))
60+(defonce ^:private row-scale (atom 1))
61+
62+;; The props that are a distance rather than a count, a flag or a name, split
63+;; by the axis each one measures. A key none of these knows crosses unscaled,
64+;; which is the right way round: a number that turns out to be a length paints
65+;; a little large, where a scaled `:value` or `:selected` would be silently
66+;; wrong.
67+(def ^:private col-props
68+ #{:width-request :max-width :min-width :margin-left :margin-right
69+ :padding-left :padding-right})
70+
71+;; `:margin` and `:padding` are one number for both axes and there is one
72+;; inset under them, so they are counted as rows: the tighter of the two
73+;; readings, and the axis where being loose costs a message.
74+(def ^:private row-props
75+ #{:height-request :max-height :reserve :size
76+ :margin :margin-top :margin-bottom
77+ :padding :padding-top :padding-bottom})
6378
6479 (defn- scaled
65- "`v` in cells, rounded away from zero so a margin that was asked for is at
66- least one cell of one."
67- [v]
68- (let [n (/ (double v) @scale)]
69- (cond
70- (zero? n) 0
71- (< (Math/abs n) 1.0) (if (pos? n) 1 -1)
72- :else (Math/round n))))
80+ "`v` divided by `scale`, to the nearest cell, with a half going down.
81+
82+ Half a cell is the case that decides how a screen reads, and it decides it
83+ many times: frq's columns are spaced 8 points apart and a row is 16, so every
84+ gap in the tree is exactly one half. Rounded up, the chat screen spends nine
85+ rows on the nine gaps between its ten children — and most of those children
86+ are empty wrappers, there to hold a place for something that is not on
87+ screen, each now costing a blank line it was never meant to have. Rounded
88+ down, a gap that thin is what it looks like at this size: nothing."
89+ [v scale]
90+ (long (Math/ceil (- (/ (double v) scale) 0.5))))
7391
7492 ;; --- props -------------------------------------------------------------------
7593 ;; :hbox and :vbox are one node in the library; the tag only implies an
@@ -82,20 +100,34 @@
82100 (let [s (name k)]
83101 (and (> (count s) 3) (= "on-" (subs s 0 3)))))
84102
103+(defn- axis-scale
104+ "Which divisor `k` is measured in, on a node laid out `vertical?`.
105+
106+ `:spacing` and `:gap` are the ones that need asking: they are the distance
107+ between a box's children, so which axis they run along is the box's own
108+ orientation and not something the name says. nil for a prop that is not a
109+ distance at all."
110+ [k vertical?]
111+ (cond
112+ (contains? col-props k) @col-scale
113+ (contains? row-props k) @row-scale
114+ (contains? #{:spacing :gap} k) (if vertical? @row-scale @col-scale)
115+ :else nil))
116+
85117 (defn- set-prop!
86118 "Write one prop to a node, in the ABI type that fits its value. nil clears
87119 nothing — the prop was already dropped by the clear that precedes a write —
88120 and an unrecognized value is stringified rather than refused, so a prop this
89121 backend has not learned yet still reaches the library."
90- [node k v]
122+ [node k v vertical?]
91123 (let [key (name k)]
92124 (cond
93125 (nil? v) nil
94126 (true? v) (ffi/node-set-bool! node key true)
95127 (false? v) (ffi/node-set-bool! node key false)
96128 (number? v) (ffi/node-set-num! node key
97- (double (if (contains? spatial-props k)
98- (scaled v)
129+ (double (if-let [scale (axis-scale k vertical?)]
130+ (scaled v scale)
99131 v)))
100132 (string? v) (ffi/node-set-str! node key v)
101133 (keyword? v) (ffi/node-set-str! node key (name v))
@@ -114,9 +146,16 @@
114146 (when-let [orientation (tag-orientation tag)]
115147 (when-not (contains? props :orientation)
116148 (ffi/node-set-str! node "orientation" orientation)))
117- (doseq [[k v] props]
118- (when-not (handler-key? k)
119- (set-prop! node k v)))
149+ ;; Which way this node lays its children out, for the props whose axis is the
150+ ;; box's rather than their own. Everything that is not explicitly a row is a
151+ ;; column, which is what the library assumes of a container it does not know.
152+ (let [vertical? (not= "horizontal"
153+ (or (some-> (:orientation props) name)
154+ (tag-orientation tag)
155+ "vertical"))]
156+ (doseq [[k v] props]
157+ (when-not (handler-key? k)
158+ (set-prop! node k v vertical?))))
120159 (swap! handlers assoc node
121160 (reduce (fn [acc [k v]]
122161 (if (and (handler-key? k) (fn? v)) (assoc acc k v) acc))
@@ -365,11 +404,15 @@
365404
366405 Options (on top of glimmer's own):
367406 :mouse report clicks and the wheel (default true)
368- :points-per-cell how many of the tree's own units go into one cell
369- (default 1). 8 is about right for a tree written against
370- a window: it is the width of a character in the size a
407+ :points-per-cell how many of the tree's own units go across one cell
408+ (default 1). 8 is about right for a tree written against a
409+ window: it is the width of a character in the size a
371410 desktop UI uses, which is what those numbers were laid out
372411 in.
412+ :points-per-row the same down the page (default: twice
413+ `:points-per-cell`, because a cell is about twice as tall
414+ as it is wide). This is the one that decides how much of a
415+ conversation fits on a screen.
373416 :fps how often the loop wakes when no input arrives (default 60)
374417 :headless [columns rows] a session with no terminal at all, for a
375418 test or a screenshot; input is fed by hand
@@ -380,9 +423,10 @@
380423 terminal in raw mode on the alternate screen which is the one failure here
381424 a reader cannot recover from without `reset`."
382425 [opts mount-root!]
383- (let [{:keys [mouse fps headless auto-quit-ms points-per-cell]
426+ (let [{:keys [mouse fps headless auto-quit-ms points-per-cell points-per-row]
384427 :or {mouse true fps 60 points-per-cell 1}} opts
385- _ (reset! scale (max 1 points-per-cell))
428+ _ (reset! col-scale (max 1 points-per-cell))
429+ _ (reset! row-scale (max 1 (or points-per-row (* 2 points-per-cell))))
386430 opened? (if headless
387431 (ffi/headless! (first headless) (second headless))
388432 (ffi/open! mouse))]
@@ -48,28 +48,46 @@
48 ;; twelve blank rows and a column wider than the screen. The reconciler is not48 ;; twelve blank rows and a column wider than the screen. The reconciler is not
49 ;; the place to fix that and neither is the app: the numbers are right, and it49 ;; the place to fix that and neither is the app: the numbers are right, and it
50 ;; is the unit under them that changed. So the backend divides on the way50 ;; is the unit under them that changed. So the backend divides on the way
51-;; across, and a tree written for cells leaves the scale at 1.51+;; across, and a tree written for cells leaves both scales at 1.
52-(defonce ^:private scale (atom 1))52+;;
53-53+;; Two scales, because a cell is not square. It is about eight points across
54-;; The props that are a distance rather than a count, a flag or a name. A key54+;; and sixteen down, so one divisor for both spends twice as much of the screen
55-;; this list does not know crosses unscaled, which is the right way round: a55+;; on vertical air as the design asked for and vertical air is the whole
56-;; number that turns out to be a length paints a little large, where a scaled56+;; budget: a chat backlog is measured in how many messages fit. A `:spacing 8`
57-;; `:value` or `:selected` would be silently wrong.57+;; between every pair of rows is half a row, which is to say none; the same 8
58-(def ^:private spatial-props58+;; between two buttons is a column, which is the space that keeps them apart.
59- #{:margin :margin-top :margin-bottom :margin-left :margin-right59+(defonce ^:private col-scale (atom 1))
60- :padding :padding-top :padding-bottom :padding-left :padding-right60+(defonce ^:private row-scale (atom 1))
61- :spacing :gap :size :reserve61+
62- :width-request :height-request :max-width :min-width :max-height})62+;; The props that are a distance rather than a count, a flag or a name, split
63+;; by the axis each one measures. A key none of these knows crosses unscaled,
64+;; which is the right way round: a number that turns out to be a length paints
65+;; a little large, where a scaled `:value` or `:selected` would be silently
66+;; wrong.
67+(def ^:private col-props
68+ #{:width-request :max-width :min-width :margin-left :margin-right
69+ :padding-left :padding-right})
70+
71+;; `:margin` and `:padding` are one number for both axes and there is one
72+;; inset under them, so they are counted as rows: the tighter of the two
73+;; readings, and the axis where being loose costs a message.
74+(def ^:private row-props
75+ #{:height-request :max-height :reserve :size
76+ :margin :margin-top :margin-bottom
77+ :padding :padding-top :padding-bottom})
63 78
64 (defn- scaled79 (defn- scaled
65- "`v` in cells, rounded away from zero so a margin that was asked for is at80+ "`v` divided by `scale`, to the nearest cell, with a half going down.
66- least one cell of one."81+
67- [v]82+ Half a cell is the case that decides how a screen reads, and it decides it
68- (let [n (/ (double v) @scale)]83+ many times: frq's columns are spaced 8 points apart and a row is 16, so every
69- (cond84+ gap in the tree is exactly one half. Rounded up, the chat screen spends nine
70- (zero? n) 085+ rows on the nine gaps between its ten children — and most of those children
71- (< (Math/abs n) 1.0) (if (pos? n) 1 -1)86+ are empty wrappers, there to hold a place for something that is not on
72- :else (Math/round n))))87+ screen, each now costing a blank line it was never meant to have. Rounded
88+ down, a gap that thin is what it looks like at this size: nothing."
89+ [v scale]
90+ (long (Math/ceil (- (/ (double v) scale) 0.5))))
73 91
74 ;; --- props -------------------------------------------------------------------92 ;; --- props -------------------------------------------------------------------
75 ;; :hbox and :vbox are one node in the library; the tag only implies an93 ;; :hbox and :vbox are one node in the library; the tag only implies an
@@ -82,20 +100,34 @@
82 (let [s (name k)]100 (let [s (name k)]
83 (and (> (count s) 3) (= "on-" (subs s 0 3)))))101 (and (> (count s) 3) (= "on-" (subs s 0 3)))))
84 102
103+(defn- axis-scale
104+ "Which divisor `k` is measured in, on a node laid out `vertical?`.
105+
106+ `:spacing` and `:gap` are the ones that need asking: they are the distance
107+ between a box's children, so which axis they run along is the box's own
108+ orientation and not something the name says. nil for a prop that is not a
109+ distance at all."
110+ [k vertical?]
111+ (cond
112+ (contains? col-props k) @col-scale
113+ (contains? row-props k) @row-scale
114+ (contains? #{:spacing :gap} k) (if vertical? @row-scale @col-scale)
115+ :else nil))
116+
85 (defn- set-prop!117 (defn- set-prop!
86 "Write one prop to a node, in the ABI type that fits its value. nil clears118 "Write one prop to a node, in the ABI type that fits its value. nil clears
87 nothing — the prop was already dropped by the clear that precedes a write —119 nothing — the prop was already dropped by the clear that precedes a write —
88 and an unrecognized value is stringified rather than refused, so a prop this120 and an unrecognized value is stringified rather than refused, so a prop this
89 backend has not learned yet still reaches the library."121 backend has not learned yet still reaches the library."
90- [node k v]122+ [node k v vertical?]
91 (let [key (name k)]123 (let [key (name k)]
92 (cond124 (cond
93 (nil? v) nil125 (nil? v) nil
94 (true? v) (ffi/node-set-bool! node key true)126 (true? v) (ffi/node-set-bool! node key true)
95 (false? v) (ffi/node-set-bool! node key false)127 (false? v) (ffi/node-set-bool! node key false)
96 (number? v) (ffi/node-set-num! node key128 (number? v) (ffi/node-set-num! node key
97- (double (if (contains? spatial-props k)129+ (double (if-let [scale (axis-scale k vertical?)]
98- (scaled v)130+ (scaled v scale)
99 v)))131 v)))
100 (string? v) (ffi/node-set-str! node key v)132 (string? v) (ffi/node-set-str! node key v)
101 (keyword? v) (ffi/node-set-str! node key (name v))133 (keyword? v) (ffi/node-set-str! node key (name v))
@@ -114,9 +146,16 @@
114 (when-let [orientation (tag-orientation tag)]146 (when-let [orientation (tag-orientation tag)]
115 (when-not (contains? props :orientation)147 (when-not (contains? props :orientation)
116 (ffi/node-set-str! node "orientation" orientation)))148 (ffi/node-set-str! node "orientation" orientation)))
117- (doseq [[k v] props]149+ ;; Which way this node lays its children out, for the props whose axis is the
118- (when-not (handler-key? k)150+ ;; box's rather than their own. Everything that is not explicitly a row is a
119- (set-prop! node k v)))151+ ;; column, which is what the library assumes of a container it does not know.
152+ (let [vertical? (not= "horizontal"
153+ (or (some-> (:orientation props) name)
154+ (tag-orientation tag)
155+ "vertical"))]
156+ (doseq [[k v] props]
157+ (when-not (handler-key? k)
158+ (set-prop! node k v vertical?))))
120 (swap! handlers assoc node159 (swap! handlers assoc node
121 (reduce (fn [acc [k v]]160 (reduce (fn [acc [k v]]
122 (if (and (handler-key? k) (fn? v)) (assoc acc k v) acc))161 (if (and (handler-key? k) (fn? v)) (assoc acc k v) acc))
@@ -365,11 +404,15 @@
365 404
366 Options (on top of glimmer's own):405 Options (on top of glimmer's own):
367 :mouse report clicks and the wheel (default true)406 :mouse report clicks and the wheel (default true)
368- :points-per-cell how many of the tree's own units go into one cell407+ :points-per-cell how many of the tree's own units go across one cell
369- (default 1). 8 is about right for a tree written against408+ (default 1). 8 is about right for a tree written against a
370- a window: it is the width of a character in the size a409+ window: it is the width of a character in the size a
371 desktop UI uses, which is what those numbers were laid out410 desktop UI uses, which is what those numbers were laid out
372 in.411 in.
412+ :points-per-row the same down the page (default: twice
413+ `:points-per-cell`, because a cell is about twice as tall
414+ as it is wide). This is the one that decides how much of a
415+ conversation fits on a screen.
373 :fps how often the loop wakes when no input arrives (default 60)416 :fps how often the loop wakes when no input arrives (default 60)
374 :headless [columns rows] a session with no terminal at all, for a417 :headless [columns rows] a session with no terminal at all, for a
375 test or a screenshot; input is fed by hand418 test or a screenshot; input is fed by hand
@@ -380,9 +423,10 @@
380 terminal in raw mode on the alternate screen which is the one failure here423 terminal in raw mode on the alternate screen which is the one failure here
381 a reader cannot recover from without `reset`."424 a reader cannot recover from without `reset`."
382 [opts mount-root!]425 [opts mount-root!]
383- (let [{:keys [mouse fps headless auto-quit-ms points-per-cell]426+ (let [{:keys [mouse fps headless auto-quit-ms points-per-cell points-per-row]
384 :or {mouse true fps 60 points-per-cell 1}} opts427 :or {mouse true fps 60 points-per-cell 1}} opts
385- _ (reset! scale (max 1 points-per-cell))428+ _ (reset! col-scale (max 1 points-per-cell))
429+ _ (reset! row-scale (max 1 (or points-per-row (* 2 points-per-cell))))
386 opened? (if headless430 opened? (if headless
387 (ffi/headless! (first headless) (second headless))431 (ffi/headless! (first headless) (second headless))
388 (ffi/open! mouse))]432 (ffi/open! mouse))]