From d4dc572c2d4d8a4f06d623f43a93af03cb79c51e Mon Sep 17 00:00:00 2001 From: Kinneyzhang Date: Wed, 5 Aug 2026 20:36:29 +0800 Subject: [PATCH] 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. --- ebox-grid.el | 45 ++++++++++++++++++++++++++++++++-------- tests/ebox-grid-tests.el | 30 +++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 9 deletions(-) diff --git a/ebox-grid.el b/ebox-grid.el index 48be643..47dbd8c 100644 --- a/ebox-grid.el +++ b/ebox-grid.el @@ -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,11 +472,12 @@ 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 - (pcase justify - ((or 'end 'right) 'right) - ('center 'center) - (_ 'left))))) + (let ((result (ebox--lines-justify + string width + (pcase justify + ((or 'end 'right) 'right) + ('center 'center) + (_ 'left))))) (ebox--lines-align-vertical result height (pcase align @@ -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)) diff --git a/tests/ebox-grid-tests.el b/tests/ebox-grid-tests.el index cf30bec..59ee857 100644 --- a/tests/ebox-grid-tests.el +++ b/tests/ebox-grid-tests.el @@ -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)))