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

Report Enter from a field, and take its width request

Two of the props a diff of frq's call sites against what this backend
reads turned up. Fifty-four of frq's five hundred and twenty-eight prop
uses go unread; these are the ones that break function rather than looks.

:on-activate is Enter, and Enter is the one key a text field must not
swallow as input — frq sends its message on it. A compose box that
accepted every character and never reported Enter takes a message and has
no way to say it is finished: typing works, sending does not, and nothing
looks broken. Recorded as widget state rather than returned, because
text-entry already answers its text and a second return value would
change every existing call.

:width-request is a minimum width, and :hexpand whether to take the rest
of the row. Eight fields in frq ask for a width and were all getting the
default.

Still unread, and listed so the next pass has somewhere to start:
scroll's stick-to-bottom, scroll-to-bottom, scroll-key, reserve, on-scroll
and on-change; hbox's wrap and align; vbox's fill-height, width-request,
reserve and margins; entry's rows and on-paste-empty; image's upscale.
The scroll ones matter most — a chat that does not follow new messages is
the next thing anyone will notice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-09-10T06:24:33-07:00 Browse files
c03a752 parent: c131fc6
modified glimmer-backends/glimmer-jvui/src/glimmer_jvui/core.clj +14 -2
@@ -219,9 +219,21 @@
219219 (:entry :text-entry)
220220 (let [was (str (or (:value props) (:text props) ""))
221221 id (c/next-id key)
222- now (w/text-entry was {:key key :placeholder (:placeholder props)})]
222+ now (w/text-entry was {:key key
223+ :placeholder (:placeholder props)
224+ ;; :width-request is what frq and
225+ ;; libvidya call a minimum width;
226+ ;; :hexpand says take the rest of the
227+ ;; row, which is this widget's default.
228+ :min-width (:width-request props)
229+ :expand (if (false? (:hexpand props))
230+ :none :horizontal)})]
223231 (record! n id)
224- (when (not= now was) (fire! n :on-change now)))
232+ (when (not= now was) (fire! n :on-change now))
233+ ;; Enter, which a field must not swallow as input: frq sends its
234+ ;; message on it, and without this the compose box accepted text
235+ ;; and had no way to say it was finished.
236+ (when (w/entry-activated? id) (fire! n :on-activate now)))
225237
226238 :progress (w/progress (num (:value props) 0.0))
227239 :separator (w/separator)
@@ -219,9 +219,21 @@
219 (:entry :text-entry)219 (:entry :text-entry)
220 (let [was (str (or (:value props) (:text props) ""))220 (let [was (str (or (:value props) (:text props) ""))
221 id (c/next-id key)221 id (c/next-id key)
222- now (w/text-entry was {:key key :placeholder (:placeholder props)})]222+ now (w/text-entry was {:key key
223+ :placeholder (:placeholder props)
224+ ;; :width-request is what frq and
225+ ;; libvidya call a minimum width;
226+ ;; :hexpand says take the rest of the
227+ ;; row, which is this widget's default.
228+ :min-width (:width-request props)
229+ :expand (if (false? (:hexpand props))
230+ :none :horizontal)})]
223 (record! n id)231 (record! n id)
224- (when (not= now was) (fire! n :on-change now)))232+ (when (not= now was) (fire! n :on-change now))
233+ ;; Enter, which a field must not swallow as input: frq sends its
234+ ;; message on it, and without this the compose box accepted text
235+ ;; and had no way to say it was finished.
236+ (when (w/entry-activated? id) (fire! n :on-activate now)))
225 237
226 :progress (w/progress (num (:value props) 0.0))238 :progress (w/progress (num (:value props) 0.0))
227 :separator (w/separator)239 :separator (w/separator)
modified glimmer-backends/glimmer-jvui/test/glimmer_jvui/tests.clj +20 -0
@@ -149,6 +149,25 @@
149149 (check! (= "hi" (ra/deref text))
150150 (str "typed text reached :on-change: " (pr-str (ra/deref text))))))
151151
152+(defn- check-enter-sends! []
153+ ;; Enter is the one key a field must not swallow. frq sends its message
154+ ;; on it, so a compose box that accepted text and never reported Enter
155+ ;; would take a message and have no way to say it was finished — typing
156+ ;; works, sending does not, and nothing looks broken.
157+ (let [root (jv/root-node) cx (ctx)
158+ text (ra/atom "hello")
159+ sent (ra/atom nil)]
160+ (gui/mount root :page
161+ [(fn [] [:entry {:value (ra/deref text)
162+ :on-change #(ra/reset! text %)
163+ :on-activate #(ra/reset! sent %)}])])
164+ (jv/render-once root cx)
165+ (let [[ex ey] (centre (tagged root :entry))]
166+ (jv/render-once root cx (click-at ex ey))) ; take focus
167+ (jv/render-once root cx [{:kind :key-down :key :return}])
168+ (check! (= "hello" (ra/deref sent))
169+ (str "Enter reached :on-activate: " (pr-str (ra/deref sent))))))
170+
152171 (defn- check-checkbox-round-trips! []
153172 (let [root (jv/root-node) cx (ctx)]
154173 (ra/reset! on? false)
@@ -169,6 +188,7 @@
169188 ["a press alone does not" check-press-alone-is-not-a-click!]
170189 ["a reorder keeps identity" check-reorder-keeps-identity!]
171190 ["an unknown tag is a container" check-unknown-tag-is-a-container!]
191+ ["Enter sends" check-enter-sends!]
172192 ["an entry round-trips" check-entry-round-trips!]
173193 ["a checkbox round-trips" check-checkbox-round-trips!]])
174194
@@ -149,6 +149,25 @@
149 (check! (= "hi" (ra/deref text))149 (check! (= "hi" (ra/deref text))
150 (str "typed text reached :on-change: " (pr-str (ra/deref text))))))150 (str "typed text reached :on-change: " (pr-str (ra/deref text))))))
151 151
152+(defn- check-enter-sends! []
153+ ;; Enter is the one key a field must not swallow. frq sends its message
154+ ;; on it, so a compose box that accepted text and never reported Enter
155+ ;; would take a message and have no way to say it was finished — typing
156+ ;; works, sending does not, and nothing looks broken.
157+ (let [root (jv/root-node) cx (ctx)
158+ text (ra/atom "hello")
159+ sent (ra/atom nil)]
160+ (gui/mount root :page
161+ [(fn [] [:entry {:value (ra/deref text)
162+ :on-change #(ra/reset! text %)
163+ :on-activate #(ra/reset! sent %)}])])
164+ (jv/render-once root cx)
165+ (let [[ex ey] (centre (tagged root :entry))]
166+ (jv/render-once root cx (click-at ex ey))) ; take focus
167+ (jv/render-once root cx [{:kind :key-down :key :return}])
168+ (check! (= "hello" (ra/deref sent))
169+ (str "Enter reached :on-activate: " (pr-str (ra/deref sent))))))
170+
152 (defn- check-checkbox-round-trips! []171 (defn- check-checkbox-round-trips! []
153 (let [root (jv/root-node) cx (ctx)]172 (let [root (jv/root-node) cx (ctx)]
154 (ra/reset! on? false)173 (ra/reset! on? false)
@@ -169,6 +188,7 @@
169 ["a press alone does not" check-press-alone-is-not-a-click!]188 ["a press alone does not" check-press-alone-is-not-a-click!]
170 ["a reorder keeps identity" check-reorder-keeps-identity!]189 ["a reorder keeps identity" check-reorder-keeps-identity!]
171 ["an unknown tag is a container" check-unknown-tag-is-a-container!]190 ["an unknown tag is a container" check-unknown-tag-is-a-container!]
191+ ["Enter sends" check-enter-sends!]
172 ["an entry round-trips" check-entry-round-trips!]192 ["an entry round-trips" check-entry-round-trips!]
173 ["a checkbox round-trips" check-checkbox-round-trips!]])193 ["a checkbox round-trips" check-checkbox-round-trips!]])
174 194
modified jvui/src/jvui/widgets.clj +19 -2
@@ -277,18 +277,28 @@
277277
278278 (defn- clamp [v lo hi] (max lo (min hi v)))
279279
280+(defn entry-activated?
281+ "Did the field under `id` see Enter on the frame just walked?
282+
283+ Asked after `text-entry`, which records it. Enter is the one key a text
284+ field must NOT treat as input — the client sends on it — and a widget
285+ that answered only its text gave a caller no way to know."
286+ [id]
287+ (boolean (c/state id :activated false)))
288+
280289 (defn text-entry
281290 "A single-line editable string. Answers the text after this frame.
282291
283292 The caret is an index into the string kept under the widget's id, which is
284293 the one piece of state a text field cannot recompute from its value."
285294 ([value] (text-entry value {}))
286- ([value {:keys [key expand placeholder] :or {expand :horizontal}}]
295+ ([value {:keys [key expand placeholder min-width]
296+ :or {expand :horizontal}}]
287297 (let [size (c/th :font-size)
288298 pad (c/th :padding)
289299 h (double (c/th :control-height))
290300 id (c/next-id key)
291- rect (c/leaf [160.0 h] expand [0.0 0.5])
301+ rect (c/leaf [(double (or min-width 160.0)) h] expand [0.0 0.5])
292302 {:keys [hover? focused?]} (c/interact! id rect)
293303 [rx ry rw rh] rect
294304 s (str value)
@@ -319,6 +329,13 @@
319329 [s caret0] (or evs []))
320330 caret' (clamp caret' 0 (count s'))]
321331 (c/state! id :caret caret')
332+ ;; Enter is not an edit and must not be swallowed as one: a client
333+ ;; sends its message on it. Recorded as state rather than returned,
334+ ;; because `text-entry` already answers the text and a second return
335+ ;; value would change every existing call.
336+ (c/state! id :activated
337+ (boolean (some #(and (= :key-down (:kind %)) (= :return (:key %)))
338+ (or evs []))))
322339 (c/fill! rect (c/th :surface) (c/th :radius)
323340 (if focused? (c/th :focus) (c/th :border))
324341 (if focused? 2.0 (c/th :border-width)))
@@ -277,18 +277,28 @@
277 277
278 (defn- clamp [v lo hi] (max lo (min hi v)))278 (defn- clamp [v lo hi] (max lo (min hi v)))
279 279
280+(defn entry-activated?
281+ "Did the field under `id` see Enter on the frame just walked?
282+
283+ Asked after `text-entry`, which records it. Enter is the one key a text
284+ field must NOT treat as input — the client sends on it — and a widget
285+ that answered only its text gave a caller no way to know."
286+ [id]
287+ (boolean (c/state id :activated false)))
288+
280 (defn text-entry289 (defn text-entry
281 "A single-line editable string. Answers the text after this frame.290 "A single-line editable string. Answers the text after this frame.
282 291
283 The caret is an index into the string kept under the widget's id, which is292 The caret is an index into the string kept under the widget's id, which is
284 the one piece of state a text field cannot recompute from its value."293 the one piece of state a text field cannot recompute from its value."
285 ([value] (text-entry value {}))294 ([value] (text-entry value {}))
286- ([value {:keys [key expand placeholder] :or {expand :horizontal}}]295+ ([value {:keys [key expand placeholder min-width]
296+ :or {expand :horizontal}}]
287 (let [size (c/th :font-size)297 (let [size (c/th :font-size)
288 pad (c/th :padding)298 pad (c/th :padding)
289 h (double (c/th :control-height))299 h (double (c/th :control-height))
290 id (c/next-id key)300 id (c/next-id key)
291- rect (c/leaf [160.0 h] expand [0.0 0.5])301+ rect (c/leaf [(double (or min-width 160.0)) h] expand [0.0 0.5])
292 {:keys [hover? focused?]} (c/interact! id rect)302 {:keys [hover? focused?]} (c/interact! id rect)
293 [rx ry rw rh] rect303 [rx ry rw rh] rect
294 s (str value)304 s (str value)
@@ -319,6 +329,13 @@
319 [s caret0] (or evs []))329 [s caret0] (or evs []))
320 caret' (clamp caret' 0 (count s'))]330 caret' (clamp caret' 0 (count s'))]
321 (c/state! id :caret caret')331 (c/state! id :caret caret')
332+ ;; Enter is not an edit and must not be swallowed as one: a client
333+ ;; sends its message on it. Recorded as state rather than returned,
334+ ;; because `text-entry` already answers the text and a second return
335+ ;; value would change every existing call.
336+ (c/state! id :activated
337+ (boolean (some #(and (= :key-down (:kind %)) (= :return (:key %)))
338+ (or evs []))))
322 (c/fill! rect (c/th :surface) (c/th :radius)339 (c/fill! rect (c/th :surface) (c/th :radius)
323 (if focused? (c/th :focus) (c/th :border))340 (if focused? (c/th :focus) (c/th :border))
324 (if focused? 2.0 (c/th :border-width)))341 (if focused? 2.0 (c/th :border-width)))