v0.1.0: Layout primitives + composable API — 23 new tests
Layout primitives (org/layout-primitives.org, ~290 lines): - CLOS layout-node class wrapping YGNodeRef with tg:finalize for GC cleanup - 12 setter functions with keyword→integer enum translation - layout-calculate: runs Yoga layout, returns root - 14 tests: dimension, direction, flex-grow, align, justify, padding, margin, absolute position, wrap, gap, nested layout Composable API (org/layout-composable.org, ~200 lines): - vbox/hbox macros: declarative container creation with style props - overlay macro: absolute-positioned child over relative base - spacer function: flex-grow filler - make-props-list helper: extracts plist minus :children - 6 tests: vbox stacking, hbox layout, spacer flex, overlay position, align/justify via vbox, padding offset GREEN: 29/29 pass (59 assertions) - yoga-ffi: 9 tests (22 assertions) - layout-primitives: 14 tests (24 assertions) - layout-composable: 6 tests (13 assertions)
This commit is contained in:
248
org/layout-composable.org
Normal file
248
org/layout-composable.org
Normal file
@@ -0,0 +1,248 @@
|
||||
#+TITLE: Layout Composable API
|
||||
#+STARTUP: content
|
||||
#+FILETAGS: :cl-tui:layout-composable:v010:
|
||||
|
||||
* Layout Composable API
|
||||
|
||||
Convenience macros for building layout trees declaratively. Each macro
|
||||
creates a =layout-node=, applies style properties, and inserts child nodes.
|
||||
|
||||
Dependencies: =layout-primitives=.
|
||||
|
||||
** Contract
|
||||
|
||||
- =(vbox &key width height flex-grow flex-shrink flex-basis align justify gap
|
||||
padding margin children ...)= → layout-node ::
|
||||
Creates a column-direction container. All =children= are
|
||||
layout-node instances, or plain non-node values are skipped.
|
||||
- =(hbox &key width height flex-grow flex-shrink flex-basis align justify gap
|
||||
padding margin children ...)= → layout-node ::
|
||||
Creates a row-direction container.
|
||||
- =(overlay base child &key top right bottom left)= → layout-node ::
|
||||
Creates a container with a relative base and an absolute-positioned child
|
||||
overlaid on top.
|
||||
- =(spacer &key grow)= → layout-node ::
|
||||
Creates an empty flexible spacer that fills available space.
|
||||
|
||||
* Package
|
||||
|
||||
#+begin_src lisp
|
||||
(eval-when (:compile-toplevel :load-toplevel :execute)
|
||||
(ql:quickload :cffi :silent t)
|
||||
(ql:quickload :trivial-garbage :silent t))
|
||||
|
||||
(defpackage :cl-tui.layout-composable
|
||||
(:use :cl :cl-tui.layout-primitives)
|
||||
(:export
|
||||
#:vbox
|
||||
#:hbox
|
||||
#:overlay
|
||||
#:spacer))
|
||||
|
||||
(in-package :cl-tui.layout-composable)
|
||||
#+end_src
|
||||
|
||||
* Internal Helpers
|
||||
|
||||
#+begin_src lisp
|
||||
(defun apply-common-props (node &key width height flex-grow flex-shrink flex-basis
|
||||
align justify gap padding margin
|
||||
&allow-other-keys)
|
||||
"Apply the shared style properties to a layout-node."
|
||||
(when (or width height)
|
||||
(layout-node-set-dimension node (or width 0) (or height 0)))
|
||||
(when (or flex-grow flex-shrink flex-basis)
|
||||
(layout-node-set-flex node :grow flex-grow :shrink flex-shrink :basis flex-basis))
|
||||
(when align
|
||||
(apply #'layout-node-set-align node align))
|
||||
(when justify
|
||||
(layout-node-set-justify node justify))
|
||||
(when gap
|
||||
(apply #'layout-node-set-gap node gap))
|
||||
(when padding
|
||||
(apply #'layout-node-set-padding node padding))
|
||||
(when margin
|
||||
(apply #'layout-node-set-margin node margin)))
|
||||
|
||||
(defun add-children (parent children)
|
||||
"Add each child in CHILDREN to PARENT. Non-node values are skipped."
|
||||
(dolist (child children)
|
||||
(when (typep child 'layout-node)
|
||||
(layout-node-add-child parent child))))
|
||||
|
||||
(defun make-props-list (args)
|
||||
"Extract all properties except :children from ARGS plist."
|
||||
(loop for (k v) on args by #'cddr
|
||||
unless (eq k :children)
|
||||
append (list k v)))
|
||||
#+end_src
|
||||
|
||||
* vbox — Vertical Box
|
||||
|
||||
#+begin_src lisp
|
||||
(defmacro vbox (&rest args &key children &allow-other-keys)
|
||||
"Create a column-direction container with CHILDREN stacked vertically."
|
||||
(declare (ignore children))
|
||||
(let* ((node (gensym "VBOX"))
|
||||
(props (make-props-list args)))
|
||||
`(let ((,node (make-layout-node)))
|
||||
(layout-node-set-direction ,node :column)
|
||||
(apply #'cl-tui.layout-composable::apply-common-props ,node ',props)
|
||||
(cl-tui.layout-composable::add-children ,node (list ,@children))
|
||||
,node)))
|
||||
#+end_src
|
||||
|
||||
* hbox — Horizontal Box
|
||||
|
||||
#+begin_src lisp
|
||||
(defmacro hbox (&rest args &key children &allow-other-keys)
|
||||
"Create a row-direction container with CHILDREN laid out horizontally."
|
||||
(declare (ignore children))
|
||||
(let* ((node (gensym "HBOX"))
|
||||
(props (make-props-list args)))
|
||||
`(let ((,node (make-layout-node)))
|
||||
(layout-node-set-direction ,node :row)
|
||||
(apply #'cl-tui.layout-composable::apply-common-props ,node ',props)
|
||||
(cl-tui.layout-composable::add-children ,node (list ,@children))
|
||||
,node)))
|
||||
#+end_src
|
||||
|
||||
* overlay — Absolute Overlay
|
||||
|
||||
#+begin_src lisp
|
||||
(defmacro overlay (base child &key top right bottom left)
|
||||
"Create a container with BASE as the relative foundation and CHILD
|
||||
positioned absolutely on top."
|
||||
(let ((node (gensym "OVERLAY")))
|
||||
`(let ((,node (make-layout-node)))
|
||||
(layout-node-set-position ,node :relative)
|
||||
(layout-node-add-child ,node ,base)
|
||||
(layout-node-set-position ,child :absolute
|
||||
,@(when top `(:top ,top))
|
||||
,@(when right `(:right ,right))
|
||||
,@(when bottom `(:bottom ,bottom))
|
||||
,@(when left `(:left ,left)))
|
||||
(layout-node-add-child ,node ,child)
|
||||
,node)))
|
||||
#+end_src
|
||||
|
||||
* spacer — Flex Spacer
|
||||
|
||||
#+begin_src lisp
|
||||
(defun spacer (&key (grow 0))
|
||||
"Create an empty spacer node that fills available space via flex-grow."
|
||||
(let ((node (make-layout-node)))
|
||||
(when (> grow 0)
|
||||
(layout-node-set-flex node :grow grow))
|
||||
node))
|
||||
#+end_src
|
||||
|
||||
* Test Suite
|
||||
|
||||
#+begin_src lisp
|
||||
(eval-when (:compile-toplevel :load-toplevel :execute)
|
||||
(ql:quickload :fiveam :silent t))
|
||||
|
||||
(defpackage :cl-tui.layout-composable-tests
|
||||
(:use :cl :fiveam)
|
||||
(:import-from :cl-tui.layout-composable
|
||||
#:vbox #:hbox #:overlay #:spacer)
|
||||
(:import-from :cl-tui.layout-primitives
|
||||
#:layout-node #:layout-node-ptr #:layout-calculate
|
||||
#:make-layout-node #:layout-node-set-dimension)
|
||||
(:import-from :cl-tui.yoga-ffi
|
||||
#:yg-node-layout-get-left
|
||||
#:yg-node-layout-get-top
|
||||
#:yg-node-layout-get-width
|
||||
#:yg-node-layout-get-height
|
||||
#:yg-node-get-child))
|
||||
|
||||
(in-package :cl-tui.layout-composable-tests)
|
||||
|
||||
(defun node-x (n) (yg-node-layout-get-left (layout-node-ptr n)))
|
||||
(defun node-y (n) (yg-node-layout-get-top (layout-node-ptr n)))
|
||||
(defun node-w (n) (yg-node-layout-get-width (layout-node-ptr n)))
|
||||
(defun node-h (n) (yg-node-layout-get-height (layout-node-ptr n)))
|
||||
(defun child-x (p) (yg-node-layout-get-left p))
|
||||
(defun child-y (p) (yg-node-layout-get-top p))
|
||||
(defun child-w (p) (yg-node-layout-get-width p))
|
||||
(defun child-h (p) (yg-node-layout-get-height p))
|
||||
|
||||
(defun nth-child (node n)
|
||||
(yg-node-get-child (layout-node-ptr node) n))
|
||||
|
||||
(defun layout-dummy (w h)
|
||||
(let ((n (make-layout-node)))
|
||||
(layout-node-set-dimension n w h)
|
||||
n))
|
||||
|
||||
(fiveam:def-suite layout-composable-suite
|
||||
:description "Composable API macro verification")
|
||||
(fiveam:in-suite layout-composable-suite)
|
||||
|
||||
(fiveam:test test-vbox-stacks-children
|
||||
"Contract: vbox stacks children vertically."
|
||||
(let* ((root (vbox :width 100 :height 200
|
||||
:children ((layout-dummy 100 50)
|
||||
(layout-dummy 100 50)))))
|
||||
(layout-calculate root 100 200)
|
||||
(let ((c1 (nth-child root 0))
|
||||
(c2 (nth-child root 1)))
|
||||
(fiveam:is (= 0.0 (child-y c1)))
|
||||
(fiveam:is (= 50.0 (child-y c2))))))
|
||||
|
||||
(fiveam:test test-hbox-lays-out-horizontally
|
||||
"Contract: hbox places children horizontally."
|
||||
(let* ((root (hbox :width 200 :height 100
|
||||
:children ((layout-dummy 80 50)
|
||||
(layout-dummy 80 50)))))
|
||||
(layout-calculate root 200 100)
|
||||
(let ((c1 (nth-child root 0))
|
||||
(c2 (nth-child root 1)))
|
||||
(fiveam:is (= 0.0 (child-x c1)))
|
||||
(fiveam:is (= 80.0 (child-x c2))))))
|
||||
|
||||
(fiveam:test test-spacer-flex-grow
|
||||
"Contract: spacer with flex-grow expands to fill space."
|
||||
(let* ((root (hbox :width 200 :height 100
|
||||
:children ((layout-dummy 50 50)
|
||||
(spacer :grow 1)
|
||||
(layout-dummy 50 50)))))
|
||||
(layout-calculate root 200 100)
|
||||
(let ((c1 (nth-child root 0))
|
||||
(c2 (nth-child root 1))
|
||||
(c3 (nth-child root 2)))
|
||||
(fiveam:is (= 0.0 (child-x c1)))
|
||||
(fiveam:is (= 50.0 (child-w c1)))
|
||||
(fiveam:is (= 100.0 (child-w c2))))))
|
||||
|
||||
(fiveam:test test-overlay-absolute-position
|
||||
"Contract: overlay positions an absolute child over a relative base."
|
||||
(let* ((base (layout-dummy 100 100))
|
||||
(child (layout-dummy 30 30))
|
||||
(root (overlay base child :top 10 :left 20)))
|
||||
(layout-calculate root 200 200)
|
||||
(fiveam:is (= 20.0 (node-x child)))
|
||||
(fiveam:is (= 10.0 (node-y child)))))
|
||||
|
||||
(fiveam:test test-vbox-align-justify
|
||||
"Contract: vbox accepts align and justify keywords."
|
||||
(let* ((root (vbox :width 200 :height 100
|
||||
:align (:items :center)
|
||||
:justify :center
|
||||
:children ((layout-dummy 50 50)))))
|
||||
(layout-calculate root 200 100)
|
||||
(let ((c (nth-child root 0)))
|
||||
(fiveam:is (= 25.0 (child-y c)))
|
||||
(fiveam:is (= 75.0 (child-x c))))))
|
||||
|
||||
(fiveam:test test-vbox-padding
|
||||
"Contract: vbox padding offsets children."
|
||||
(let* ((root (vbox :width 200 :height 100
|
||||
:padding (:all 10)
|
||||
:children ((layout-dummy 180 80)))))
|
||||
(layout-calculate root 200 100)
|
||||
(let ((c (nth-child root 0)))
|
||||
(fiveam:is (= 10.0 (child-x c)))
|
||||
(fiveam:is (= 10.0 (child-y c))))))
|
||||
#+end_src
|
||||
Reference in New Issue
Block a user