v0.0.3: layout engine — pure CL Flexbox constraint solver and push
This commit is contained in:
169
layout/layout.lisp
Normal file
169
layout/layout.lisp
Normal file
@@ -0,0 +1,169 @@
|
||||
;;; layout — Pure CL Flexbox layout engine
|
||||
|
||||
(defpackage :cl-tui.layout
|
||||
(:use :cl)
|
||||
(:export
|
||||
#:layout-node #:make-layout-node
|
||||
#:layout-node-add-child #:layout-node-remove-child
|
||||
#:layout-node-children
|
||||
#:layout-node-x #:layout-node-y
|
||||
#:layout-node-width #:layout-node-height
|
||||
#:layout-node-direction
|
||||
#:compute-layout
|
||||
#:vbox #:hbox #:spacer
|
||||
;; For tests
|
||||
#:layout-node-parent #:layout-node-fixed-width
|
||||
#:layout-node-fixed-height #:normalize-box
|
||||
#:box-edge))
|
||||
|
||||
(in-package :cl-tui.layout)
|
||||
|
||||
(defclass layout-node ()
|
||||
((parent :initform nil :accessor layout-node-parent)
|
||||
(children :initform nil :accessor layout-node-children)
|
||||
(x :initform 0 :accessor layout-node-x)
|
||||
(y :initform 0 :accessor layout-node-y)
|
||||
(width :initform 0 :accessor layout-node-width)
|
||||
(height :initform 0 :accessor layout-node-height)
|
||||
(direction :initform :column :initarg :direction :accessor layout-node-direction)
|
||||
(grow :initform 0 :initarg :grow :accessor layout-node-grow)
|
||||
(shrink :initform 1 :initarg :shrink :accessor layout-node-shrink)
|
||||
(padding :initform '(:top 0 :right 0 :bottom 0 :left 0) :initarg :padding :accessor layout-node-padding)
|
||||
(margin :initform '(:top 0 :right 0 :bottom 0 :left 0) :initarg :margin :accessor layout-node-margin)
|
||||
(gap :initform 0 :initarg :gap :accessor layout-node-gap)
|
||||
(position-type :initform :relative :initarg :position-type :accessor layout-node-position-type)
|
||||
(position-offset :initform nil :initarg :position-offset :accessor layout-node-position-offset)
|
||||
(fixed-width :initform nil :initarg :width :accessor layout-node-fixed-width)
|
||||
(fixed-height :initform nil :initarg :height :accessor layout-node-fixed-height)))
|
||||
|
||||
(defun make-layout-node (&key direction grow shrink padding margin gap
|
||||
position-type position-offset width height)
|
||||
(make-instance 'layout-node
|
||||
:direction (or direction :column)
|
||||
:grow (or grow 0) :shrink (or shrink 1)
|
||||
:padding (normalize-box padding) :margin (normalize-box margin)
|
||||
:gap gap
|
||||
:position-type (or position-type :relative)
|
||||
:position-offset position-offset
|
||||
:width width :height height))
|
||||
|
||||
(defun normalize-box (spec)
|
||||
(cond ((null spec) '(:top 0 :right 0 :bottom 0 :left 0))
|
||||
((numberp spec) `(:top ,spec :right ,spec :bottom ,spec :left ,spec))
|
||||
((getf spec :top) spec)
|
||||
(t '(:top 0 :right 0 :bottom 0 :left 0))))
|
||||
|
||||
(defun box-edge (box edge)
|
||||
(or (getf box edge) 0))
|
||||
|
||||
(defun layout-node-add-child (parent child)
|
||||
(setf (layout-node-parent child) parent)
|
||||
(push child (layout-node-children parent))
|
||||
child)
|
||||
|
||||
(defun layout-node-remove-child (parent child)
|
||||
(setf (layout-node-parent child) nil)
|
||||
(setf (layout-node-children parent)
|
||||
(delete child (layout-node-children parent)))
|
||||
child)
|
||||
|
||||
;; ── Solver ─────────────────────────────────────────────────────
|
||||
|
||||
(defun distribute-sizes (children avail gap)
|
||||
"Compute child sizes given available space and gap."
|
||||
(let* ((n (length children))
|
||||
(default-size (if (zerop n) 0 (round avail n)))
|
||||
(gap-total (* gap (max 0 (1- n))))
|
||||
(sizes (mapcar (lambda (c)
|
||||
(or (if (eql (layout-node-direction c) :row)
|
||||
(layout-node-fixed-width c)
|
||||
(layout-node-fixed-height c))
|
||||
default-size))
|
||||
children))
|
||||
(total (reduce #'+ sizes))
|
||||
(remaining (- total (- avail gap-total)))
|
||||
(grow-total (reduce #'+ (mapcar #'layout-node-grow children)))
|
||||
(shrink-total (reduce #'+ (mapcar #'layout-node-shrink children))))
|
||||
(mapcar (lambda (c sz)
|
||||
(let ((g (layout-node-grow c))
|
||||
(s (layout-node-shrink c))
|
||||
(size sz))
|
||||
(when (and (plusp remaining) (plusp grow-total))
|
||||
(incf size (round (* remaining (/ g grow-total)))))
|
||||
(when (and (minusp remaining) (plusp shrink-total))
|
||||
(decf size (round (* (abs remaining) (/ s shrink-total)))))
|
||||
(max 1 size)))
|
||||
children sizes)))
|
||||
|
||||
(defun compute-layout (root available-width available-height)
|
||||
(labels ((place-children (node x y max-w max-h)
|
||||
(let* ((children (reverse (layout-node-children node)))
|
||||
(is-row (eql (layout-node-direction node) :row))
|
||||
(pl (box-edge (layout-node-padding node) :left))
|
||||
(pt (box-edge (layout-node-padding node) :top))
|
||||
(pr (box-edge (layout-node-padding node) :right))
|
||||
(pb (box-edge (layout-node-padding node) :bottom))
|
||||
(cw (max 0 (- max-w pl pr)))
|
||||
(ch (max 0 (- max-h pt pb)))
|
||||
(gap (layout-node-gap node))
|
||||
(sizes (distribute-sizes children (if is-row cw ch) gap)))
|
||||
(setf (layout-node-x node) (+ x pl)
|
||||
(layout-node-y node) (+ y pt))
|
||||
(loop with pos = 0
|
||||
for child in children
|
||||
for size in sizes
|
||||
do (if is-row
|
||||
(setf (layout-node-width child) size
|
||||
(layout-node-x child) (+ x pl pos)
|
||||
(layout-node-height child) ch
|
||||
(layout-node-y child) (+ y pt))
|
||||
(setf (layout-node-height child) size
|
||||
(layout-node-y child) (+ y pt pos)
|
||||
(layout-node-width child) cw
|
||||
(layout-node-x child) (+ x pl)))
|
||||
(place-children child (layout-node-x child) (layout-node-y child)
|
||||
(if is-row size cw) (if is-row ch size))
|
||||
(incf pos (+ size gap))))
|
||||
(let ((last (car (last children))))
|
||||
(if is-row
|
||||
(setf (layout-node-width node)
|
||||
(or (layout-node-fixed-width node)
|
||||
(if last (+ (layout-node-x node) (layout-node-width last) (box-edge (layout-node-padding node) :right)) max-w))
|
||||
(layout-node-height node) max-h)
|
||||
(setf (layout-node-height node)
|
||||
(or (layout-node-fixed-height node)
|
||||
(if last (+ (layout-node-y node) (layout-node-height last) (box-edge (layout-node-padding node) :bottom)) max-h))
|
||||
(layout-node-width node) max-w)))))
|
||||
(place-children root 0 0 available-width available-height)
|
||||
root))
|
||||
|
||||
;; ── Macros ─────────────────────────────────────────────────────
|
||||
|
||||
(defmacro vbox ((&key grow shrink padding margin gap width height) &body children)
|
||||
(let ((n (gensym)))
|
||||
`(let ((,n (make-layout-node :direction :column
|
||||
,@(when grow `(:grow ,grow))
|
||||
,@(when shrink `(:shrink ,shrink))
|
||||
,@(when padding `(:padding ,padding))
|
||||
,@(when margin `(:margin ,margin))
|
||||
,@(when gap `(:gap ,gap))
|
||||
,@(when width `(:width ,width))
|
||||
,@(when height `(:height ,height)))))
|
||||
,@(loop for c in children collect `(layout-node-add-child ,n ,c))
|
||||
,n)))
|
||||
|
||||
(defmacro hbox ((&key grow shrink padding margin gap width height) &body children)
|
||||
(let ((n (gensym)))
|
||||
`(let ((,n (make-layout-node :direction :row
|
||||
,@(when grow `(:grow ,grow))
|
||||
,@(when shrink `(:shrink ,shrink))
|
||||
,@(when padding `(:padding ,padding))
|
||||
,@(when margin `(:margin ,margin))
|
||||
,@(when gap `(:gap ,gap))
|
||||
,@(when width `(:width ,width))
|
||||
,@(when height `(:height ,height)))))
|
||||
,@(loop for c in children collect `(layout-node-add-child ,n ,c))
|
||||
,n)))
|
||||
|
||||
(defmacro spacer (&key grow)
|
||||
`(make-layout-node :grow ,(or grow 1)))
|
||||
Reference in New Issue
Block a user