Split the verified renderer, layout engine, Grid support, native boundary, tests, examples, and paired documentation into the independent Ebox repository. Keep ETAF and application concerns outside this package.
649 lines
28 KiB
EmacsLisp
649 lines
28 KiB
EmacsLisp
;;; ebox-grid.el --- Grid formatting context for Ebox -*- lexical-binding: t; -*-
|
|
|
|
;; SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
;;; Commentary:
|
|
;; Grid is a two-dimensional layout node. It deliberately reuses Ebox's
|
|
;; existing pixel/line units, box renderer, and retained tree identity rather
|
|
;; than introducing a second styling or measurement model.
|
|
|
|
;;; Code:
|
|
|
|
(require 'cl-lib)
|
|
(require 'seq)
|
|
(require 'subr-x)
|
|
(require 'ebox-layout)
|
|
(require 'ebox-flex)
|
|
|
|
(declare-function ebox--ensure-node-id "ebox" (node))
|
|
(declare-function ebox--ensure-region-id "ebox" (box))
|
|
(declare-function ebox-create "ebox" (&rest plist))
|
|
(declare-function ebox-render "ebox-layout" (node))
|
|
(declare-function ebox--lines-concat-horizontal "ebox" (&rest strings))
|
|
(declare-function ebox-string-lines "ebox" (string))
|
|
(declare-function ebox-string-height "ebox" (string))
|
|
(declare-function ebox-lines-join "ebox" (lines))
|
|
(declare-function ebox--lines-align-vertical "ebox-layout" (string height align))
|
|
(declare-function ebox--flex-spacing "ebox-flex" (mode leftover count base-gap))
|
|
(declare-function ebox--render-with-cache "ebox-incremental" (node &optional force cache-probe))
|
|
(defvar ebox--region-box-table)
|
|
|
|
(defconst ebox--grid-layout-prop-keys
|
|
'(:grid-template-columns :grid-template-rows
|
|
:grid-auto-columns :grid-auto-rows :grid-auto-flow
|
|
:grid-column-gap :grid-row-gap :gap
|
|
:justify-items :align-items :justify-content :align-content)
|
|
"Properties owned by the grid formatting context.")
|
|
|
|
(defconst ebox--grid-item-prop-keys
|
|
'(:grid-column :grid-row :grid-column-span :grid-row-span)
|
|
"Properties that describe a child's position in a grid.")
|
|
|
|
(defun ebox-grid--split-attrs (items)
|
|
"Split grid ITEMS into a property plist and child list."
|
|
(let (props children)
|
|
(while items
|
|
(let ((item (pop items)))
|
|
(if (keywordp item)
|
|
(progn
|
|
(unless items
|
|
(error "ebox-grid: missing value for %S" item))
|
|
(push item props)
|
|
(push (pop items) props))
|
|
(push item children))))
|
|
(cons (nreverse props) (nreverse children))))
|
|
|
|
(defun ebox-grid--track-form-p (value)
|
|
"Return non-nil when VALUE is one composite track expression."
|
|
(and (consp value)
|
|
(memq (car value) '(fr minmax repeat))))
|
|
|
|
(defun ebox-grid--track-list (value)
|
|
"Return VALUE as a list of track expressions."
|
|
(cond
|
|
((null value) nil)
|
|
((ebox-grid--track-form-p value) (list value))
|
|
((or (numberp value) (symbolp value)) (list value))
|
|
(t value)))
|
|
|
|
(defun ebox-grid--expand-repeat (tracks)
|
|
"Expand REPEAT expressions in TRACKS."
|
|
(cl-mapcan
|
|
(lambda (track)
|
|
(if (and (consp track) (eq (car track) 'repeat))
|
|
(let ((count (cadr track))
|
|
(value (caddr track)))
|
|
(unless (and (null (cdddr track))
|
|
(integerp count) (> count 0) value)
|
|
(error "ebox-grid: invalid repeat track %S" track))
|
|
(make-list count value))
|
|
(list track)))
|
|
tracks))
|
|
|
|
(defun ebox-grid--normalize-track (value axis)
|
|
"Normalize one track VALUE for AXIS (`columns' or `rows')."
|
|
(setq value (if (and (consp value) (eq (car value) 'quote))
|
|
(cadr value)
|
|
value))
|
|
(cond
|
|
((eq value 'auto) '(:kind auto :factor 0))
|
|
((and (symbolp value)
|
|
(string-match "\\`\\([0-9]+\\(?:\\.[0-9]+\\)?\\)fr\\'"
|
|
(symbol-name value)))
|
|
(list :kind 'fr :factor
|
|
(string-to-number (match-string 1 (symbol-name value)))))
|
|
((and (consp value) (eq (car value) 'fr))
|
|
(unless (and (numberp (cadr value)) (> (cadr value) 0))
|
|
(error "ebox-grid: invalid fractional track %S" value))
|
|
(list :kind 'fr :factor (cadr value)))
|
|
((and (consp value) (eq (car value) 'minmax))
|
|
(unless (and (consp (cdr value))
|
|
(consp (cddr value))
|
|
(null (cdddr value)))
|
|
(error "ebox-grid: invalid minmax track %S" value))
|
|
(let ((minimum (ebox-grid--normalize-track (cadr value) axis))
|
|
(maximum (ebox-grid--normalize-track (caddr value) axis)))
|
|
(when (and (eq (plist-get maximum :kind) 'fixed)
|
|
(eq (plist-get minimum :kind) 'fixed)
|
|
(> (plist-get minimum :size) (plist-get maximum :size)))
|
|
(error "ebox-grid: minmax minimum exceeds maximum: %S" value))
|
|
(list :kind 'minmax :min minimum :max maximum)))
|
|
((eq axis 'columns)
|
|
(list :kind 'fixed
|
|
:size (ebox--nonnegative-horizontal-size-pixels value 0)))
|
|
(t
|
|
(let ((lines (ebox--flex-line-value value 0)))
|
|
(list :kind 'fixed :size lines)))))
|
|
|
|
(defun ebox-grid--normalize-tracks (value axis)
|
|
"Normalize a track template VALUE along AXIS."
|
|
(mapcar (lambda (track) (ebox-grid--normalize-track track axis))
|
|
(ebox-grid--expand-repeat (ebox-grid--track-list value))))
|
|
|
|
(defun ebox-grid--gap-pair (props)
|
|
"Return normalized (ROW-GAP . COLUMN-GAP) from grid PROPS."
|
|
(let* ((pair (ebox--flex-gap-pair (plist-get props :gap)))
|
|
(row (if (plist-member props :grid-row-gap)
|
|
(plist-get props :grid-row-gap)
|
|
(car pair)))
|
|
(column (if (plist-member props :grid-column-gap)
|
|
(plist-get props :grid-column-gap)
|
|
(cdr pair))))
|
|
(cons (or (ebox--flex-line-value row 0) 0)
|
|
(or (ebox--nonnegative-horizontal-size-pixels column 0) 0))))
|
|
|
|
(defun ebox-grid--visual-props-p (props)
|
|
"Return non-nil when PROPS contains visible box behavior."
|
|
(cl-loop for (key _value) on props by #'cddr
|
|
thereis (not (memq key (append ebox--grid-layout-prop-keys
|
|
'(:width :height :box-sizing)
|
|
ebox--grid-item-prop-keys)))))
|
|
|
|
(defun ebox-grid--wrapper (props)
|
|
"Create a visual wrapper for grid PROPS, or return nil."
|
|
(when (ebox-grid--visual-props-p props)
|
|
(when (plist-member props :content)
|
|
(error "ebox-grid: grid cannot combine :content with child nodes"))
|
|
(apply #'ebox-create
|
|
(append (ebox--preformatted-box-props props)
|
|
(list :content "")))))
|
|
|
|
;;;###autoload
|
|
(defun ebox-grid-fr (factor)
|
|
"Return a fractional grid track with FACTOR weight."
|
|
(unless (and (numberp factor) (> factor 0))
|
|
(error "ebox-grid-fr: FACTOR must be positive: %S" factor))
|
|
(list 'fr factor))
|
|
|
|
;;;###autoload
|
|
(defun ebox-grid-item (node &rest props)
|
|
"Return NODE with grid placement PROPS attached.
|
|
Supported properties are `:grid-column', `:grid-row',
|
|
`:grid-column-span', and `:grid-row-span'."
|
|
(unless (and (listp node) (plist-member node :ebox-type))
|
|
(error "ebox-grid-item: NODE is not an Ebox node: %S" node))
|
|
(let ((copy (copy-sequence node)))
|
|
(while props
|
|
(let* ((key (pop props))
|
|
(value (progn
|
|
(unless props
|
|
(error "ebox-grid-item: missing value for %S" key))
|
|
(pop props))))
|
|
(unless (memq key ebox--grid-item-prop-keys)
|
|
(error "ebox-grid-item: unsupported property %S" key))
|
|
(plist-put copy key value)))
|
|
copy))
|
|
|
|
(defun ebox-grid--placement-part (value default)
|
|
"Return (START SPAN) parsed from placement VALUE and DEFAULT."
|
|
(cond
|
|
((null value) (list default 1))
|
|
((numberp value)
|
|
(if (> value 0)
|
|
(list value 1)
|
|
(error "ebox-grid: placement starts at one: %S" value)))
|
|
((and (consp value) (numberp (car value)))
|
|
(let ((start (car value))
|
|
(tail (cdr value)))
|
|
(unless (> start 0)
|
|
(error "ebox-grid: placement starts at one: %S" value))
|
|
(cond
|
|
((null tail) (list start 1))
|
|
((and (eq (car tail) :span) (numberp (cadr tail)))
|
|
(list start (cadr tail)))
|
|
((numberp (car tail))
|
|
(list start (- (car tail) start)))
|
|
(t (error "ebox-grid: invalid placement %S" value)))))
|
|
(t (error "ebox-grid: invalid placement %S" value))))
|
|
|
|
(defun ebox-grid--placement (child)
|
|
"Return CHILD placement as (ROW COL ROW-SPAN COL-SPAN)."
|
|
(let* ((row (ebox-grid--placement-part (plist-get child :grid-row) 0))
|
|
(column (ebox-grid--placement-part
|
|
(plist-get child :grid-column) 0))
|
|
(row-span (or (plist-get child :grid-row-span) (cadr row)))
|
|
(column-span (or (plist-get child :grid-column-span) (cadr column))))
|
|
(dolist (part (list row column))
|
|
(when (or (not (integerp (car part))) (< (car part) 0)
|
|
(not (integerp (cadr part))) (< (cadr part) 1))
|
|
(error "ebox-grid: invalid placement %S" part)))
|
|
(unless (and (integerp row-span) (> row-span 0)
|
|
(integerp column-span) (> column-span 0))
|
|
(error "ebox-grid: grid spans must be positive integers"))
|
|
(list (car row) (car column) row-span column-span)))
|
|
|
|
(defun ebox-grid--occupancy (rows columns)
|
|
"Return an empty ROWS by COLUMNS occupancy matrix."
|
|
(let ((matrix (make-vector rows nil)))
|
|
(cl-loop for row from 0 below rows
|
|
do (aset matrix row (make-vector columns nil))
|
|
finally return matrix)))
|
|
|
|
(defun ebox-grid--grow (matrix rows columns)
|
|
"Grow MATRIX to at least ROWS and COLUMNS and return it."
|
|
(let* ((result matrix)
|
|
(existing-columns
|
|
(if (> (length result) 0)
|
|
(length (aref result 0))
|
|
0))
|
|
(target-columns (max columns existing-columns)))
|
|
(when (> columns 0)
|
|
(dotimes (row (length result))
|
|
(when (< (length (aref result row)) target-columns)
|
|
(setf (aref result row)
|
|
(vconcat (aref result row)
|
|
(make-vector
|
|
(- target-columns (length (aref result row)))
|
|
nil))))))
|
|
(when (> rows (length result))
|
|
(setq result
|
|
(vconcat result
|
|
(make-vector (- rows (length result))
|
|
(make-vector target-columns nil)))))
|
|
result))
|
|
|
|
(defun ebox-grid--free-p (matrix row column row-span column-span)
|
|
"Return non-nil when a placement rectangle is free in MATRIX."
|
|
(and (> row 0) (> column 0)
|
|
(cl-loop for r from (1- row) below (+ (1- row) row-span)
|
|
always (cl-loop for c from (1- column)
|
|
below (+ (1- column) column-span)
|
|
always (not (aref (aref matrix r) c))))))
|
|
|
|
(defun ebox-grid--mark (matrix entry row column row-span column-span)
|
|
"Mark ENTRY in placement rectangle MATRIX."
|
|
(cl-loop for r from (1- row) below (+ (1- row) row-span) do
|
|
(cl-loop for c from (1- column)
|
|
below (+ (1- column) column-span) do
|
|
(setf (aref (aref matrix r) c) entry))))
|
|
|
|
(defun ebox-grid--find-position (matrix placement flow)
|
|
"Find a free position for PLACEMENT in MATRIX using FLOW."
|
|
(let ((row (nth 0 placement))
|
|
(column (nth 1 placement))
|
|
(row-span (nth 2 placement))
|
|
(column-span (nth 3 placement)))
|
|
(cl-labels
|
|
((free-at-p (r c)
|
|
(and (<= (+ r row-span -1) (length matrix))
|
|
(<= (+ c column-span -1)
|
|
(length (aref matrix 0)))
|
|
(ebox-grid--free-p matrix r c row-span column-span)))
|
|
(scan-row (r start)
|
|
(cl-loop for c from start to (length (aref matrix 0))
|
|
when (free-at-p r c) return (list r c)))
|
|
(scan-column (c start)
|
|
(cl-loop for r from start to (length matrix)
|
|
when (free-at-p r c) return (list r c)))
|
|
(scan-flow ()
|
|
(or (if (eq flow 'column)
|
|
(cl-loop for c from 1 to (length (aref matrix 0))
|
|
thereis (scan-column c 1))
|
|
(cl-loop for r from 1 to (length matrix)
|
|
thereis (scan-row r 1)))
|
|
(if (eq flow 'column)
|
|
(list 1 (1+ (length (aref matrix 0))))
|
|
(list (1+ (length matrix)) 1)))))
|
|
(cond
|
|
((and (> row 0) (> column 0)) (list row column))
|
|
((> row 0)
|
|
(or (scan-row row (if (> column 0) column 1))
|
|
(list (1+ (length matrix)) 1)))
|
|
((> column 0)
|
|
(or (scan-column column 1)
|
|
(list (1+ (length matrix)) 1)))
|
|
(t (scan-flow))))))
|
|
|
|
(defun ebox-grid--place-children (children columns rows flow)
|
|
"Place CHILDREN and return (ENTRIES MATRIX ROWS COLUMNS)."
|
|
(let ((matrix (ebox-grid--occupancy rows columns))
|
|
entries)
|
|
(dolist (child children)
|
|
(let* ((placement (ebox-grid--placement child))
|
|
(row-span (nth 2 placement))
|
|
(column-span (nth 3 placement))
|
|
(row (nth 0 placement))
|
|
(column (nth 1 placement))
|
|
(explicit-p (and (> row 0) (> column 0)))
|
|
(candidate-row (if (> row 0) row 1))
|
|
(candidate-column (if (> column 0) column 1)))
|
|
(setq matrix (ebox-grid--grow matrix
|
|
(+ candidate-row row-span -1)
|
|
(+ candidate-column column-span -1)))
|
|
(if (ebox-grid--free-p matrix candidate-row candidate-column
|
|
row-span column-span)
|
|
(setq row candidate-row column candidate-column)
|
|
(when explicit-p
|
|
(error "ebox-grid: overlapping explicit placement at row %S column %S"
|
|
row column))
|
|
(let ((position (ebox-grid--find-position
|
|
matrix (list row column row-span column-span) flow)))
|
|
(setq row (car position) column (cadr position))
|
|
(setq matrix (ebox-grid--grow matrix (+ row row-span -1)
|
|
(+ column column-span -1)))))
|
|
(let ((entry (list :node child :row row :column column
|
|
:row-span row-span :column-span column-span)))
|
|
(ebox-grid--mark matrix entry row column row-span column-span)
|
|
(push entry entries))))
|
|
(list (nreverse entries) matrix (length matrix)
|
|
(length (aref matrix 0)))))
|
|
|
|
(defun ebox-grid--track-intrinsic (track axis entries rendered key slot-index)
|
|
"Return intrinsic size for TRACK from RENDERED grid ENTRIES."
|
|
(pcase (plist-get track :kind)
|
|
('fixed (plist-get track :size))
|
|
('fr 0)
|
|
('minmax
|
|
(ebox-grid--track-intrinsic (plist-get track :min)
|
|
axis entries rendered key slot-index))
|
|
(_ (let ((position (1+ slot-index))
|
|
(best 0))
|
|
(dolist (entry entries best)
|
|
(when (and (<= (plist-get entry key) position)
|
|
(< position
|
|
(+ (plist-get entry key)
|
|
(plist-get entry
|
|
(if (eq key :column)
|
|
:column-span
|
|
:row-span)))))
|
|
(let* ((node (plist-get entry :node))
|
|
(string (gethash node rendered))
|
|
(value (if (eq axis 'columns)
|
|
(ebox--string-pixel-width string)
|
|
(ebox-string-height string))))
|
|
(setq best (max best value)))))))))
|
|
|
|
(defun ebox-grid--sum (values)
|
|
"Return the numeric sum of VALUES."
|
|
(apply #'+ (or values '(0))))
|
|
|
|
(defun ebox-grid--track-max (track)
|
|
"Return TRACK's fixed maximum, or nil when it is unbounded."
|
|
(pcase (plist-get track :kind)
|
|
('fixed (plist-get track :size))
|
|
('minmax (ebox-grid--track-max (plist-get track :max)))
|
|
(_ nil)))
|
|
|
|
(defun ebox-grid--clamp-track-size (track size)
|
|
"Clamp SIZE to TRACK's fixed maximum when one exists."
|
|
(if-let ((maximum (ebox-grid--track-max track)))
|
|
(min size maximum)
|
|
size))
|
|
|
|
(defun ebox-grid--resolve-sizes
|
|
(tracks count axis entries rendered gap available &optional auto-track)
|
|
"Resolve TRACKS to COUNT sizes along AXIS using AUTO-TRACK for implicit tracks."
|
|
(let* ((auto-track (or auto-track '(:kind auto :factor 0)))
|
|
(tracks (append tracks nil))
|
|
(tracks (append tracks
|
|
(make-list (max 0 (- count (length tracks)))
|
|
auto-track)))
|
|
(count (length tracks))
|
|
(sizes (cl-loop for track in tracks
|
|
for index from 0
|
|
collect (ebox-grid--clamp-track-size
|
|
track
|
|
(ebox-grid--track-intrinsic
|
|
track axis entries rendered
|
|
(if (eq axis 'columns) :column :row)
|
|
index))))
|
|
(fr-total (apply #'+
|
|
(mapcar (lambda (track)
|
|
(if (eq (plist-get track :kind) 'fr)
|
|
(plist-get track :factor) 0))
|
|
tracks)))
|
|
(used (+ (ebox-grid--sum sizes) (* gap (max 0 (1- count))))))
|
|
(when (and available (> fr-total 0))
|
|
(let ((remaining (max 0 (- available used))))
|
|
(cl-loop for track in tracks
|
|
for index from 0
|
|
when (eq (plist-get track :kind) 'fr)
|
|
do (setf (nth index sizes)
|
|
(floor (* remaining
|
|
(/ (float (plist-get track :factor))
|
|
fr-total)))))))
|
|
sizes))
|
|
|
|
(defun ebox-grid--distribute (amount count)
|
|
"Return COUNT integer shares that sum to AMOUNT."
|
|
(let ((base (if (> count 0) (/ amount count) 0))
|
|
(remainder (if (> count 0) (% amount count) 0)))
|
|
(cl-loop for index from 0 below count
|
|
collect (+ base (if (< index remainder) 1 0)))))
|
|
|
|
(defun ebox-grid--content-layout (sizes gap available alignment)
|
|
"Return track SIZES and content spacing for AVAILABLE space."
|
|
(let* ((natural (+ (ebox-grid--sum sizes)
|
|
(* gap (max 0 (1- (length sizes))))))
|
|
(target (max natural (or available natural)))
|
|
(leftover (- target natural)))
|
|
(if (and (> leftover 0) (memq alignment '(stretch normal)))
|
|
(let ((extra (ebox-grid--distribute leftover (length sizes))))
|
|
(list :sizes (cl-mapcar #'+ sizes extra)
|
|
:leading 0 :between gap :trailing 0
|
|
:target target))
|
|
(let ((spacing (ebox--flex-spacing
|
|
(or alignment 'start) leftover (length sizes) gap)))
|
|
(list :sizes sizes
|
|
:leading (nth 0 spacing)
|
|
:between (nth 1 spacing)
|
|
:trailing (nth 2 spacing)
|
|
:target target)))))
|
|
|
|
(defun ebox-grid--entry-size (entry sizes gap)
|
|
"Return the target pixel/line size for ENTRY across SIZES."
|
|
(let* ((start (1- (plist-get entry :column)))
|
|
(end (min (length sizes)
|
|
(+ start (plist-get entry :column-span))))
|
|
(base (ebox-grid--sum (seq-subseq sizes start end)))
|
|
(extra (* gap (max 0 (1- (plist-get entry :column-span))))))
|
|
(+ base extra)))
|
|
|
|
(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)))
|
|
(end (min (length sizes)
|
|
(+ start (plist-get entry :row-span))))
|
|
(base (ebox-grid--sum (seq-subseq sizes start end)))
|
|
(extra (* gap (max 0 (1- (plist-get entry :row-span))))))
|
|
(+ base extra)))
|
|
|
|
(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)))))
|
|
(ebox--lines-align-vertical
|
|
result height
|
|
(pcase align
|
|
((or 'end 'bottom) 'bottom)
|
|
('center 'center)
|
|
(_ 'top)))))
|
|
|
|
(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))
|
|
(height (ebox-grid--row-entry-size entry heights row-gap))
|
|
(justify (or (plist-get props :justify-items) 'stretch))
|
|
(align (or (plist-get props :align-items) 'stretch)))
|
|
(ebox-grid--align source width height justify align)))
|
|
|
|
(defun ebox-grid--entry-at (matrix row column)
|
|
"Return entry occupying ROW and COLUMN in MATRIX."
|
|
(and (< row (length matrix))
|
|
(< column (length (aref matrix row)))
|
|
(aref (aref matrix row) column)))
|
|
|
|
(defun ebox-grid--entry-start-p (entry row column)
|
|
"Return non-nil when ENTRY starts at zero-based ROW and COLUMN."
|
|
(and (= row (1- (plist-get entry :row)))
|
|
(= column (1- (plist-get entry :column)))))
|
|
|
|
(defun ebox-grid--blank (width height)
|
|
"Return a blank grid area of WIDTH pixels and HEIGHT lines."
|
|
(or (ebox--pixel-blank width height)
|
|
(ebox-lines-join (make-list (max 1 height) ""))))
|
|
|
|
(defun ebox-grid--slice (string offset height)
|
|
"Return HEIGHT lines from STRING beginning at OFFSET."
|
|
(ebox-lines-join
|
|
(append (seq-take (nthcdr offset (ebox-string-lines string)) height)
|
|
(make-list (max 0 (- height
|
|
(length (seq-take
|
|
(nthcdr offset (ebox-string-lines string))
|
|
height))))
|
|
""))))
|
|
|
|
(defun ebox-grid--render (entries matrix column-layout row-layout props rendered)
|
|
"Render placed grid ENTRIES from MATRIX and resolved track layouts."
|
|
(let* ((widths (plist-get column-layout :sizes))
|
|
(heights (plist-get row-layout :sizes))
|
|
(col-gap (plist-get column-layout :between))
|
|
(row-gap (plist-get row-layout :between))
|
|
(total-width (plist-get column-layout :target))
|
|
(strings (make-hash-table :test 'eq))
|
|
result)
|
|
(dolist (entry entries)
|
|
(puthash entry
|
|
(ebox-grid--entry-string entry rendered widths heights
|
|
col-gap row-gap props)
|
|
strings))
|
|
(dotimes (row (length heights))
|
|
(let ((parts nil)
|
|
(column 0))
|
|
(while (< column (length widths))
|
|
(let ((entry (ebox-grid--entry-at matrix row column)))
|
|
(if (and entry (ebox-grid--entry-start-p entry row column))
|
|
(let* ((string (gethash entry strings))
|
|
(start (1- (plist-get entry :row)))
|
|
(offset (+ (ebox-grid--sum
|
|
(seq-subseq heights start row))
|
|
(* row-gap (- row start))))
|
|
(height (nth row heights)))
|
|
(push (ebox-grid--slice string offset height) parts)
|
|
(setq column (+ column (plist-get entry :column-span))))
|
|
(let ((width (nth column widths)))
|
|
(push (ebox-grid--blank width (nth row heights)) parts)
|
|
(setq column (1+ column))))
|
|
(when (< column (length widths))
|
|
(push (ebox-pixel-space col-gap) parts))))
|
|
(setq result
|
|
(append result
|
|
(list (apply #'ebox--lines-concat-horizontal
|
|
(append
|
|
(when (> (plist-get column-layout :leading) 0)
|
|
(list (ebox-pixel-space
|
|
(plist-get column-layout :leading))))
|
|
(nreverse parts)
|
|
(when (> (plist-get column-layout :trailing) 0)
|
|
(list (ebox-pixel-space
|
|
(plist-get column-layout :trailing)))))))))
|
|
(when (< row (1- (length heights)))
|
|
(setq result
|
|
(append result
|
|
(list (ebox-grid--blank total-width row-gap)))))
|
|
))
|
|
(ebox-lines-join
|
|
(append
|
|
(when (> (plist-get row-layout :leading) 0)
|
|
(list (ebox-grid--blank total-width
|
|
(plist-get row-layout :leading))))
|
|
result
|
|
(when (> (plist-get row-layout :trailing) 0)
|
|
(list (ebox-grid--blank total-width
|
|
(plist-get row-layout :trailing))))))))
|
|
|
|
(defun ebox--render-grid-box (node rendered)
|
|
"Render grid NODE's optional visual wrapper around RENDERED."
|
|
(if-let ((box (plist-get node :box)))
|
|
(let ((copy (copy-sequence box))
|
|
(region-id (ebox--ensure-region-id box)))
|
|
(plist-put copy :region-id region-id)
|
|
(plist-put copy :content rendered)
|
|
(plist-put copy :ebox-content-node nil)
|
|
(plist-put copy :ebox-content-width-exact-p t)
|
|
(prog1 (ebox-render copy)
|
|
(puthash region-id box ebox--region-box-table)))
|
|
rendered))
|
|
|
|
(defun ebox--render-grid (node)
|
|
"Render GRID NODE to a propertized string."
|
|
(let* ((props (plist-get node :raw-props))
|
|
(children (plist-get node :children))
|
|
(columns (ebox-grid--normalize-tracks
|
|
(plist-get props :grid-template-columns) 'columns))
|
|
(rows (ebox-grid--normalize-tracks
|
|
(plist-get props :grid-template-rows) 'rows))
|
|
(auto-columns (car (ebox-grid--normalize-tracks
|
|
(plist-get props :grid-auto-columns) 'columns)))
|
|
(auto-rows (car (ebox-grid--normalize-tracks
|
|
(plist-get props :grid-auto-rows) 'rows)))
|
|
(gaps (ebox-grid--gap-pair props))
|
|
(flow (or (plist-get props :grid-auto-flow) 'row))
|
|
(placed (ebox-grid--place-children children
|
|
(max 1 (length columns))
|
|
(max 1 (length rows)) flow))
|
|
(entries (nth 0 placed))
|
|
(matrix (nth 1 placed))
|
|
(row-count (nth 2 placed))
|
|
(column-count (nth 3 placed))
|
|
(available (ebox--nonnegative-horizontal-size-pixels
|
|
(plist-get props :width)
|
|
(ebox--viewport-pixel-width nil)))
|
|
(height (ebox--flex-line-value (plist-get props :height) nil))
|
|
(rendered (make-hash-table :test 'eq)))
|
|
(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* ((widths (ebox-grid--resolve-sizes columns column-count 'columns
|
|
entries rendered (cdr gaps)
|
|
available auto-columns))
|
|
(heights (ebox-grid--resolve-sizes rows row-count 'rows
|
|
entries rendered (car gaps)
|
|
height auto-rows))
|
|
(column-layout
|
|
(ebox-grid--content-layout
|
|
widths (cdr gaps) available
|
|
(or (plist-get props :justify-content) 'start)))
|
|
(row-layout
|
|
(ebox-grid--content-layout
|
|
heights (car gaps) height
|
|
(or (plist-get props :align-content) 'start)))
|
|
(body (ebox-grid--render entries matrix column-layout row-layout
|
|
props rendered)))
|
|
(ebox--render-grid-box node body))))
|
|
|
|
;;;###autoload
|
|
(defun ebox-grid (&rest items)
|
|
"Return a two-dimensional grid layout node from ITEMS.
|
|
|
|
Grid properties include `:grid-template-columns', `:grid-template-rows',
|
|
`:grid-auto-flow', `:gap', `:grid-row-gap', `:grid-column-gap',
|
|
`:justify-items', `:align-items', `:width', and `:height'. Track sizes use
|
|
Ebox pixel/line units, `auto', `(fr FACTOR)', or symbols such as `1fr'.
|
|
Children may carry `:grid-column' and `:grid-row' placement properties.
|
|
`ebox-grid-fr' is a convenient constructor for fractional tracks."
|
|
(let* ((split (ebox-grid--split-attrs items))
|
|
(raw-props (car split))
|
|
(children (delq nil (cdr split)))
|
|
(box-props (cl-loop for (key value) on raw-props by #'cddr
|
|
unless (memq key ebox--grid-layout-prop-keys)
|
|
collect key and collect value))
|
|
(wrapper (ebox-grid--wrapper box-props)))
|
|
(list :ebox-type 'grid
|
|
:display '(block grid)
|
|
:props raw-props
|
|
:raw-props raw-props
|
|
:box wrapper
|
|
:children children)))
|
|
|
|
(provide 'ebox-grid)
|
|
|
|
;;; ebox-grid.el ends here
|