Give the Flutter backlog the gaps it was already asking for
Every message row writes `:margin-top 10` for the gap above it and `:margin-right 10` for clearance from the scrollbar, and the hiccup backend read `:margin` and nothing else — so on Flutter the whole conversation was drawn edge to edge, one message touching the next, while the same tree under libcosmic breathed. `insets-of` reads all three spellings in one place: `:margin` for four sides, a per-side key for one, and the per-side key winning where a box gives both — which is how `message-row` writes `:margin 0` with a top and a right over it. Nil when nothing is asked for, so a box with no margin still gets no Padding. Rows get it too, for the jump button's `:margin-bottom` under the backlog. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
795fda4 parent: 3f26695 modified
flutter/src/frq/hiccup.cljd +44 -22 | @@ -46,6 +46,24 @@ | ||
| 46 | 46 | (cond (number? x) (double x) |
| 47 | 47 | :else default)) |
| 48 | 48 | |
| 49 | +(defn- insets-of | |
| 50 | + "The margin a container asks for, or nil when it asks for none. | |
| 51 | + | |
| 52 | + Three spellings, and a box may mix them: `:margin` is all four sides, the | |
| 53 | + per-side keys are each one side, and a per-side key wins over `:margin` | |
| 54 | + where both are given — that is how `message-row` writes `:margin 0` with a | |
| 55 | + `:margin-top` and a `:margin-right` over it. | |
| 56 | + | |
| 57 | + The per-side keys are not decoration: the backlog's gap between messages and | |
| 58 | + its clearance for the scrollbar are both written that way, so a backend that | |
| 59 | + reads `:margin` alone draws the conversation edge to edge." | |
| 60 | + [p] | |
| 61 | + (let [side (fn [k] (dbl (get p k) (dbl (:margin p) 0.0))) | |
| 62 | + l (side :margin-left) r (side :margin-right) | |
| 63 | + tp (side :margin-top) b (side :margin-bottom)] | |
| 64 | + (when (some pos? [l r tp b]) | |
| 65 | + (m/EdgeInsets.only .left l .right r .top tp .bottom b)))) | |
| 66 | + | |
| 49 | 67 | ;; ------------------------------------------------------------- text entry |
| 50 | 68 | |
| 51 | 69 | (defonce ^:private controllers |
| @@ -359,8 +377,8 @@ | ||
| 359 | 377 | m/MainAxisSize.min) |
| 360 | 378 | .spacing (dbl (:spacing p) 0.0) |
| 361 | 379 | .children (flexed fills-column? (body node))) |
| 362 | - c (if-let [mg (:margin p)] | |
| 363 | - (m/Padding .padding (m/EdgeInsets.all (dbl mg 0.0)) .child c) | |
| 380 | + c (if-let [mg (insets-of p)] | |
| 381 | + (m/Padding .padding mg .child c) | |
| 364 | 382 | c)] |
| 365 | 383 | (if-let [w (width-of p)] |
| 366 | 384 | (m/SizedBox .width w .child c) |
| @@ -391,26 +409,30 @@ | ||
| 391 | 409 | ;; Wrap. Without it the longest message decided the width of the |
| 392 | 410 | ;; conversation and the rest overflowed off the right. |
| 393 | 411 | :hbox |
| 394 | - (if (true? (:wrap p)) | |
| 395 | - (m/Wrap | |
| 396 | - .spacing (dbl (:spacing p) 0.0) | |
| 397 | - .runSpacing (dbl (:spacing p) 0.0) | |
| 398 | - .crossAxisAlignment m/WrapCrossAlignment.center | |
| 399 | - .children kids) | |
| 400 | - (m/Row | |
| 401 | - ;; Stretch when a child fills vertically: a Row hands its children a | |
| 402 | - ;; loose height by default, and a column that wants what is left of it | |
| 403 | - ;; needs a tight one. | |
| 404 | - .crossAxisAlignment (if (some #(fills-column? (expand %)) (body node)) | |
| 405 | - m/CrossAxisAlignment.stretch | |
| 406 | - m/CrossAxisAlignment.center) | |
| 407 | - ;; A row holding something that fills has to be given the width to | |
| 408 | - ;; divide, so it is max rather than min whenever a child asks. | |
| 409 | - .mainAxisSize (if (some #(fills-row? (expand %)) (body node)) | |
| 410 | - m/MainAxisSize.max | |
| 411 | - m/MainAxisSize.min) | |
| 412 | - .spacing (dbl (:spacing p) 0.0) | |
| 413 | - .children (flexed fills-row? (body node)))) | |
| 412 | + (let [row (if (true? (:wrap p)) | |
| 413 | + (m/Wrap | |
| 414 | + .spacing (dbl (:spacing p) 0.0) | |
| 415 | + .runSpacing (dbl (:spacing p) 0.0) | |
| 416 | + .crossAxisAlignment m/WrapCrossAlignment.center | |
| 417 | + .children kids) | |
| 418 | + (m/Row | |
| 419 | + ;; Stretch when a child fills vertically: a Row hands its | |
| 420 | + ;; children a loose height by default, and a column that | |
| 421 | + ;; wants what is left of it needs a tight one. | |
| 422 | + .crossAxisAlignment (if (some #(fills-column? (expand %)) (body node)) | |
| 423 | + m/CrossAxisAlignment.stretch | |
| 424 | + m/CrossAxisAlignment.center) | |
| 425 | + ;; A row holding something that fills has to be given the | |
| 426 | + ;; width to divide, so it is max rather than min whenever a | |
| 427 | + ;; child asks. | |
| 428 | + .mainAxisSize (if (some #(fills-row? (expand %)) (body node)) | |
| 429 | + m/MainAxisSize.max | |
| 430 | + m/MainAxisSize.min) | |
| 431 | + .spacing (dbl (:spacing p) 0.0) | |
| 432 | + .children (flexed fills-row? (body node))))] | |
| 433 | + (if-let [mg (insets-of p)] | |
| 434 | + (m/Padding .padding mg .child row) | |
| 435 | + row)) | |
| 414 | 436 | |
| 415 | 437 | :label (txt ctx (:label p "") t/text-body t/on-bg) |
| 416 | 438 | :dim-label (txt ctx (:label p "") t/text-caption t/dim) |
| @@ -46,6 +46,24 @@ | |||
| 46 | (cond (number? x) (double x) | 46 | (cond (number? x) (double x) |
| 47 | :else default)) | 47 | :else default)) |
| 48 | 48 | ||
| 49 | +(defn- insets-of | ||
| 50 | + "The margin a container asks for, or nil when it asks for none. | ||
| 51 | + | ||
| 52 | + Three spellings, and a box may mix them: `:margin` is all four sides, the | ||
| 53 | + per-side keys are each one side, and a per-side key wins over `:margin` | ||
| 54 | + where both are given — that is how `message-row` writes `:margin 0` with a | ||
| 55 | + `:margin-top` and a `:margin-right` over it. | ||
| 56 | + | ||
| 57 | + The per-side keys are not decoration: the backlog's gap between messages and | ||
| 58 | + its clearance for the scrollbar are both written that way, so a backend that | ||
| 59 | + reads `:margin` alone draws the conversation edge to edge." | ||
| 60 | + [p] | ||
| 61 | + (let [side (fn [k] (dbl (get p k) (dbl (:margin p) 0.0))) | ||
| 62 | + l (side :margin-left) r (side :margin-right) | ||
| 63 | + tp (side :margin-top) b (side :margin-bottom)] | ||
| 64 | + (when (some pos? [l r tp b]) | ||
| 65 | + (m/EdgeInsets.only .left l .right r .top tp .bottom b)))) | ||
| 66 | + | ||
| 49 | ;; ------------------------------------------------------------- text entry | 67 | ;; ------------------------------------------------------------- text entry |
| 50 | 68 | ||
| 51 | (defonce ^:private controllers | 69 | (defonce ^:private controllers |
| @@ -359,8 +377,8 @@ | |||
| 359 | m/MainAxisSize.min) | 377 | m/MainAxisSize.min) |
| 360 | .spacing (dbl (:spacing p) 0.0) | 378 | .spacing (dbl (:spacing p) 0.0) |
| 361 | .children (flexed fills-column? (body node))) | 379 | .children (flexed fills-column? (body node))) |
| 362 | - c (if-let [mg (:margin p)] | 380 | + c (if-let [mg (insets-of p)] |
| 363 | - (m/Padding .padding (m/EdgeInsets.all (dbl mg 0.0)) .child c) | 381 | + (m/Padding .padding mg .child c) |
| 364 | c)] | 382 | c)] |
| 365 | (if-let [w (width-of p)] | 383 | (if-let [w (width-of p)] |
| 366 | (m/SizedBox .width w .child c) | 384 | (m/SizedBox .width w .child c) |
| @@ -391,26 +409,30 @@ | |||
| 391 | ;; Wrap. Without it the longest message decided the width of the | 409 | ;; Wrap. Without it the longest message decided the width of the |
| 392 | ;; conversation and the rest overflowed off the right. | 410 | ;; conversation and the rest overflowed off the right. |
| 393 | :hbox | 411 | :hbox |
| 394 | - (if (true? (:wrap p)) | 412 | + (let [row (if (true? (:wrap p)) |
| 395 | - (m/Wrap | 413 | + (m/Wrap |
| 396 | - .spacing (dbl (:spacing p) 0.0) | 414 | + .spacing (dbl (:spacing p) 0.0) |
| 397 | - .runSpacing (dbl (:spacing p) 0.0) | 415 | + .runSpacing (dbl (:spacing p) 0.0) |
| 398 | - .crossAxisAlignment m/WrapCrossAlignment.center | 416 | + .crossAxisAlignment m/WrapCrossAlignment.center |
| 399 | - .children kids) | 417 | + .children kids) |
| 400 | - (m/Row | 418 | + (m/Row |
| 401 | - ;; Stretch when a child fills vertically: a Row hands its children a | 419 | + ;; Stretch when a child fills vertically: a Row hands its |
| 402 | - ;; loose height by default, and a column that wants what is left of it | 420 | + ;; children a loose height by default, and a column that |
| 403 | - ;; needs a tight one. | 421 | + ;; wants what is left of it needs a tight one. |
| 404 | - .crossAxisAlignment (if (some #(fills-column? (expand %)) (body node)) | 422 | + .crossAxisAlignment (if (some #(fills-column? (expand %)) (body node)) |
| 405 | - m/CrossAxisAlignment.stretch | 423 | + m/CrossAxisAlignment.stretch |
| 406 | - m/CrossAxisAlignment.center) | 424 | + m/CrossAxisAlignment.center) |
| 407 | - ;; A row holding something that fills has to be given the width to | 425 | + ;; A row holding something that fills has to be given the |
| 408 | - ;; divide, so it is max rather than min whenever a child asks. | 426 | + ;; width to divide, so it is max rather than min whenever a |
| 409 | - .mainAxisSize (if (some #(fills-row? (expand %)) (body node)) | 427 | + ;; child asks. |
| 410 | - m/MainAxisSize.max | 428 | + .mainAxisSize (if (some #(fills-row? (expand %)) (body node)) |
| 411 | - m/MainAxisSize.min) | 429 | + m/MainAxisSize.max |
| 412 | - .spacing (dbl (:spacing p) 0.0) | 430 | + m/MainAxisSize.min) |
| 413 | - .children (flexed fills-row? (body node)))) | 431 | + .spacing (dbl (:spacing p) 0.0) |
| 432 | + .children (flexed fills-row? (body node))))] | ||
| 433 | + (if-let [mg (insets-of p)] | ||
| 434 | + (m/Padding .padding mg .child row) | ||
| 435 | + row)) | ||
| 414 | 436 | ||
| 415 | :label (txt ctx (:label p "") t/text-body t/on-bg) | 437 | :label (txt ctx (:label p "") t/text-body t/on-bg) |
| 416 | :dim-label (txt ctx (:label p "") t/text-caption t/dim) | 438 | :dim-label (txt ctx (:label p "") t/text-caption t/dim) |