fix: keep grid borders aligned across multiline items

Measure auto-width children intrinsically, constrain them to resolved tracks, account for border-box wrapper content, and align every rendered line before painting the wrapper border.
This commit is contained in:
Kinneyzhang 2026-08-05 20:36:29 +08:00
parent ab9efe62a7
commit d4dc572c2d
2 changed files with 66 additions and 9 deletions

View File

@ -439,6 +439,28 @@ Supported properties are `:grid-column', `:grid-row',
(extra (* gap (max 0 (1- (plist-get entry :column-span))))))
(+ base extra)))
(defun ebox-grid--auto-width-node-p (node)
"Return non-nil when NODE derives its width from the active viewport."
(let ((width (plist-get node :width)))
(or (null width) (eq width 'auto))))
(defun ebox-grid--entry-source (entry rendered width props)
"Return ENTRY's content rendered within WIDTH when it is auto-sized.
Stretching is the default grid item alignment. Re-render an auto-sized
child at its assigned width for that alignment, while preserving its natural
size for start/center/end alignment unless it would overflow its track."
(let* ((node (plist-get entry :node))
(source (gethash node rendered))
(auto-width-p (ebox-grid--auto-width-node-p node))
(justify (or (plist-get props :justify-items) 'stretch))
(stretch-p (memq justify '(normal stretch)))
(overflow-p (> (ebox--string-max-pixel-width source) width)))
(if (and auto-width-p (or stretch-p overflow-p))
(let ((ebox-viewport-width width))
(ebox--render-with-cache node))
source)))
(defun ebox-grid--row-entry-size (entry sizes gap)
"Return the target line size for ENTRY across row SIZES."
(let* ((start (1- (plist-get entry :row)))
@ -450,7 +472,8 @@ Supported properties are `:grid-column', `:grid-row',
(defun ebox-grid--align (string width height justify align)
"Align STRING to WIDTH and HEIGHT using JUSTIFY and ALIGN."
(let ((result (ebox--pixel-reach string width
(let ((result (ebox--lines-justify
string width
(pcase justify
((or 'end 'right) 'right)
('center 'center)
@ -464,10 +487,9 @@ Supported properties are `:grid-column', `:grid-row',
(defun ebox-grid--entry-string (entry rendered widths heights col-gap row-gap props)
"Render and align ENTRY within its grid rectangle."
(let* ((node (plist-get entry :node))
(source (gethash node rendered))
(width (ebox-grid--entry-size entry widths col-gap))
(let* ((width (ebox-grid--entry-size entry widths col-gap))
(height (ebox-grid--row-entry-size entry heights row-gap))
(source (ebox-grid--entry-source entry rendered width props))
(justify (or (plist-get props :justify-items) 'stretch))
(align (or (plist-get props :align-items) 'stretch)))
(ebox-grid--align source width height justify align)))
@ -594,13 +616,18 @@ Supported properties are `:grid-column', `:grid-row',
(available (ebox--nonnegative-horizontal-size-pixels
(plist-get props :width)
(ebox--viewport-pixel-width nil)))
(wrapper (plist-get node :box))
(height (ebox--flex-line-value (plist-get props :height) nil))
(rendered (make-hash-table :test 'eq)))
(when wrapper
(setq available (ebox--box-sizing-content-pixel wrapper available)))
(unless (memq flow '(row column))
(error "ebox-grid: :grid-auto-flow must be `row' or `column'"))
(dolist (entry entries)
(let ((child (plist-get entry :node)))
(puthash child (ebox--render-with-cache child) rendered)))
(let ((ebox-viewport-width nil)
(ebox--intrinsic-layout-measurement t))
(puthash child (ebox--render-with-cache child) rendered))))
(let* ((widths (ebox-grid--resolve-sizes columns column-count 'columns
entries rendered (cdr gaps)
available auto-columns))

View File

@ -37,6 +37,36 @@
(should (= (length lines) 1))
(should (= (ebox--string-pixel-width (car lines)) 120))))
(ert-deftest ebox-grid-constrains-auto-width-children-to-track-size ()
"Auto-width grid children should render within their assigned track."
(let* ((ebox-viewport-width 120)
(node (ebox-grid
:width '(120)
:grid-template-columns '((50) (50))
:gap '(0 (10))
:border "#334155"
(ebox-create :content "A\nA-long" :padding '(0 (4)))
(ebox-create :content "B\nB-long" :padding '(0 (4)))))
(lines (ebox-string-lines (ebox-render node)))
(widths (mapcar #'ebox--string-pixel-width lines)))
(should (equal widths (make-list (length widths) 120)))))
(ert-deftest ebox-grid-aligns-every-line-before-border-render ()
"Grid wrapper borders should stay on one right edge across child lines."
(let* ((node (ebox-grid
:width '(120)
:grid-template-columns '((30) auto 1fr)
:grid-template-rows '(2)
:gap '(1 (4))
:justify-content 'space-between
:border "#334155"
(ebox-create :content "FIXED\n30 PX")
(ebox-create :content "AUTO\nINTRINSIC")
(ebox-create :content "1FR\nREMAINDER")))
(lines (ebox-string-lines (ebox-render node)))
(widths (mapcar #'ebox--string-pixel-width lines)))
(should (equal widths (make-list (length widths) 120)))))
(ert-deftest ebox-grid-keeps-padded-items-inside-column-width ()
"Stack padding should use the widest Grid line instead of its first line."
(let* ((header (ebox-create :content "Header" :width '(720)))