1 Commits

Author SHA1 Message Date
Hermes
bd22f1a43d test: check token permissions 2026-05-11 11:45:59 +00:00
9 changed files with 4 additions and 2970 deletions

View File

@@ -50,3 +50,4 @@ See ~docs/ROADMAP.org~ for the full release plan.
** License
TBD
# Test

View File

@@ -1,11 +0,0 @@
(defsystem :cl-tui
:name "cl-tui"
:author "memex"
:version "0.1.0"
:license "AGPLv3"
:description "Reusable Common Lisp Terminal UI Framework"
:depends-on (:cffi :croatoan :trivial-garbage)
:serial t
:components ((:file "lisp/yoga-ffi")
(:file "lisp/layout-primitives")
(:file "lisp/layout-composable")))

View File

@@ -23,28 +23,22 @@ When a version ships:
Yoga Flexbox backend wrapped in a Common Lisp API. This is the foundation —
every component after v0.1.0 uses the layout engine for positioning.
*** DONE Yoga FFI binding
*** TODO Yoga FFI binding
:PROPERTIES:
:ID: id-v010-yoga-ffi
:CREATED: [2026-05-10 Sat]
:END:
:LOGBOOK:
- State "DONE" from "TODO" [2026-05-11 Mon]
:END:
- Load the Yoga shared library via CFFI
- Define foreign types for ~YGNodeRef~, ~YGSize~, ~YGValue~, ~YGDirection~, ~YGFlexDirection~, ~YGAlign~, ~YGJustify~, ~YGWrap~, ~YGPositionType~, ~YGOverflow~, ~YGDisplay~, ~YGEdge~
- Bind core functions: ~node-new~, ~node-free~, ~node-style-set-*~, ~node-layout-get-*~, ~calculate-layout~
- ~100 lines CFFI
*** DONE Layout primitives
*** TODO Layout primitives
:PROPERTIES:
:ID: id-v010-layout-primitives
:CREATED: [2026-05-10 Sat]
:END:
:LOGBOOK:
- State "DONE" from "TODO" [2026-05-11 Mon]
:END:
- ~(make-layout-node)~ — wraps a ~YGNodeRef~ in a CLOS object
- ~(layout-node-set-dimension node width height)~ — sets width/height in points
@@ -62,14 +56,11 @@ every component after v0.1.0 uses the layout engine for positioning.
- ~(layout-calculate root width height)~ — runs Yoga's calculateLayout, populates each node's computed x/y/w/h
- ~200 lines CL
*** DONE Layout composable API
*** TODO Layout composable API
:PROPERTIES:
:ID: id-v010-layout-composable
:CREATED: [2026-05-10 Sat]
:END:
:LOGBOOK:
- State "DONE" from "TODO" [2026-05-11 Mon]
:END:
Convenience macros to build layout trees from CL function calls:

View File

@@ -1,194 +0,0 @@
(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)
(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)))
(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)))
(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)))
(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)))
(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))
(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))))))

View File

@@ -1,511 +0,0 @@
(eval-when (:compile-toplevel :load-toplevel :execute)
(ql:quickload :cffi :silent t)
(ql:quickload :trivial-garbage :silent t))
(defpackage :cl-tui.layout-primitives
(:use :cl)
(:import-from :cl-tui.yoga-ffi
#:load-yoga
#:yg-node-new
#:yg-node-free
#:yg-node-insert-child
#:yg-node-remove-child
#:yg-node-get-child-count
#:yg-node-calculate-layout
#:yg-node-layout-get-left
#:yg-node-layout-get-top
#:yg-node-layout-get-width
#:yg-node-layout-get-height
#:yg-node-layout-get-right
#:yg-node-layout-get-bottom
#:yg-node-style-set-direction
#:yg-node-style-set-flex-direction
#:yg-node-style-set-justify-content
#:yg-node-style-set-align-items
#:yg-node-style-set-align-self
#:yg-node-style-set-align-content
#:yg-node-style-set-flex-wrap
#:yg-node-style-set-position-type
#:yg-node-style-set-flex-grow
#:yg-node-style-set-flex-shrink
#:yg-node-style-set-flex-basis
#:yg-node-style-set-flex-basis-auto
#:yg-node-style-set-overflow
#:yg-node-style-set-display
#:yg-node-style-set-width
#:yg-node-style-set-width-auto
#:yg-node-style-set-height
#:yg-node-style-set-height-auto
#:yg-node-style-set-min-width
#:yg-node-style-set-min-height
#:yg-node-style-set-max-width
#:yg-node-style-set-max-height
#:yg-node-style-set-aspect-ratio
#:yg-node-style-set-padding
#:yg-node-style-set-margin
#:yg-node-style-set-margin-auto
#:yg-node-style-set-border
#:yg-node-style-set-gap
#:yg-node-style-set-position
;; enum constants
#:+yg-flex-direction-column+
#:+yg-flex-direction-column-reverse+
#:+yg-flex-direction-row+
#:+yg-flex-direction-row-reverse+
#:+yg-wrap-nowrap+
#:+yg-wrap-wrap+
#:+yg-wrap-wrap-reverse+
#:+yg-justify-auto+
#:+yg-justify-flex-start+
#:+yg-justify-center+
#:+yg-justify-flex-end+
#:+yg-justify-space-between+
#:+yg-justify-space-around+
#:+yg-justify-space-evenly+
#:+yg-align-auto+
#:+yg-align-flex-start+
#:+yg-align-center+
#:+yg-align-flex-end+
#:+yg-align-stretch+
#:+yg-align-baseline+
#:+yg-align-space-between+
#:+yg-align-space-around+
#:+yg-align-space-evenly+
#:+yg-position-type-static+
#:+yg-position-type-relative+
#:+yg-position-type-absolute+
#:+yg-overflow-visible+
#:+yg-overflow-hidden+
#:+yg-overflow-scroll+
#:+yg-display-flex+
#:+yg-display-none+
#:+yg-edge-left+
#:+yg-edge-top+
#:+yg-edge-right+
#:+yg-edge-bottom+
#:+yg-edge-all+
#:+yg-edge-start+
#:+yg-edge-end+
#:+yg-edge-horizontal+
#:+yg-edge-vertical+
#:+yg-gutter-column+
#:+yg-gutter-row+
#:+yg-gutter-all+
#:+yg-direction-inherit+
#:+yg-direction-ltr+
#:+yg-direction-rtl+)
(:export
#:layout-node
#:layout-node-ptr
#:make-layout-node
#:layout-node-add-child
#:layout-node-set-dimension
#:layout-node-set-flex
#:layout-node-set-direction
#:layout-node-set-wrap
#:layout-node-set-align
#:layout-node-set-justify
#:layout-node-set-padding
#:layout-node-set-margin
#:layout-node-set-gap
#:layout-node-set-position
#:layout-node-set-border
#:layout-node-set-overflow
#:layout-node-set-display
#:layout-node-set-aspect-ratio
#:layout-calculate))
(in-package :cl-tui.layout-primitives)
;; Keyword → integer translation tables. Used by setter functions
;; so callers use (:flex-start) instead of (+yg-justify-flex-start+).
(defparameter *flex-direction-map*
'((:column . 0) (:column-reverse . 1) (:row . 2) (:row-reverse . 3)))
(defparameter *wrap-map*
'((:nowrap . 0) (:wrap . 1) (:wrap-reverse . 2)))
(defparameter *justify-map*
'((:auto . 0) (:flex-start . 1) (:center . 2) (:flex-end . 3)
(:space-between . 4) (:space-around . 5) (:space-evenly . 6)))
(defparameter *align-map*
'((:auto . 0) (:flex-start . 1) (:center . 2) (:flex-end . 3)
(:stretch . 4) (:baseline . 5) (:space-between . 6) (:space-around . 7)
(:space-evenly . 8)))
(defparameter *position-type-map*
'((:static . 0) (:relative . 1) (:absolute . 2)))
(defparameter *overflow-map*
'((:visible . 0) (:hidden . 1) (:scroll . 2)))
(defparameter *display-map*
'((:flex . 0) (:none . 1)))
(defparameter *edge-map*
'((:left . 0) (:top . 1) (:right . 2) (:bottom . 3)
(:start . 4) (:end . 5) (:horizontal . 6) (:vertical . 7) (:all . 8)))
(defparameter *direction-map*
'((:inherit . 0) (:ltr . 1) (:rtl . 2)))
(defun resolve-enum (map keyword)
"Look up KEYWORD in MAP (an alist). Throws if not found."
(or (cdr (assoc keyword map))
(error "Unknown enum keyword ~a" keyword)))
(defclass layout-node ()
((ptr :initarg :ptr :reader layout-node-ptr
:documentation "Raw YGNodeRef pointer")))
(defmethod print-object ((node layout-node) stream)
(print-unreadable-object (node stream :type t)
(format stream "~a" (layout-node-ptr node))))
(defun make-layout-node ()
"Allocate a new Yoga node and wrap it in a layout-node."
(let ((node (make-instance 'layout-node :ptr (yg-node-new))))
(tg:finalize node (lambda () (yg-node-free (layout-node-ptr node))))
node))
(defun layout-node-add-child (parent child)
"Insert CHILD at the end of PARENT's children list."
(let ((count (yg-node-get-child-count (layout-node-ptr parent))))
(yg-node-insert-child (layout-node-ptr parent) (layout-node-ptr child) count)))
(defun layout-node-set-dimension (node width height)
"Set fixed width and height in points."
(yg-node-style-set-width (layout-node-ptr node) (coerce width 'single-float))
(yg-node-style-set-height (layout-node-ptr node) (coerce height 'single-float)))
(defun layout-node-set-flex (node &key grow shrink basis)
"Set flex properties. Unspecified keys are left unchanged."
(let ((p (layout-node-ptr node)))
(when grow (yg-node-style-set-flex-grow p (coerce grow 'single-float)))
(when shrink (yg-node-style-set-flex-shrink p (coerce shrink 'single-float)))
(when basis (yg-node-style-set-flex-basis p (coerce basis 'single-float)))))
(defun layout-node-set-aspect-ratio (node ratio)
"Set aspect ratio (width/height)."
(yg-node-style-set-aspect-ratio (layout-node-ptr node) (coerce ratio 'single-float)))
(defun layout-node-set-direction (node direction)
"Set flex-direction. DIRECTION is :column, :column-reverse, :row, or :row-reverse."
(yg-node-style-set-flex-direction
(layout-node-ptr node)
(resolve-enum *flex-direction-map* direction)))
(defun layout-node-set-wrap (node wrap)
"Set flex-wrap. WRAP is :nowrap, :wrap, or :wrap-reverse."
(yg-node-style-set-flex-wrap
(layout-node-ptr node)
(resolve-enum *wrap-map* wrap)))
(defun layout-node-set-align (node &key items self content)
"Set align-items, align-self, align-content. Values are keywords like :flex-start."
(let ((p (layout-node-ptr node)))
(when items (yg-node-style-set-align-items p (resolve-enum *align-map* items)))
(when self (yg-node-style-set-align-self p (resolve-enum *align-map* self)))
(when content (yg-node-style-set-align-content p (resolve-enum *align-map* content)))))
(defun layout-node-set-justify (node justify)
"Set justify-content. JUSTIFY is :flex-start, :center, :flex-end, :space-between, etc."
(yg-node-style-set-justify-content
(layout-node-ptr node)
(resolve-enum *justify-map* justify)))
(defun layout-node-set-position (node type &key top right bottom left)
"Set position type and offsets. TYPE is :static, :relative, or :absolute."
(let ((p (layout-node-ptr node)))
(yg-node-style-set-position-type p (resolve-enum *position-type-map* type))
(when left (yg-node-style-set-position p +yg-edge-left+ (coerce left 'single-float)))
(when top (yg-node-style-set-position p +yg-edge-top+ (coerce top 'single-float)))
(when right (yg-node-style-set-position p +yg-edge-right+ (coerce right 'single-float)))
(when bottom (yg-node-style-set-position p +yg-edge-bottom+ (coerce bottom 'single-float)))))
(defun set-edges (p fn all top right bottom left x y)
"Helper: call FN on each specified edge. FN is (fn ptr edge value)."
(flet ((s (edge val) (funcall fn p edge (coerce val 'single-float))))
(when all (dolist (e (list +yg-edge-left+ +yg-edge-top+ +yg-edge-right+ +yg-edge-bottom+))
(s e all)))
(when top (s +yg-edge-top+ top))
(when right (s +yg-edge-right+ right))
(when bottom (s +yg-edge-bottom+ bottom))
(when left (s +yg-edge-left+ left))
(when x (s +yg-edge-horizontal+ x))
(when y (s +yg-edge-vertical+ y))))
(defun layout-node-set-padding (node &key all top right bottom left x y)
"Set padding on specified edges in points."
(set-edges (layout-node-ptr node) #'yg-node-style-set-padding all top right bottom left x y))
(defun layout-node-set-margin (node &key all top right bottom left x y)
"Set margin on specified edges in points."
(set-edges (layout-node-ptr node) #'yg-node-style-set-margin all top right bottom left x y))
(defun layout-node-set-border (node width &key all top right bottom left x y)
"Set border width on specified edges."
(let ((p (layout-node-ptr node)))
(flet ((s (edge val) (yg-node-style-set-border p edge (coerce val 'single-float))))
(when all (dolist (e (list +yg-edge-left+ +yg-edge-top+ +yg-edge-right+ +yg-edge-bottom+))
(s e all)))
(when top (s +yg-edge-top+ top))
(when right (s +yg-edge-right+ right))
(when bottom (s +yg-edge-bottom+ bottom))
(when left (s +yg-edge-left+ left))
(when x (s +yg-edge-horizontal+ x))
(when y (s +yg-edge-vertical+ y)))))
(defun layout-node-set-gap (node &key row column)
"Set gap between children."
(let ((p (layout-node-ptr node)))
(when row (yg-node-style-set-gap p +yg-gutter-row+ (coerce row 'single-float)))
(when column (yg-node-style-set-gap p +yg-gutter-column+ (coerce column 'single-float)))))
(defun layout-node-set-overflow (node overflow)
"Set overflow mode. OVERFLOW is :visible, :hidden, or :scroll."
(yg-node-style-set-overflow
(layout-node-ptr node)
(resolve-enum *overflow-map* overflow)))
(defun layout-node-set-display (node display)
"Set display mode. DISPLAY is :flex or :none."
(yg-node-style-set-display
(layout-node-ptr node)
(resolve-enum *display-map* display)))
(defun layout-calculate (root width height &optional (direction :ltr))
"Run Yoga layout on the tree rooted at ROOT.
Returns ROOT (for chaining). Each node's computed position is available via
the raw FFI layout getter functions (yg-node-layout-get-left etc.)."
(yg-node-calculate-layout
(layout-node-ptr root)
(coerce width 'single-float)
(coerce height 'single-float)
(resolve-enum *direction-map* direction))
root)
(eval-when (:compile-toplevel :load-toplevel :execute)
(ql:quickload :fiveam :silent t))
(defpackage :cl-tui.layout-primitives-tests
(:use :cl :fiveam)
(:import-from :cl-tui.layout-primitives
#:make-layout-node
#:layout-node-add-child
#:layout-node-set-dimension
#:layout-node-set-flex
#:layout-node-set-direction
#:layout-node-set-wrap
#:layout-node-set-align
#:layout-node-set-justify
#:layout-node-set-padding
#:layout-node-set-margin
#:layout-node-set-gap
#:layout-node-set-position
#:layout-node-set-border
#:layout-node-set-overflow
#:layout-node-set-display
#:layout-node-set-aspect-ratio
#:layout-calculate
#:layout-node-ptr)
(: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))
(in-package :cl-tui.layout-primitives-tests)
(fiveam:def-suite layout-primitives-suite
:description "Layout primitive CLOS wrappers verification")
(fiveam:in-suite layout-primitives-suite)
(defun node-x (node) (yg-node-layout-get-left (layout-node-ptr node)))
(defun node-y (node) (yg-node-layout-get-top (layout-node-ptr node)))
(defun node-w (node) (yg-node-layout-get-width (layout-node-ptr node)))
(defun node-h (node) (yg-node-layout-get-height (layout-node-ptr node)))
(fiveam:test test-make-layout-node
"Contract: make-layout-node returns a live node."
(let ((n (make-layout-node)))
(fiveam:is (not (cffi:null-pointer-p (layout-node-ptr n))))))
(fiveam:test test-layout-node-add-child
"Contract: adding a child makes it appear in the tree."
(let* ((parent (make-layout-node))
(child (make-layout-node)))
(layout-node-add-child parent child)
(layout-node-set-dimension parent 100 100)
(layout-node-set-dimension child 50 50)
(layout-calculate parent 100 100)
(fiveam:is (= 50.0 (node-w child)))
(fiveam:is (= 50.0 (node-h child)))))
(fiveam:test test-set-dimension
"Contract: layout-node-set-dimension sets width and height."
(let ((n (make-layout-node)))
(layout-node-set-dimension n 200 100)
(layout-calculate n 200 100)
(fiveam:is (= 200.0 (node-w n)))
(fiveam:is (= 100.0 (node-h n)))))
(fiveam:test test-set-direction-column
"Contract: column direction stacks children vertically."
(let* ((root (make-layout-node))
(a (make-layout-node))
(b (make-layout-node)))
(layout-node-set-dimension root 100 200)
(layout-node-set-dimension a 100 50)
(layout-node-set-dimension b 100 50)
(layout-node-add-child root a)
(layout-node-add-child root b)
(layout-node-set-direction root :column)
(layout-calculate root 100 200)
(fiveam:is (= 0.0 (node-y a)))
(fiveam:is (= 50.0 (node-y b)))))
(fiveam:test test-set-direction-row
"Contract: row direction places children horizontally."
(let* ((root (make-layout-node))
(a (make-layout-node))
(b (make-layout-node)))
(layout-node-set-dimension root 200 100)
(layout-node-set-dimension a 80 50)
(layout-node-set-dimension b 80 50)
(layout-node-add-child root a)
(layout-node-add-child root b)
(layout-node-set-direction root :row)
(layout-calculate root 200 100)
(fiveam:is (= 0.0 (node-x a)))
(fiveam:is (= 80.0 (node-x b)))))
(fiveam:test test-set-flex-grow
"Contract: flex-grow distributes remaining space."
(let* ((root (make-layout-node))
(a (make-layout-node))
(b (make-layout-node)))
(layout-node-set-dimension root 200 100)
(layout-node-set-dimension a 0 100)
(layout-node-set-dimension b 0 100)
(layout-node-set-flex a :grow 1)
(layout-node-set-flex b :grow 2)
(layout-node-add-child root a)
(layout-node-add-child root b)
(layout-node-set-direction root :row)
(layout-calculate root 200 100)
(fiveam:is (< 0.0 (node-w a)))
(fiveam:is (< 0.0 (node-w b)))
(fiveam:is (= 200.0 (+ (node-w a) (node-w b))))))
(fiveam:test test-set-align-center
"Contract: align-items :center centers children on the cross axis."
(let* ((root (make-layout-node))
(child (make-layout-node)))
(layout-node-set-dimension root 200 100)
(layout-node-set-dimension child 50 50)
(layout-node-set-direction root :row)
(layout-node-add-child root child)
(layout-node-set-align root :items :center)
(layout-calculate root 200 100)
(fiveam:is (= 25.0 (node-y child)))))
(fiveam:test test-set-justify
"Contract: justify-content :center centers children on the main axis."
(let* ((root (make-layout-node))
(child (make-layout-node)))
(layout-node-set-dimension root 200 100)
(layout-node-set-dimension child 50 50)
(layout-node-set-direction root :row)
(layout-node-add-child root child)
(layout-node-set-justify root :center)
(layout-calculate root 200 100)
(fiveam:is (= 75.0 (node-x child)))))
(fiveam:test test-set-padding
"Contract: padding offsets children from the parent edges."
(let* ((root (make-layout-node))
(child (make-layout-node)))
(layout-node-set-dimension root 200 100)
(layout-node-set-dimension child 100 50)
(layout-node-add-child root child)
(layout-node-set-padding root :all 10)
(layout-calculate root 200 100)
(fiveam:is (= 10.0 (node-x child)))
(fiveam:is (= 10.0 (node-y child)))))
(fiveam:test test-set-margin
"Contract: margin offsets the child from its siblings/parent."
(let* ((root (make-layout-node))
(child (make-layout-node)))
(layout-node-set-dimension root 200 100)
(layout-node-set-dimension child 80 50)
(layout-node-add-child root child)
(layout-node-set-margin child :left 20)
(layout-calculate root 200 100)
(fiveam:is (= 20.0 (node-x child)))))
(fiveam:test test-set-position-absolute
"Contract: absolute positioning places a child at exact coordinates."
(let* ((root (make-layout-node))
(child (make-layout-node)))
(layout-node-set-dimension root 300 300)
(layout-node-set-dimension child 50 50)
(layout-node-add-child root child)
(layout-node-set-position child :absolute :left 100 :top 50)
(layout-calculate root 300 300)
(fiveam:is (= 100.0 (node-x child)))
(fiveam:is (= 50.0 (node-y child)))))
(fiveam:test test-set-wrap
"Contract: flex-wrap :wrap allows children to wrap to next line."
(let* ((root (make-layout-node))
(a (make-layout-node))
(b (make-layout-node))
(c (make-layout-node)))
(layout-node-set-dimension root 100 200)
(layout-node-set-dimension a 60 50)
(layout-node-set-dimension b 60 50)
(layout-node-set-dimension c 60 50)
(layout-node-add-child root a)
(layout-node-add-child root b)
(layout-node-add-child root c)
(layout-node-set-direction root :row)
(layout-node-set-wrap root :wrap)
(layout-calculate root 100 200)
(fiveam:is (< 0 (node-h a)))
;; Second child (b) should wrap to next row since 60+60 > 100
(fiveam:is (> (node-y b) (node-y a)))))
(fiveam:test test-set-gap
"Contract: gap adds spacing between children."
(let* ((root (make-layout-node))
(a (make-layout-node))
(b (make-layout-node)))
(layout-node-set-dimension root 200 100)
(layout-node-set-dimension a 50 50)
(layout-node-set-dimension b 50 50)
(layout-node-add-child root a)
(layout-node-add-child root b)
(layout-node-set-direction root :column)
(layout-node-set-gap root :row 20)
(layout-calculate root 200 100)
(fiveam:is (= 70.0 (node-y b)))))
(fiveam:test test-nested-layout
"Contract: nested containers produce correct leaf positions."
(let* ((root (make-layout-node))
(outer (make-layout-node))
(inner (make-layout-node)))
(layout-node-set-dimension root 400 400)
(layout-node-set-dimension outer 400 200)
(layout-node-set-dimension inner 100 100)
(layout-node-add-child outer inner)
(layout-node-add-child root outer)
(layout-node-set-direction root :column)
(layout-calculate root 400 400)
(fiveam:is (= 0.0 (node-x inner)))
(fiveam:is (= 100.0 (node-w inner)))
(fiveam:is (= 100.0 (node-h inner)))))

View File

@@ -1,631 +0,0 @@
(eval-when (:compile-toplevel :load-toplevel :execute)
(ql:quickload :cffi :silent t))
(defpackage :cl-tui.yoga-ffi
(:use :cl)
(:export
;; library
#:*libyoga*
#:load-yoga
;; constants
#:+yg-direction-inherit+
#:+yg-direction-ltr+
#:+yg-direction-rtl+
#:+yg-flex-direction-column+
#:+yg-flex-direction-column-reverse+
#:+yg-flex-direction-row+
#:+yg-flex-direction-row-reverse+
#:+yg-justify-auto+
#:+yg-justify-flex-start+
#:+yg-justify-center+
#:+yg-justify-flex-end+
#:+yg-justify-space-between+
#:+yg-justify-space-around+
#:+yg-justify-space-evenly+
#:+yg-justify-stretch+
#:+yg-align-auto+
#:+yg-align-flex-start+
#:+yg-align-center+
#:+yg-align-flex-end+
#:+yg-align-stretch+
#:+yg-align-baseline+
#:+yg-align-space-between+
#:+yg-align-space-around+
#:+yg-align-space-evenly+
#:+yg-wrap-nowrap+
#:+yg-wrap-wrap+
#:+yg-wrap-wrap-reverse+
#:+yg-position-type-static+
#:+yg-position-type-relative+
#:+yg-position-type-absolute+
#:+yg-overflow-visible+
#:+yg-overflow-hidden+
#:+yg-overflow-scroll+
#:+yg-display-flex+
#:+yg-display-none+
#:+yg-display-contents+
#:+yg-edge-left+
#:+yg-edge-top+
#:+yg-edge-right+
#:+yg-edge-bottom+
#:+yg-edge-start+
#:+yg-edge-end+
#:+yg-edge-horizontal+
#:+yg-edge-vertical+
#:+yg-edge-all+
#:+yg-gutter-column+
#:+yg-gutter-row+
#:+yg-gutter-all+
#:+yg-unit-undefined+
#:+yg-unit-point+
#:+yg-unit-percent+
#:+yg-unit-auto+
;; types
#:yg-node-ref
#:yg-node-const-ref
;; node management
#:yg-node-new
#:yg-node-free
#:yg-node-free-recursive
#:yg-node-insert-child
#:yg-node-remove-child
#:yg-node-remove-all-children
#:yg-node-get-child-count
#:yg-node-get-child
;; layout
#:yg-node-calculate-layout
#:yg-node-layout-get-left
#:yg-node-layout-get-top
#:yg-node-layout-get-width
#:yg-node-layout-get-height
#:yg-node-layout-get-right
#:yg-node-layout-get-bottom
#:yg-node-is-dirty
#:yg-node-mark-dirty
;; style direction & flex
#:yg-node-style-set-direction
#:yg-node-style-set-flex-direction
#:yg-node-style-set-justify-content
#:yg-node-style-set-align-items
#:yg-node-style-set-align-self
#:yg-node-style-set-align-content
#:yg-node-style-set-flex-wrap
#:yg-node-style-set-position-type
#:yg-node-style-set-flex-grow
#:yg-node-style-set-flex-shrink
#:yg-node-style-set-flex-basis
#:yg-node-style-set-flex-basis-auto
#:yg-node-style-set-flex-basis-percent
#:yg-node-style-set-overflow
#:yg-node-style-set-display
;; style dimensions
#:yg-node-style-set-width
#:yg-node-style-set-width-percent
#:yg-node-style-set-width-auto
#:yg-node-style-set-height
#:yg-node-style-set-height-percent
#:yg-node-style-set-height-auto
#:yg-node-style-set-min-width
#:yg-node-style-set-min-width-percent
#:yg-node-style-set-min-height
#:yg-node-style-set-min-height-percent
#:yg-node-style-set-max-width
#:yg-node-style-set-max-height
#:yg-node-style-set-aspect-ratio
;; style padding/margin/border/gap/position
#:yg-node-style-set-padding
#:yg-node-style-set-padding-percent
#:yg-node-style-set-margin
#:yg-node-style-set-margin-percent
#:yg-node-style-set-margin-auto
#:yg-node-style-set-border
#:yg-node-style-set-gap
#:yg-node-style-set-position
#:yg-node-style-set-position-percent
;; style getters
#:yg-node-style-get-width
#:yg-node-style-get-height
#:yg-node-style-get-flex-direction
#:yg-node-style-get-align-items
#:yg-node-style-get-justify-content))
(in-package :cl-tui.yoga-ffi)
(defparameter *libyoga* nil "Handle for the loaded Yoga shared library.")
(defun load-yoga ()
"Load the Yoga shared library via CFFI."
(setf *libyoga* (cffi:load-foreign-library "/usr/local/lib/libyoga.so"))
(sb-int:set-floating-point-modes :traps '())
*libyoga*)
(load-yoga)
(eval-when (:compile-toplevel :load-toplevel :execute)
;; YGDirection
(defconstant +yg-direction-inherit+ 0)
(defconstant +yg-direction-ltr+ 1)
(defconstant +yg-direction-rtl+ 2)
;; YGFlexDirection
(defconstant +yg-flex-direction-column+ 0)
(defconstant +yg-flex-direction-column-reverse+ 1)
(defconstant +yg-flex-direction-row+ 2)
(defconstant +yg-flex-direction-row-reverse+ 3)
;; YGJustify
(defconstant +yg-justify-auto+ 0)
(defconstant +yg-justify-flex-start+ 1)
(defconstant +yg-justify-center+ 2)
(defconstant +yg-justify-flex-end+ 3)
(defconstant +yg-justify-space-between+ 4)
(defconstant +yg-justify-space-around+ 5)
(defconstant +yg-justify-space-evenly+ 6)
(defconstant +yg-justify-stretch+ 7)
;; YGAlign
(defconstant +yg-align-auto+ 0)
(defconstant +yg-align-flex-start+ 1)
(defconstant +yg-align-center+ 2)
(defconstant +yg-align-flex-end+ 3)
(defconstant +yg-align-stretch+ 4)
(defconstant +yg-align-baseline+ 5)
(defconstant +yg-align-space-between+ 6)
(defconstant +yg-align-space-around+ 7)
(defconstant +yg-align-space-evenly+ 8)
;; YGWrap
(defconstant +yg-wrap-nowrap+ 0)
(defconstant +yg-wrap-wrap+ 1)
(defconstant +yg-wrap-wrap-reverse+ 2)
;; YGPositionType
(defconstant +yg-position-type-static+ 0)
(defconstant +yg-position-type-relative+ 1)
(defconstant +yg-position-type-absolute+ 2)
;; YGOverflow
(defconstant +yg-overflow-visible+ 0)
(defconstant +yg-overflow-hidden+ 1)
(defconstant +yg-overflow-scroll+ 2)
;; YGDisplay
(defconstant +yg-display-flex+ 0)
(defconstant +yg-display-none+ 1)
(defconstant +yg-display-contents+ 2)
;; YGEdge
(defconstant +yg-edge-left+ 0)
(defconstant +yg-edge-top+ 1)
(defconstant +yg-edge-right+ 2)
(defconstant +yg-edge-bottom+ 3)
(defconstant +yg-edge-start+ 4)
(defconstant +yg-edge-end+ 5)
(defconstant +yg-edge-horizontal+ 6)
(defconstant +yg-edge-vertical+ 7)
(defconstant +yg-edge-all+ 8)
;; YGGutter
(defconstant +yg-gutter-column+ 0)
(defconstant +yg-gutter-row+ 1)
(defconstant +yg-gutter-all+ 2)
;; YGUnit
(defconstant +yg-unit-undefined+ 0)
(defconstant +yg-unit-point+ 1)
(defconstant +yg-unit-percent+ 2)
(defconstant +yg-unit-auto+ 3))
(cffi:defctype yg-node-ref :pointer)
(cffi:defctype yg-node-const-ref :pointer)
(cffi:defcstruct yg-size
(width :float)
(height :float))
(cffi:defcstruct yg-value
(value :float)
(unit :int))
(cffi:defcfun ("YGNodeNew" yg-node-new) yg-node-ref)
(cffi:defcfun ("YGNodeFree" yg-node-free) :void
(node yg-node-ref))
(cffi:defcfun ("YGNodeFreeRecursive" yg-node-free-recursive) :void
(node yg-node-ref))
(cffi:defcfun ("YGNodeInsertChild" yg-node-insert-child) :void
(node yg-node-ref)
(child yg-node-ref)
(index :unsigned-int))
(cffi:defcfun ("YGNodeRemoveChild" yg-node-remove-child) :void
(node yg-node-ref)
(child yg-node-ref))
(cffi:defcfun ("YGNodeRemoveAllChildren" yg-node-remove-all-children) :void
(node yg-node-ref))
(cffi:defcfun ("YGNodeGetChildCount" yg-node-get-child-count) :unsigned-int
(node yg-node-const-ref))
(cffi:defcfun ("YGNodeGetChild" yg-node-get-child) yg-node-ref
(node yg-node-ref)
(index :unsigned-int))
(cffi:defcfun ("YGNodeCalculateLayout" yg-node-calculate-layout) :void
(node yg-node-ref)
(available-width :float)
(available-height :float)
(owner-direction :int))
(cffi:defcfun ("YGNodeLayoutGetLeft" yg-node-layout-get-left) :float
(node yg-node-const-ref))
(cffi:defcfun ("YGNodeLayoutGetTop" yg-node-layout-get-top) :float
(node yg-node-const-ref))
(cffi:defcfun ("YGNodeLayoutGetWidth" yg-node-layout-get-width) :float
(node yg-node-const-ref))
(cffi:defcfun ("YGNodeLayoutGetHeight" yg-node-layout-get-height) :float
(node yg-node-const-ref))
(cffi:defcfun ("YGNodeLayoutGetRight" yg-node-layout-get-right) :float
(node yg-node-const-ref))
(cffi:defcfun ("YGNodeLayoutGetBottom" yg-node-layout-get-bottom) :float
(node yg-node-const-ref))
(cffi:defcfun ("YGNodeIsDirty" yg-node-is-dirty) :boolean
(node yg-node-const-ref))
(cffi:defcfun ("YGNodeMarkDirty" yg-node-mark-dirty) :void
(node yg-node-ref))
(cffi:defcfun ("YGNodeStyleSetDirection" yg-node-style-set-direction) :void
(node yg-node-ref)
(direction :int))
(cffi:defcfun ("YGNodeStyleSetFlexDirection" yg-node-style-set-flex-direction) :void
(node yg-node-ref)
(flex-direction :int))
(cffi:defcfun ("YGNodeStyleSetJustifyContent" yg-node-style-set-justify-content) :void
(node yg-node-ref)
(justify :int))
(cffi:defcfun ("YGNodeStyleSetAlignItems" yg-node-style-set-align-items) :void
(node yg-node-ref)
(align :int))
(cffi:defcfun ("YGNodeStyleSetAlignSelf" yg-node-style-set-align-self) :void
(node yg-node-ref)
(align :int))
(cffi:defcfun ("YGNodeStyleSetAlignContent" yg-node-style-set-align-content) :void
(node yg-node-ref)
(align :int))
(cffi:defcfun ("YGNodeStyleSetFlexWrap" yg-node-style-set-flex-wrap) :void
(node yg-node-ref)
(wrap :int))
(cffi:defcfun ("YGNodeStyleSetPositionType" yg-node-style-set-position-type) :void
(node yg-node-ref)
(position-type :int))
(cffi:defcfun ("YGNodeStyleSetFlexGrow" yg-node-style-set-flex-grow) :void
(node yg-node-ref)
(flex-grow :float))
(cffi:defcfun ("YGNodeStyleSetFlexShrink" yg-node-style-set-flex-shrink) :void
(node yg-node-ref)
(flex-shrink :float))
(cffi:defcfun ("YGNodeStyleSetFlexBasis" yg-node-style-set-flex-basis) :void
(node yg-node-ref)
(flex-basis :float))
(cffi:defcfun ("YGNodeStyleSetFlexBasisAuto" yg-node-style-set-flex-basis-auto) :void
(node yg-node-ref))
(cffi:defcfun ("YGNodeStyleSetFlexBasisPercent" yg-node-style-set-flex-basis-percent) :void
(node yg-node-ref)
(flex-basis :float))
(cffi:defcfun ("YGNodeStyleSetOverflow" yg-node-style-set-overflow) :void
(node yg-node-ref)
(overflow :int))
(cffi:defcfun ("YGNodeStyleSetDisplay" yg-node-style-set-display) :void
(node yg-node-ref)
(display :int))
(cffi:defcfun ("YGNodeStyleSetWidth" yg-node-style-set-width) :void
(node yg-node-ref)
(width :float))
(cffi:defcfun ("YGNodeStyleSetWidthPercent" yg-node-style-set-width-percent) :void
(node yg-node-ref)
(width :float))
(cffi:defcfun ("YGNodeStyleSetWidthAuto" yg-node-style-set-width-auto) :void
(node yg-node-ref))
(cffi:defcfun ("YGNodeStyleSetHeight" yg-node-style-set-height) :void
(node yg-node-ref)
(height :float))
(cffi:defcfun ("YGNodeStyleSetHeightPercent" yg-node-style-set-height-percent) :void
(node yg-node-ref)
(height :float))
(cffi:defcfun ("YGNodeStyleSetHeightAuto" yg-node-style-set-height-auto) :void
(node yg-node-ref))
(cffi:defcfun ("YGNodeStyleSetMinWidth" yg-node-style-set-min-width) :void
(node yg-node-ref)
(min-width :float))
(cffi:defcfun ("YGNodeStyleSetMinWidthPercent" yg-node-style-set-min-width-percent) :void
(node yg-node-ref)
(min-width :float))
(cffi:defcfun ("YGNodeStyleSetMinHeight" yg-node-style-set-min-height) :void
(node yg-node-ref)
(min-height :float))
(cffi:defcfun ("YGNodeStyleSetMinHeightPercent" yg-node-style-set-min-height-percent) :void
(node yg-node-ref)
(min-height :float))
(cffi:defcfun ("YGNodeStyleSetMaxWidth" yg-node-style-set-max-width) :void
(node yg-node-ref)
(max-width :float))
(cffi:defcfun ("YGNodeStyleSetMaxHeight" yg-node-style-set-max-height) :void
(node yg-node-ref)
(max-height :float))
(cffi:defcfun ("YGNodeStyleSetAspectRatio" yg-node-style-set-aspect-ratio) :void
(node yg-node-ref)
(aspect-ratio :float))
(cffi:defcfun ("YGNodeStyleSetPadding" yg-node-style-set-padding) :void
(node yg-node-ref)
(edge :int)
(padding :float))
(cffi:defcfun ("YGNodeStyleSetPaddingPercent" yg-node-style-set-padding-percent) :void
(node yg-node-ref)
(edge :int)
(padding :float))
(cffi:defcfun ("YGNodeStyleSetMargin" yg-node-style-set-margin) :void
(node yg-node-ref)
(edge :int)
(margin :float))
(cffi:defcfun ("YGNodeStyleSetMarginPercent" yg-node-style-set-margin-percent) :void
(node yg-node-ref)
(edge :int)
(margin :float))
(cffi:defcfun ("YGNodeStyleSetMarginAuto" yg-node-style-set-margin-auto) :void
(node yg-node-ref)
(edge :int))
(cffi:defcfun ("YGNodeStyleSetBorder" yg-node-style-set-border) :void
(node yg-node-ref)
(edge :int)
(border :float))
(cffi:defcfun ("YGNodeStyleSetGap" yg-node-style-set-gap) :void
(node yg-node-ref)
(gutter :int)
(gap :float))
(cffi:defcfun ("YGNodeStyleSetPosition" yg-node-style-set-position) :void
(node yg-node-ref)
(edge :int)
(position :float))
(cffi:defcfun ("YGNodeStyleSetPositionPercent" yg-node-style-set-position-percent) :void
(node yg-node-ref)
(edge :int)
(position :float))
(cffi:defcfun ("YGNodeStyleGetWidth" yg-node-style-get-width) yg-value
(node yg-node-const-ref))
(cffi:defcfun ("YGNodeStyleGetHeight" yg-node-style-get-height) yg-value
(node yg-node-const-ref))
(cffi:defcfun ("YGNodeStyleGetFlexDirection" yg-node-style-get-flex-direction) :int
(node yg-node-const-ref))
(cffi:defcfun ("YGNodeStyleGetAlignItems" yg-node-style-get-align-items) :int
(node yg-node-const-ref))
(cffi:defcfun ("YGNodeStyleGetJustifyContent" yg-node-style-get-justify-content) :int
(node yg-node-const-ref))
(eval-when (:compile-toplevel :load-toplevel :execute)
(ql:quickload :fiveam :silent t))
(defpackage :cl-tui.yoga-ffi-tests
(:use :cl :fiveam)
(:import-from :cl-tui.yoga-ffi
#:load-yoga
#:yg-node-new
#:yg-node-free
#:yg-node-free-recursive
#:yg-node-insert-child
#:yg-node-get-child-count
#:yg-node-calculate-layout
#:yg-node-layout-get-left
#:yg-node-layout-get-top
#:yg-node-layout-get-width
#:yg-node-layout-get-height
#:yg-node-style-set-flex-direction
#:yg-node-style-set-justify-content
#:yg-node-style-set-align-items
#:yg-node-style-set-width
#:yg-node-style-set-height
#:yg-node-style-set-padding
#:yg-node-style-set-margin
#:yg-node-style-set-flex-grow
#:yg-node-style-set-position-type
#:yg-node-style-set-position
#:+yg-flex-direction-column+
#:+yg-flex-direction-row+
#:+yg-justify-center+
#:+yg-justify-flex-start+
#:+yg-align-stretch+
#:+yg-align-center+
#:+yg-edge-all+
#:+yg-edge-left+
#:+yg-edge-top+
#:+yg-edge-right+
#:+yg-edge-bottom+
#:+yg-position-type-relative+
#:+yg-position-type-absolute+
;; YGDirection constants (different from YGFlexDirection!)
#:+yg-direction-inherit+
#:+yg-direction-ltr+
#:+yg-direction-rtl+))
(in-package :cl-tui.yoga-ffi-tests)
(fiveam:def-suite yoga-ffi-suite
:description "Yoga FFI binding verification")
(fiveam:in-suite yoga-ffi-suite)
(fiveam:test test-node-create-free
"Contract: yg-node-new returns a non-null pointer; yg-node-free doesn't crash."
(let ((node (yg-node-new)))
(fiveam:is (not (cffi:null-pointer-p node)))
(yg-node-free node)
(fiveam:pass)))
(fiveam:test test-node-child-count
"Contract: yg-node-get-child-count returns 0 for a new node."
(let ((node (yg-node-new)))
(fiveam:is (= 0 (yg-node-get-child-count node)))
(yg-node-free node)))
(fiveam:test test-node-insert-child
"Contract: inserting a child increments the child count."
(let ((parent (yg-node-new))
(child (yg-node-new)))
(yg-node-insert-child parent child 0)
(fiveam:is (= 1 (yg-node-get-child-count parent)))
(yg-node-free-recursive parent)))
(fiveam:test test-layout-basic-column
"Contract: a column with two fixed-height children positions them vertically."
(let* ((root (yg-node-new)))
(yg-node-style-set-width root 100.0)
(yg-node-style-set-height root 200.0)
(yg-node-style-set-flex-direction root +yg-flex-direction-column+)
(let ((child1 (yg-node-new))
(child2 (yg-node-new)))
(yg-node-style-set-width child1 100.0)
(yg-node-style-set-height child1 50.0)
(yg-node-style-set-width child2 100.0)
(yg-node-style-set-height child2 50.0)
(yg-node-insert-child root child1 0)
(yg-node-insert-child root child2 1)
(yg-node-calculate-layout root 100.0 200.0 +yg-direction-ltr+)
(fiveam:is (= 0.0 (yg-node-layout-get-left child1)))
(fiveam:is (= 0.0 (yg-node-layout-get-top child1)))
(fiveam:is (= 50.0 (yg-node-layout-get-top child2)))
(yg-node-free-recursive root))))
(fiveam:test test-layout-basic-row
"Contract: a row with two fixed-width children positions them horizontally."
(let* ((root (yg-node-new)))
(yg-node-style-set-width root 200.0)
(yg-node-style-set-height root 100.0)
(yg-node-style-set-flex-direction root +yg-flex-direction-row+)
(let ((child1 (yg-node-new))
(child2 (yg-node-new)))
(yg-node-style-set-width child1 80.0)
(yg-node-style-set-height child1 50.0)
(yg-node-style-set-width child2 80.0)
(yg-node-style-set-height child2 50.0)
(yg-node-insert-child root child1 0)
(yg-node-insert-child root child2 1)
(yg-node-calculate-layout root 200.0 100.0 +yg-direction-ltr+)
(fiveam:is (= 0.0 (yg-node-layout-get-left child1)))
(fiveam:is (= 0.0 (yg-node-layout-get-top child1)))
(fiveam:is (= 80.0 (yg-node-layout-get-left child2)))
(yg-node-free-recursive root))))
(fiveam:test test-layout-flex-grow
"Contract: flex-grow distributes remaining space proportionally."
(let* ((root (yg-node-new)))
(yg-node-style-set-width root 200.0)
(yg-node-style-set-height root 100.0)
(yg-node-style-set-flex-direction root +yg-flex-direction-row+)
(let ((child1 (yg-node-new))
(child2 (yg-node-new)))
(yg-node-style-set-height child1 100.0)
(yg-node-style-set-flex-grow child1 1.0)
(yg-node-style-set-height child2 100.0)
(yg-node-style-set-flex-grow child2 2.0)
(yg-node-insert-child root child1 0)
(yg-node-insert-child root child2 1)
(yg-node-calculate-layout root 200.0 100.0 +yg-direction-ltr+)
(fiveam:is (< 0.0 (yg-node-layout-get-width child1)))
(fiveam:is (< 0.0 (yg-node-layout-get-width child2)))
(fiveam:is (= 200.0 (+ (yg-node-layout-get-width child1)
(yg-node-layout-get-width child2))))
(yg-node-free-recursive root))))
(fiveam:test test-layout-absolute-position
"Contract: an absolute child positions relative to its parent."
(let* ((root (yg-node-new)))
(yg-node-style-set-width root 300.0)
(yg-node-style-set-height root 300.0)
(let ((child (yg-node-new)))
(yg-node-style-set-width child 50.0)
(yg-node-style-set-height child 50.0)
(yg-node-style-set-position-type child +yg-position-type-absolute+)
(yg-node-style-set-position child +yg-edge-left+ 100.0)
(yg-node-style-set-position child +yg-edge-top+ 50.0)
(yg-node-insert-child root child 0)
(yg-node-calculate-layout root 300.0 300.0 +yg-direction-ltr+)
(fiveam:is (= 100.0 (yg-node-layout-get-left child)))
(fiveam:is (= 50.0 (yg-node-layout-get-top child)))
(fiveam:is (= 50.0 (yg-node-layout-get-width child)))
(fiveam:is (= 50.0 (yg-node-layout-get-height child)))
(yg-node-free-recursive root))))
(fiveam:test test-layout-padding
"Contract: padding reduces the available space for children."
(let* ((root (yg-node-new)))
(yg-node-style-set-width root 200.0)
(yg-node-style-set-height root 100.0)
(yg-node-style-set-padding root +yg-edge-all+ 10.0)
(let ((child (yg-node-new)))
(yg-node-style-set-width child 180.0)
(yg-node-style-set-height child 80.0)
(yg-node-insert-child root child 0)
(yg-node-calculate-layout root 200.0 100.0 +yg-direction-ltr+)
(fiveam:is (= 10.0 (yg-node-layout-get-left child)))
(fiveam:is (= 10.0 (yg-node-layout-get-top child)))
(yg-node-free-recursive root))))
(fiveam:test test-layout-nested
"Contract: nested containers produce correct leaf positions."
(let* ((root (yg-node-new)))
(yg-node-style-set-width root 400.0)
(yg-node-style-set-height root 400.0)
(yg-node-style-set-flex-direction root +yg-flex-direction-column+)
(let ((outer (yg-node-new)))
(yg-node-style-set-width outer 400.0)
(yg-node-style-set-height outer 200.0)
(yg-node-style-set-flex-direction outer +yg-flex-direction-row+)
(let ((inner (yg-node-new)))
(yg-node-style-set-width inner 100.0)
(yg-node-style-set-height inner 100.0)
(yg-node-insert-child outer inner 0)
(yg-node-insert-child root outer 0)
(yg-node-calculate-layout root 400.0 400.0 +yg-direction-ltr+)
(fiveam:is (= 0.0 (yg-node-layout-get-left inner)))
(fiveam:is (= 100.0 (yg-node-layout-get-width inner)))
(fiveam:is (= 100.0 (yg-node-layout-get-height inner)))
(yg-node-free-recursive root)))))

View File

@@ -1,248 +0,0 @@
#+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

View File

@@ -1,608 +0,0 @@
#+TITLE: Layout Primitives
#+STARTUP: content
#+FILETAGS: :cl-tui:layout-primitives:v010:
* Layout Primitives
CLOS wrappers around the raw Yoga FFI bindings. Each =layout-node= wraps a
=YGNodeRef= with automatic finalization. Setter functions translate Lisp
keywords to Yoga enum integers, providing a safe, idiomatic Common Lisp API.
This file depends on =yoga-ffi.lisp= (the CFFI bindings). The composable API
(=vbox=, =hbox=, =overlay=, =spacer=) is in =layout-composable.org=.
** Contract
- =(make-layout-node)= → layout-node :: allocates a new Yoga node, wraps it in
a CLOS instance. The YGNodeRef is freed when the layout-node is garbage
collected (via trivial-garbage).
- =(layout-node-ptr node)= → YGNodeRef :: returns the raw C pointer for use
with raw FFI functions.
- =(layout-node-add-child parent child)= :: inserts child at the end of
parent's children list. Throws if child is nil.
- =(layout-node-set-dimension node width height)= :: sets fixed width and
height in points.
- =(layout-node-set-flex node &key grow shrink basis)= :: sets flex-grow,
flex-shrink, flex-basis. Unspecified keys are left unchanged.
- =(layout-node-set-direction node direction)= :: sets flex-direction.
direction is one of: :column :column-reverse :row :row-reverse.
- =(layout-node-set-wrap node wrap)= :: sets flex-wrap.
wrap is one of: :nowrap :wrap :wrap-reverse.
- =(layout-node-set-align node &key items self content)= :: sets align-items,
align-self, align-content. Values from: :auto, :flex-start, :center,
:flex-end, :stretch, :baseline, :space-between, :space-around, :space-evenly.
- =(layout-node-set-justify node justify)= :: sets justify-content. Values
from: :auto :flex-start :center :flex-end :space-between :space-around
:space-evenly.
- =(layout-node-set-padding node &key all top right bottom left x y)= :: sets
padding in points on specified edges. :all sets all 4 edges.
- =(layout-node-set-margin node &key all top right bottom left x y)= :: sets
margin in points.
- =(layout-node-set-gap node &key row column)= :: sets gap between children.
- =(layout-node-set-position node type &key top right bottom left)= :: sets
position type (:static :relative :absolute) and offsets.
- =(layout-node-set-border node width &key top right bottom left all)= ::
sets border width on edges.
- =(layout-node-set-overflow node overflow)= :: sets overflow mode. Values:
:visible :hidden :scroll.
- =(layout-node-set-display node display)= :: sets display mode. Values: :flex
:none.
- =(layout-node-set-aspect-ratio node ratio)= :: sets aspect ratio.
- =(layout-calculate root width height &optional (direction :ltr))= :: runs
Yoga's calculateLayout, populating each node's computed x/y/w/h.
* Package and Dependencies
#+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-primitives
(:use :cl)
(:import-from :cl-tui.yoga-ffi
#:load-yoga
#:yg-node-new
#:yg-node-free
#:yg-node-insert-child
#:yg-node-remove-child
#:yg-node-get-child-count
#:yg-node-calculate-layout
#:yg-node-layout-get-left
#:yg-node-layout-get-top
#:yg-node-layout-get-width
#:yg-node-layout-get-height
#:yg-node-layout-get-right
#:yg-node-layout-get-bottom
#:yg-node-style-set-direction
#:yg-node-style-set-flex-direction
#:yg-node-style-set-justify-content
#:yg-node-style-set-align-items
#:yg-node-style-set-align-self
#:yg-node-style-set-align-content
#:yg-node-style-set-flex-wrap
#:yg-node-style-set-position-type
#:yg-node-style-set-flex-grow
#:yg-node-style-set-flex-shrink
#:yg-node-style-set-flex-basis
#:yg-node-style-set-flex-basis-auto
#:yg-node-style-set-overflow
#:yg-node-style-set-display
#:yg-node-style-set-width
#:yg-node-style-set-width-auto
#:yg-node-style-set-height
#:yg-node-style-set-height-auto
#:yg-node-style-set-min-width
#:yg-node-style-set-min-height
#:yg-node-style-set-max-width
#:yg-node-style-set-max-height
#:yg-node-style-set-aspect-ratio
#:yg-node-style-set-padding
#:yg-node-style-set-margin
#:yg-node-style-set-margin-auto
#:yg-node-style-set-border
#:yg-node-style-set-gap
#:yg-node-style-set-position
;; enum constants
#:+yg-flex-direction-column+
#:+yg-flex-direction-column-reverse+
#:+yg-flex-direction-row+
#:+yg-flex-direction-row-reverse+
#:+yg-wrap-nowrap+
#:+yg-wrap-wrap+
#:+yg-wrap-wrap-reverse+
#:+yg-justify-auto+
#:+yg-justify-flex-start+
#:+yg-justify-center+
#:+yg-justify-flex-end+
#:+yg-justify-space-between+
#:+yg-justify-space-around+
#:+yg-justify-space-evenly+
#:+yg-align-auto+
#:+yg-align-flex-start+
#:+yg-align-center+
#:+yg-align-flex-end+
#:+yg-align-stretch+
#:+yg-align-baseline+
#:+yg-align-space-between+
#:+yg-align-space-around+
#:+yg-align-space-evenly+
#:+yg-position-type-static+
#:+yg-position-type-relative+
#:+yg-position-type-absolute+
#:+yg-overflow-visible+
#:+yg-overflow-hidden+
#:+yg-overflow-scroll+
#:+yg-display-flex+
#:+yg-display-none+
#:+yg-edge-left+
#:+yg-edge-top+
#:+yg-edge-right+
#:+yg-edge-bottom+
#:+yg-edge-all+
#:+yg-edge-start+
#:+yg-edge-end+
#:+yg-edge-horizontal+
#:+yg-edge-vertical+
#:+yg-gutter-column+
#:+yg-gutter-row+
#:+yg-gutter-all+
#:+yg-direction-inherit+
#:+yg-direction-ltr+
#:+yg-direction-rtl+)
(:export
#:layout-node
#:layout-node-ptr
#:make-layout-node
#:layout-node-add-child
#:layout-node-set-dimension
#:layout-node-set-flex
#:layout-node-set-direction
#:layout-node-set-wrap
#:layout-node-set-align
#:layout-node-set-justify
#:layout-node-set-padding
#:layout-node-set-margin
#:layout-node-set-gap
#:layout-node-set-position
#:layout-node-set-border
#:layout-node-set-overflow
#:layout-node-set-display
#:layout-node-set-aspect-ratio
#:layout-calculate))
(in-package :cl-tui.layout-primitives)
#+end_src
* Enum Translation Tables
#+begin_src lisp
;; Keyword → integer translation tables. Used by setter functions
;; so callers use (:flex-start) instead of (+yg-justify-flex-start+).
(defparameter *flex-direction-map*
'((:column . 0) (:column-reverse . 1) (:row . 2) (:row-reverse . 3)))
(defparameter *wrap-map*
'((:nowrap . 0) (:wrap . 1) (:wrap-reverse . 2)))
(defparameter *justify-map*
'((:auto . 0) (:flex-start . 1) (:center . 2) (:flex-end . 3)
(:space-between . 4) (:space-around . 5) (:space-evenly . 6)))
(defparameter *align-map*
'((:auto . 0) (:flex-start . 1) (:center . 2) (:flex-end . 3)
(:stretch . 4) (:baseline . 5) (:space-between . 6) (:space-around . 7)
(:space-evenly . 8)))
(defparameter *position-type-map*
'((:static . 0) (:relative . 1) (:absolute . 2)))
(defparameter *overflow-map*
'((:visible . 0) (:hidden . 1) (:scroll . 2)))
(defparameter *display-map*
'((:flex . 0) (:none . 1)))
(defparameter *edge-map*
'((:left . 0) (:top . 1) (:right . 2) (:bottom . 3)
(:start . 4) (:end . 5) (:horizontal . 6) (:vertical . 7) (:all . 8)))
(defparameter *direction-map*
'((:inherit . 0) (:ltr . 1) (:rtl . 2)))
(defun resolve-enum (map keyword)
"Look up KEYWORD in MAP (an alist). Throws if not found."
(or (cdr (assoc keyword map))
(error "Unknown enum keyword ~a" keyword)))
#+end_src
* Layout Node Class
#+begin_src lisp
(defclass layout-node ()
((ptr :initarg :ptr :reader layout-node-ptr
:documentation "Raw YGNodeRef pointer")))
(defmethod print-object ((node layout-node) stream)
(print-unreadable-object (node stream :type t)
(format stream "~a" (layout-node-ptr node))))
(defun make-layout-node ()
"Allocate a new Yoga node and wrap it in a layout-node."
(let ((node (make-instance 'layout-node :ptr (yg-node-new))))
(tg:finalize node (lambda () (yg-node-free (layout-node-ptr node))))
node))
(defun layout-node-add-child (parent child)
"Insert CHILD at the end of PARENT's children list."
(let ((count (yg-node-get-child-count (layout-node-ptr parent))))
(yg-node-insert-child (layout-node-ptr parent) (layout-node-ptr child) count)))
#+end_src
* Dimension Setters
#+begin_src lisp
(defun layout-node-set-dimension (node width height)
"Set fixed width and height in points."
(yg-node-style-set-width (layout-node-ptr node) (coerce width 'single-float))
(yg-node-style-set-height (layout-node-ptr node) (coerce height 'single-float)))
(defun layout-node-set-flex (node &key grow shrink basis)
"Set flex properties. Unspecified keys are left unchanged."
(let ((p (layout-node-ptr node)))
(when grow (yg-node-style-set-flex-grow p (coerce grow 'single-float)))
(when shrink (yg-node-style-set-flex-shrink p (coerce shrink 'single-float)))
(when basis (yg-node-style-set-flex-basis p (coerce basis 'single-float)))))
(defun layout-node-set-aspect-ratio (node ratio)
"Set aspect ratio (width/height)."
(yg-node-style-set-aspect-ratio (layout-node-ptr node) (coerce ratio 'single-float)))
#+end_src
* Layout Direction and Wrapping
#+begin_src lisp
(defun layout-node-set-direction (node direction)
"Set flex-direction. DIRECTION is :column, :column-reverse, :row, or :row-reverse."
(yg-node-style-set-flex-direction
(layout-node-ptr node)
(resolve-enum *flex-direction-map* direction)))
(defun layout-node-set-wrap (node wrap)
"Set flex-wrap. WRAP is :nowrap, :wrap, or :wrap-reverse."
(yg-node-style-set-flex-wrap
(layout-node-ptr node)
(resolve-enum *wrap-map* wrap)))
#+end_src
* Alignment and Justification
#+begin_src lisp
(defun layout-node-set-align (node &key items self content)
"Set align-items, align-self, align-content. Values are keywords like :flex-start."
(let ((p (layout-node-ptr node)))
(when items (yg-node-style-set-align-items p (resolve-enum *align-map* items)))
(when self (yg-node-style-set-align-self p (resolve-enum *align-map* self)))
(when content (yg-node-style-set-align-content p (resolve-enum *align-map* content)))))
(defun layout-node-set-justify (node justify)
"Set justify-content. JUSTIFY is :flex-start, :center, :flex-end, :space-between, etc."
(yg-node-style-set-justify-content
(layout-node-ptr node)
(resolve-enum *justify-map* justify)))
#+end_src
* Position Type and Offsets
#+begin_src lisp
(defun layout-node-set-position (node type &key top right bottom left)
"Set position type and offsets. TYPE is :static, :relative, or :absolute."
(let ((p (layout-node-ptr node)))
(yg-node-style-set-position-type p (resolve-enum *position-type-map* type))
(when left (yg-node-style-set-position p +yg-edge-left+ (coerce left 'single-float)))
(when top (yg-node-style-set-position p +yg-edge-top+ (coerce top 'single-float)))
(when right (yg-node-style-set-position p +yg-edge-right+ (coerce right 'single-float)))
(when bottom (yg-node-style-set-position p +yg-edge-bottom+ (coerce bottom 'single-float)))))
#+end_src
* Padding, Margin, Border, Gap
#+begin_src lisp
(defun set-edges (p fn all top right bottom left x y)
"Helper: call FN on each specified edge. FN is (fn ptr edge value)."
(flet ((s (edge val) (funcall fn p edge (coerce val 'single-float))))
(when all (dolist (e (list +yg-edge-left+ +yg-edge-top+ +yg-edge-right+ +yg-edge-bottom+))
(s e all)))
(when top (s +yg-edge-top+ top))
(when right (s +yg-edge-right+ right))
(when bottom (s +yg-edge-bottom+ bottom))
(when left (s +yg-edge-left+ left))
(when x (s +yg-edge-horizontal+ x))
(when y (s +yg-edge-vertical+ y))))
(defun layout-node-set-padding (node &key all top right bottom left x y)
"Set padding on specified edges in points."
(set-edges (layout-node-ptr node) #'yg-node-style-set-padding all top right bottom left x y))
(defun layout-node-set-margin (node &key all top right bottom left x y)
"Set margin on specified edges in points."
(set-edges (layout-node-ptr node) #'yg-node-style-set-margin all top right bottom left x y))
(defun layout-node-set-border (node width &key all top right bottom left x y)
"Set border width on specified edges."
(let ((p (layout-node-ptr node)))
(flet ((s (edge val) (yg-node-style-set-border p edge (coerce val 'single-float))))
(when all (dolist (e (list +yg-edge-left+ +yg-edge-top+ +yg-edge-right+ +yg-edge-bottom+))
(s e all)))
(when top (s +yg-edge-top+ top))
(when right (s +yg-edge-right+ right))
(when bottom (s +yg-edge-bottom+ bottom))
(when left (s +yg-edge-left+ left))
(when x (s +yg-edge-horizontal+ x))
(when y (s +yg-edge-vertical+ y)))))
(defun layout-node-set-gap (node &key row column)
"Set gap between children."
(let ((p (layout-node-ptr node)))
(when row (yg-node-style-set-gap p +yg-gutter-row+ (coerce row 'single-float)))
(when column (yg-node-style-set-gap p +yg-gutter-column+ (coerce column 'single-float)))))
#+end_src
* Overflow and Display
#+begin_src lisp
(defun layout-node-set-overflow (node overflow)
"Set overflow mode. OVERFLOW is :visible, :hidden, or :scroll."
(yg-node-style-set-overflow
(layout-node-ptr node)
(resolve-enum *overflow-map* overflow)))
(defun layout-node-set-display (node display)
"Set display mode. DISPLAY is :flex or :none."
(yg-node-style-set-display
(layout-node-ptr node)
(resolve-enum *display-map* display)))
#+end_src
* Layout Calculation
#+begin_src lisp
(defun layout-calculate (root width height &optional (direction :ltr))
"Run Yoga layout on the tree rooted at ROOT.
Returns ROOT (for chaining). Each node's computed position is available via
the raw FFI layout getter functions (yg-node-layout-get-left etc.)."
(yg-node-calculate-layout
(layout-node-ptr root)
(coerce width 'single-float)
(coerce height 'single-float)
(resolve-enum *direction-map* direction))
root)
#+end_src
* Test Suite
#+begin_src lisp
(eval-when (:compile-toplevel :load-toplevel :execute)
(ql:quickload :fiveam :silent t))
(defpackage :cl-tui.layout-primitives-tests
(:use :cl :fiveam)
(:import-from :cl-tui.layout-primitives
#:make-layout-node
#:layout-node-add-child
#:layout-node-set-dimension
#:layout-node-set-flex
#:layout-node-set-direction
#:layout-node-set-wrap
#:layout-node-set-align
#:layout-node-set-justify
#:layout-node-set-padding
#:layout-node-set-margin
#:layout-node-set-gap
#:layout-node-set-position
#:layout-node-set-border
#:layout-node-set-overflow
#:layout-node-set-display
#:layout-node-set-aspect-ratio
#:layout-calculate
#:layout-node-ptr)
(: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))
(in-package :cl-tui.layout-primitives-tests)
(fiveam:def-suite layout-primitives-suite
:description "Layout primitive CLOS wrappers verification")
(fiveam:in-suite layout-primitives-suite)
(defun node-x (node) (yg-node-layout-get-left (layout-node-ptr node)))
(defun node-y (node) (yg-node-layout-get-top (layout-node-ptr node)))
(defun node-w (node) (yg-node-layout-get-width (layout-node-ptr node)))
(defun node-h (node) (yg-node-layout-get-height (layout-node-ptr node)))
(fiveam:test test-make-layout-node
"Contract: make-layout-node returns a live node."
(let ((n (make-layout-node)))
(fiveam:is (not (cffi:null-pointer-p (layout-node-ptr n))))))
(fiveam:test test-layout-node-add-child
"Contract: adding a child makes it appear in the tree."
(let* ((parent (make-layout-node))
(child (make-layout-node)))
(layout-node-add-child parent child)
(layout-node-set-dimension parent 100 100)
(layout-node-set-dimension child 50 50)
(layout-calculate parent 100 100)
(fiveam:is (= 50.0 (node-w child)))
(fiveam:is (= 50.0 (node-h child)))))
(fiveam:test test-set-dimension
"Contract: layout-node-set-dimension sets width and height."
(let ((n (make-layout-node)))
(layout-node-set-dimension n 200 100)
(layout-calculate n 200 100)
(fiveam:is (= 200.0 (node-w n)))
(fiveam:is (= 100.0 (node-h n)))))
(fiveam:test test-set-direction-column
"Contract: column direction stacks children vertically."
(let* ((root (make-layout-node))
(a (make-layout-node))
(b (make-layout-node)))
(layout-node-set-dimension root 100 200)
(layout-node-set-dimension a 100 50)
(layout-node-set-dimension b 100 50)
(layout-node-add-child root a)
(layout-node-add-child root b)
(layout-node-set-direction root :column)
(layout-calculate root 100 200)
(fiveam:is (= 0.0 (node-y a)))
(fiveam:is (= 50.0 (node-y b)))))
(fiveam:test test-set-direction-row
"Contract: row direction places children horizontally."
(let* ((root (make-layout-node))
(a (make-layout-node))
(b (make-layout-node)))
(layout-node-set-dimension root 200 100)
(layout-node-set-dimension a 80 50)
(layout-node-set-dimension b 80 50)
(layout-node-add-child root a)
(layout-node-add-child root b)
(layout-node-set-direction root :row)
(layout-calculate root 200 100)
(fiveam:is (= 0.0 (node-x a)))
(fiveam:is (= 80.0 (node-x b)))))
(fiveam:test test-set-flex-grow
"Contract: flex-grow distributes remaining space."
(let* ((root (make-layout-node))
(a (make-layout-node))
(b (make-layout-node)))
(layout-node-set-dimension root 200 100)
(layout-node-set-dimension a 0 100)
(layout-node-set-dimension b 0 100)
(layout-node-set-flex a :grow 1)
(layout-node-set-flex b :grow 2)
(layout-node-add-child root a)
(layout-node-add-child root b)
(layout-node-set-direction root :row)
(layout-calculate root 200 100)
(fiveam:is (< 0.0 (node-w a)))
(fiveam:is (< 0.0 (node-w b)))
(fiveam:is (= 200.0 (+ (node-w a) (node-w b))))))
(fiveam:test test-set-align-center
"Contract: align-items :center centers children on the cross axis."
(let* ((root (make-layout-node))
(child (make-layout-node)))
(layout-node-set-dimension root 200 100)
(layout-node-set-dimension child 50 50)
(layout-node-set-direction root :row)
(layout-node-add-child root child)
(layout-node-set-align root :items :center)
(layout-calculate root 200 100)
(fiveam:is (= 25.0 (node-y child)))))
(fiveam:test test-set-justify
"Contract: justify-content :center centers children on the main axis."
(let* ((root (make-layout-node))
(child (make-layout-node)))
(layout-node-set-dimension root 200 100)
(layout-node-set-dimension child 50 50)
(layout-node-set-direction root :row)
(layout-node-add-child root child)
(layout-node-set-justify root :center)
(layout-calculate root 200 100)
(fiveam:is (= 75.0 (node-x child)))))
(fiveam:test test-set-padding
"Contract: padding offsets children from the parent edges."
(let* ((root (make-layout-node))
(child (make-layout-node)))
(layout-node-set-dimension root 200 100)
(layout-node-set-dimension child 100 50)
(layout-node-add-child root child)
(layout-node-set-padding root :all 10)
(layout-calculate root 200 100)
(fiveam:is (= 10.0 (node-x child)))
(fiveam:is (= 10.0 (node-y child)))))
(fiveam:test test-set-margin
"Contract: margin offsets the child from its siblings/parent."
(let* ((root (make-layout-node))
(child (make-layout-node)))
(layout-node-set-dimension root 200 100)
(layout-node-set-dimension child 80 50)
(layout-node-add-child root child)
(layout-node-set-margin child :left 20)
(layout-calculate root 200 100)
(fiveam:is (= 20.0 (node-x child)))))
(fiveam:test test-set-position-absolute
"Contract: absolute positioning places a child at exact coordinates."
(let* ((root (make-layout-node))
(child (make-layout-node)))
(layout-node-set-dimension root 300 300)
(layout-node-set-dimension child 50 50)
(layout-node-add-child root child)
(layout-node-set-position child :absolute :left 100 :top 50)
(layout-calculate root 300 300)
(fiveam:is (= 100.0 (node-x child)))
(fiveam:is (= 50.0 (node-y child)))))
(fiveam:test test-set-wrap
"Contract: flex-wrap :wrap allows children to wrap to next line."
(let* ((root (make-layout-node))
(a (make-layout-node))
(b (make-layout-node))
(c (make-layout-node)))
(layout-node-set-dimension root 100 200)
(layout-node-set-dimension a 60 50)
(layout-node-set-dimension b 60 50)
(layout-node-set-dimension c 60 50)
(layout-node-add-child root a)
(layout-node-add-child root b)
(layout-node-add-child root c)
(layout-node-set-direction root :row)
(layout-node-set-wrap root :wrap)
(layout-calculate root 100 200)
(fiveam:is (< 0 (node-h a)))
;; Second child (b) should wrap to next row since 60+60 > 100
(fiveam:is (> (node-y b) (node-y a)))))
(fiveam:test test-set-gap
"Contract: gap adds spacing between children."
(let* ((root (make-layout-node))
(a (make-layout-node))
(b (make-layout-node)))
(layout-node-set-dimension root 200 100)
(layout-node-set-dimension a 50 50)
(layout-node-set-dimension b 50 50)
(layout-node-add-child root a)
(layout-node-add-child root b)
(layout-node-set-direction root :column)
(layout-node-set-gap root :row 20)
(layout-calculate root 200 100)
(fiveam:is (= 70.0 (node-y b)))))
(fiveam:test test-nested-layout
"Contract: nested containers produce correct leaf positions."
(let* ((root (make-layout-node))
(outer (make-layout-node))
(inner (make-layout-node)))
(layout-node-set-dimension root 400 400)
(layout-node-set-dimension outer 400 200)
(layout-node-set-dimension inner 100 100)
(layout-node-add-child outer inner)
(layout-node-add-child root outer)
(layout-node-set-direction root :column)
(layout-calculate root 400 400)
(fiveam:is (= 0.0 (node-x inner)))
(fiveam:is (= 100.0 (node-w inner)))
(fiveam:is (= 100.0 (node-h inner)))))
#+end_src

View File

@@ -1,755 +0,0 @@
#+TITLE: Yoga FFI Binding
#+STARTUP: content
#+FILETAGS: :cl-tui:yoga-ffi:v010:
* Yoga FFI Binding
CFFI bindings for Facebook's Yoga Flexbox layout engine. Provides raw access
to the C library — node management, style setters, layout calculation, and
layout getters. The next file (=layout-primitives.org=) wraps these in CLOS.
The Yoga shared library is at =/usr/local/lib/libyoga.so=, built from
=facebook/yoga= with C++17.
** Contract
- =(load-yoga)= :: loads the shared library via CFFI. Signal an error if the
library cannot be found.
- =(yg-node-new)= → YGNodeRef :: wraps =YGNodeNew=. Allocates a new Yoga node.
- =(yg-node-free node)= :: wraps =YGNodeFree=. Frees a node and detaches it
from its owner and children.
- =(yg-node-free-recursive node)= :: wraps =YGNodeFreeRecursive=. Frees the
entire subtree.
- =(yg-node-insert-child node child index)= :: wraps =YGNodeInsertChild=.
Inserts a child at the given index.
- =(yg-node-remove-child node child)= :: wraps =YGNodeRemoveChild=.
- =(yg-node-get-child-count node)= → integer :: wraps =YGNodeGetChildCount=.
- =(yg-node-calculate-layout node width height direction)= ::
wraps =YGNodeCalculateLayout=. Runs the layout algorithm.
- =(yg-node-layout-get-left node)= → float :: wraps =YGNodeLayoutGetLeft=.
Returns the computed X position.
- =(yg-node-layout-get-top node)= → float :: wraps =YGNodeLayoutGetTop=.
Returns the computed Y position.
- =(yg-node-layout-get-width node)= → float :: wraps =YGNodeLayoutGetWidth=.
Returns the computed width.
- =(yg-node-layout-get-height node)= → float :: wraps =YGNodeLayoutGetHeight=.
Returns the computed height.
- =(yg-node-style-set-direction node dir)= :: sets the text direction.
- =(yg-node-style-set-flex-direction node dir)= :: sets the flex direction.
- =(yg-node-style-set-justify-content node justify)= :: sets main-axis
alignment.
- =(yg-node-style-set-align-items node align)= :: sets cross-axis alignment.
- =(yg-node-style-set-align-self node align)= :: sets self alignment.
- =(yg-node-style-set-flex-wrap node wrap)= :: sets wrapping mode.
- =(yg-node-style-set-flex-grow node value)= :: sets the flex grow factor.
- =(yg-node-style-set-flex-shrink node value)= :: sets the flex shrink factor.
- =(yg-node-style-set-position-type node type)= :: sets positioning type.
- =(yg-node-style-set-width node points)= :: sets width in points.
- =(yg-node-style-set-width-percent node pct)= :: sets width as percentage.
- =(yg-node-style-set-width-auto node)= :: sets width to auto.
- =(yg-node-style-set-height node points)= :: sets height in points.
- =(yg-node-style-set-height-percent node pct)= :: sets height as percentage.
- =(yg-node-style-set-height-auto node)= :: sets height to auto.
- =(yg-node-style-set-padding node edge points)= :: sets padding on an edge.
- =(yg-node-style-set-margin node edge points)= :: sets margin on an edge.
- =(yg-node-style-set-border node edge points)= :: sets border on an edge.
- =(yg-node-style-set-gap node gutter length)= :: sets gap between children.
- =(yg-node-style-set-overflow node overflow)= :: sets overflow mode.
- =(yg-node-style-set-display node display)= :: sets display mode.
- =(yg-node-style-set-position-percent node edge pct)= :: sets position as
percentage.
- =(yg-node-style-set-position node edge points)= :: sets position in points.
Enum keywords (mapping C enum → Lisp keyword):
- YGDirection: :inherit (0), :ltr (1), :rtl (2)
- YGFlexDirection: :column (0), :column-reverse (1), :row (2), :row-reverse (3)
- YGJustify: :auto (0), :flex-start (1), :center (2), :flex-end (3),
:space-between (4), :space-around (5), :space-evenly (6), :stretch (7)
- YGAlign: :auto (0), :flex-start (1), :center (2), :flex-end (3),
:stretch (4), :baseline (5), :space-between (6), :space-around (7),
:space-evenly (8)
- YGWrap: :nowrap (0), :wrap (1), :wrap-reverse (2)
- YGPositionType: :static (0), :relative (1), :absolute (2)
- YGOverflow: :visible (0), :hidden (1), :scroll (2)
- YGDisplay: :flex (0), :none (1), :contents (2)
- YGEdge: :left (0), :top (1), :right (2), :bottom (3), :start (4), :end (5),
:horizontal (6), :vertical (7), :all (8)
- YGGutter: :column (0), :row (1), :all (2)
- YGUnit: :undefined (0), :point (1), :percent (2), :auto (3)
* Application-Level Package
#+begin_src lisp
(eval-when (:compile-toplevel :load-toplevel :execute)
(ql:quickload :cffi :silent t))
(defpackage :cl-tui.yoga-ffi
(:use :cl)
(:export
;; library
#:*libyoga*
#:load-yoga
;; constants
#:+yg-direction-inherit+
#:+yg-direction-ltr+
#:+yg-direction-rtl+
#:+yg-flex-direction-column+
#:+yg-flex-direction-column-reverse+
#:+yg-flex-direction-row+
#:+yg-flex-direction-row-reverse+
#:+yg-justify-auto+
#:+yg-justify-flex-start+
#:+yg-justify-center+
#:+yg-justify-flex-end+
#:+yg-justify-space-between+
#:+yg-justify-space-around+
#:+yg-justify-space-evenly+
#:+yg-justify-stretch+
#:+yg-align-auto+
#:+yg-align-flex-start+
#:+yg-align-center+
#:+yg-align-flex-end+
#:+yg-align-stretch+
#:+yg-align-baseline+
#:+yg-align-space-between+
#:+yg-align-space-around+
#:+yg-align-space-evenly+
#:+yg-wrap-nowrap+
#:+yg-wrap-wrap+
#:+yg-wrap-wrap-reverse+
#:+yg-position-type-static+
#:+yg-position-type-relative+
#:+yg-position-type-absolute+
#:+yg-overflow-visible+
#:+yg-overflow-hidden+
#:+yg-overflow-scroll+
#:+yg-display-flex+
#:+yg-display-none+
#:+yg-display-contents+
#:+yg-edge-left+
#:+yg-edge-top+
#:+yg-edge-right+
#:+yg-edge-bottom+
#:+yg-edge-start+
#:+yg-edge-end+
#:+yg-edge-horizontal+
#:+yg-edge-vertical+
#:+yg-edge-all+
#:+yg-gutter-column+
#:+yg-gutter-row+
#:+yg-gutter-all+
#:+yg-unit-undefined+
#:+yg-unit-point+
#:+yg-unit-percent+
#:+yg-unit-auto+
;; types
#:yg-node-ref
#:yg-node-const-ref
;; node management
#:yg-node-new
#:yg-node-free
#:yg-node-free-recursive
#:yg-node-insert-child
#:yg-node-remove-child
#:yg-node-remove-all-children
#:yg-node-get-child-count
#:yg-node-get-child
;; layout
#:yg-node-calculate-layout
#:yg-node-layout-get-left
#:yg-node-layout-get-top
#:yg-node-layout-get-width
#:yg-node-layout-get-height
#:yg-node-layout-get-right
#:yg-node-layout-get-bottom
#:yg-node-is-dirty
#:yg-node-mark-dirty
;; style direction & flex
#:yg-node-style-set-direction
#:yg-node-style-set-flex-direction
#:yg-node-style-set-justify-content
#:yg-node-style-set-align-items
#:yg-node-style-set-align-self
#:yg-node-style-set-align-content
#:yg-node-style-set-flex-wrap
#:yg-node-style-set-position-type
#:yg-node-style-set-flex-grow
#:yg-node-style-set-flex-shrink
#:yg-node-style-set-flex-basis
#:yg-node-style-set-flex-basis-auto
#:yg-node-style-set-flex-basis-percent
#:yg-node-style-set-overflow
#:yg-node-style-set-display
;; style dimensions
#:yg-node-style-set-width
#:yg-node-style-set-width-percent
#:yg-node-style-set-width-auto
#:yg-node-style-set-height
#:yg-node-style-set-height-percent
#:yg-node-style-set-height-auto
#:yg-node-style-set-min-width
#:yg-node-style-set-min-width-percent
#:yg-node-style-set-min-height
#:yg-node-style-set-min-height-percent
#:yg-node-style-set-max-width
#:yg-node-style-set-max-height
#:yg-node-style-set-aspect-ratio
;; style padding/margin/border/gap/position
#:yg-node-style-set-padding
#:yg-node-style-set-padding-percent
#:yg-node-style-set-margin
#:yg-node-style-set-margin-percent
#:yg-node-style-set-margin-auto
#:yg-node-style-set-border
#:yg-node-style-set-gap
#:yg-node-style-set-position
#:yg-node-style-set-position-percent
;; style getters
#:yg-node-style-get-width
#:yg-node-style-get-height
#:yg-node-style-get-flex-direction
#:yg-node-style-get-align-items
#:yg-node-style-get-justify-content))
(in-package :cl-tui.yoga-ffi)
#+end_src
* Foreign Library Loading
#+begin_src lisp
(defparameter *libyoga* nil "Handle for the loaded Yoga shared library.")
(defun load-yoga ()
"Load the Yoga shared library via CFFI."
(setf *libyoga* (cffi:load-foreign-library "/usr/local/lib/libyoga.so"))
(sb-int:set-floating-point-modes :traps '())
*libyoga*)
(load-yoga)
#+end_src
* Enum Constants
#+begin_src lisp
(eval-when (:compile-toplevel :load-toplevel :execute)
;; YGDirection
(defconstant +yg-direction-inherit+ 0)
(defconstant +yg-direction-ltr+ 1)
(defconstant +yg-direction-rtl+ 2)
;; YGFlexDirection
(defconstant +yg-flex-direction-column+ 0)
(defconstant +yg-flex-direction-column-reverse+ 1)
(defconstant +yg-flex-direction-row+ 2)
(defconstant +yg-flex-direction-row-reverse+ 3)
;; YGJustify
(defconstant +yg-justify-auto+ 0)
(defconstant +yg-justify-flex-start+ 1)
(defconstant +yg-justify-center+ 2)
(defconstant +yg-justify-flex-end+ 3)
(defconstant +yg-justify-space-between+ 4)
(defconstant +yg-justify-space-around+ 5)
(defconstant +yg-justify-space-evenly+ 6)
(defconstant +yg-justify-stretch+ 7)
;; YGAlign
(defconstant +yg-align-auto+ 0)
(defconstant +yg-align-flex-start+ 1)
(defconstant +yg-align-center+ 2)
(defconstant +yg-align-flex-end+ 3)
(defconstant +yg-align-stretch+ 4)
(defconstant +yg-align-baseline+ 5)
(defconstant +yg-align-space-between+ 6)
(defconstant +yg-align-space-around+ 7)
(defconstant +yg-align-space-evenly+ 8)
;; YGWrap
(defconstant +yg-wrap-nowrap+ 0)
(defconstant +yg-wrap-wrap+ 1)
(defconstant +yg-wrap-wrap-reverse+ 2)
;; YGPositionType
(defconstant +yg-position-type-static+ 0)
(defconstant +yg-position-type-relative+ 1)
(defconstant +yg-position-type-absolute+ 2)
;; YGOverflow
(defconstant +yg-overflow-visible+ 0)
(defconstant +yg-overflow-hidden+ 1)
(defconstant +yg-overflow-scroll+ 2)
;; YGDisplay
(defconstant +yg-display-flex+ 0)
(defconstant +yg-display-none+ 1)
(defconstant +yg-display-contents+ 2)
;; YGEdge
(defconstant +yg-edge-left+ 0)
(defconstant +yg-edge-top+ 1)
(defconstant +yg-edge-right+ 2)
(defconstant +yg-edge-bottom+ 3)
(defconstant +yg-edge-start+ 4)
(defconstant +yg-edge-end+ 5)
(defconstant +yg-edge-horizontal+ 6)
(defconstant +yg-edge-vertical+ 7)
(defconstant +yg-edge-all+ 8)
;; YGGutter
(defconstant +yg-gutter-column+ 0)
(defconstant +yg-gutter-row+ 1)
(defconstant +yg-gutter-all+ 2)
;; YGUnit
(defconstant +yg-unit-undefined+ 0)
(defconstant +yg-unit-point+ 1)
(defconstant +yg-unit-percent+ 2)
(defconstant +yg-unit-auto+ 3))
#+end_src
* CFFI Foreign Type Definitions
#+begin_src lisp
(cffi:defctype yg-node-ref :pointer)
(cffi:defctype yg-node-const-ref :pointer)
(cffi:defcstruct yg-size
(width :float)
(height :float))
(cffi:defcstruct yg-value
(value :float)
(unit :int))
#+end_src
* Node Management
#+begin_src lisp
(cffi:defcfun ("YGNodeNew" yg-node-new) yg-node-ref)
(cffi:defcfun ("YGNodeFree" yg-node-free) :void
(node yg-node-ref))
(cffi:defcfun ("YGNodeFreeRecursive" yg-node-free-recursive) :void
(node yg-node-ref))
(cffi:defcfun ("YGNodeInsertChild" yg-node-insert-child) :void
(node yg-node-ref)
(child yg-node-ref)
(index :unsigned-int))
(cffi:defcfun ("YGNodeRemoveChild" yg-node-remove-child) :void
(node yg-node-ref)
(child yg-node-ref))
(cffi:defcfun ("YGNodeRemoveAllChildren" yg-node-remove-all-children) :void
(node yg-node-ref))
(cffi:defcfun ("YGNodeGetChildCount" yg-node-get-child-count) :unsigned-int
(node yg-node-const-ref))
(cffi:defcfun ("YGNodeGetChild" yg-node-get-child) yg-node-ref
(node yg-node-ref)
(index :unsigned-int))
#+end_src
* Layout Calculation
#+begin_src lisp
(cffi:defcfun ("YGNodeCalculateLayout" yg-node-calculate-layout) :void
(node yg-node-ref)
(available-width :float)
(available-height :float)
(owner-direction :int))
(cffi:defcfun ("YGNodeLayoutGetLeft" yg-node-layout-get-left) :float
(node yg-node-const-ref))
(cffi:defcfun ("YGNodeLayoutGetTop" yg-node-layout-get-top) :float
(node yg-node-const-ref))
(cffi:defcfun ("YGNodeLayoutGetWidth" yg-node-layout-get-width) :float
(node yg-node-const-ref))
(cffi:defcfun ("YGNodeLayoutGetHeight" yg-node-layout-get-height) :float
(node yg-node-const-ref))
(cffi:defcfun ("YGNodeLayoutGetRight" yg-node-layout-get-right) :float
(node yg-node-const-ref))
(cffi:defcfun ("YGNodeLayoutGetBottom" yg-node-layout-get-bottom) :float
(node yg-node-const-ref))
(cffi:defcfun ("YGNodeIsDirty" yg-node-is-dirty) :boolean
(node yg-node-const-ref))
(cffi:defcfun ("YGNodeMarkDirty" yg-node-mark-dirty) :void
(node yg-node-ref))
#+end_src
* Style Setters — Direction and Flex
#+begin_src lisp
(cffi:defcfun ("YGNodeStyleSetDirection" yg-node-style-set-direction) :void
(node yg-node-ref)
(direction :int))
(cffi:defcfun ("YGNodeStyleSetFlexDirection" yg-node-style-set-flex-direction) :void
(node yg-node-ref)
(flex-direction :int))
(cffi:defcfun ("YGNodeStyleSetJustifyContent" yg-node-style-set-justify-content) :void
(node yg-node-ref)
(justify :int))
(cffi:defcfun ("YGNodeStyleSetAlignItems" yg-node-style-set-align-items) :void
(node yg-node-ref)
(align :int))
(cffi:defcfun ("YGNodeStyleSetAlignSelf" yg-node-style-set-align-self) :void
(node yg-node-ref)
(align :int))
(cffi:defcfun ("YGNodeStyleSetAlignContent" yg-node-style-set-align-content) :void
(node yg-node-ref)
(align :int))
(cffi:defcfun ("YGNodeStyleSetFlexWrap" yg-node-style-set-flex-wrap) :void
(node yg-node-ref)
(wrap :int))
(cffi:defcfun ("YGNodeStyleSetPositionType" yg-node-style-set-position-type) :void
(node yg-node-ref)
(position-type :int))
(cffi:defcfun ("YGNodeStyleSetFlexGrow" yg-node-style-set-flex-grow) :void
(node yg-node-ref)
(flex-grow :float))
(cffi:defcfun ("YGNodeStyleSetFlexShrink" yg-node-style-set-flex-shrink) :void
(node yg-node-ref)
(flex-shrink :float))
(cffi:defcfun ("YGNodeStyleSetFlexBasis" yg-node-style-set-flex-basis) :void
(node yg-node-ref)
(flex-basis :float))
(cffi:defcfun ("YGNodeStyleSetFlexBasisAuto" yg-node-style-set-flex-basis-auto) :void
(node yg-node-ref))
(cffi:defcfun ("YGNodeStyleSetFlexBasisPercent" yg-node-style-set-flex-basis-percent) :void
(node yg-node-ref)
(flex-basis :float))
(cffi:defcfun ("YGNodeStyleSetOverflow" yg-node-style-set-overflow) :void
(node yg-node-ref)
(overflow :int))
(cffi:defcfun ("YGNodeStyleSetDisplay" yg-node-style-set-display) :void
(node yg-node-ref)
(display :int))
#+end_src
* Style Setters — Dimensions
#+begin_src lisp
(cffi:defcfun ("YGNodeStyleSetWidth" yg-node-style-set-width) :void
(node yg-node-ref)
(width :float))
(cffi:defcfun ("YGNodeStyleSetWidthPercent" yg-node-style-set-width-percent) :void
(node yg-node-ref)
(width :float))
(cffi:defcfun ("YGNodeStyleSetWidthAuto" yg-node-style-set-width-auto) :void
(node yg-node-ref))
(cffi:defcfun ("YGNodeStyleSetHeight" yg-node-style-set-height) :void
(node yg-node-ref)
(height :float))
(cffi:defcfun ("YGNodeStyleSetHeightPercent" yg-node-style-set-height-percent) :void
(node yg-node-ref)
(height :float))
(cffi:defcfun ("YGNodeStyleSetHeightAuto" yg-node-style-set-height-auto) :void
(node yg-node-ref))
(cffi:defcfun ("YGNodeStyleSetMinWidth" yg-node-style-set-min-width) :void
(node yg-node-ref)
(min-width :float))
(cffi:defcfun ("YGNodeStyleSetMinWidthPercent" yg-node-style-set-min-width-percent) :void
(node yg-node-ref)
(min-width :float))
(cffi:defcfun ("YGNodeStyleSetMinHeight" yg-node-style-set-min-height) :void
(node yg-node-ref)
(min-height :float))
(cffi:defcfun ("YGNodeStyleSetMinHeightPercent" yg-node-style-set-min-height-percent) :void
(node yg-node-ref)
(min-height :float))
(cffi:defcfun ("YGNodeStyleSetMaxWidth" yg-node-style-set-max-width) :void
(node yg-node-ref)
(max-width :float))
(cffi:defcfun ("YGNodeStyleSetMaxHeight" yg-node-style-set-max-height) :void
(node yg-node-ref)
(max-height :float))
(cffi:defcfun ("YGNodeStyleSetAspectRatio" yg-node-style-set-aspect-ratio) :void
(node yg-node-ref)
(aspect-ratio :float))
#+end_src
* Style Setters — Padding, Margin, Border, Gap, Position
#+begin_src lisp
(cffi:defcfun ("YGNodeStyleSetPadding" yg-node-style-set-padding) :void
(node yg-node-ref)
(edge :int)
(padding :float))
(cffi:defcfun ("YGNodeStyleSetPaddingPercent" yg-node-style-set-padding-percent) :void
(node yg-node-ref)
(edge :int)
(padding :float))
(cffi:defcfun ("YGNodeStyleSetMargin" yg-node-style-set-margin) :void
(node yg-node-ref)
(edge :int)
(margin :float))
(cffi:defcfun ("YGNodeStyleSetMarginPercent" yg-node-style-set-margin-percent) :void
(node yg-node-ref)
(edge :int)
(margin :float))
(cffi:defcfun ("YGNodeStyleSetMarginAuto" yg-node-style-set-margin-auto) :void
(node yg-node-ref)
(edge :int))
(cffi:defcfun ("YGNodeStyleSetBorder" yg-node-style-set-border) :void
(node yg-node-ref)
(edge :int)
(border :float))
(cffi:defcfun ("YGNodeStyleSetGap" yg-node-style-set-gap) :void
(node yg-node-ref)
(gutter :int)
(gap :float))
(cffi:defcfun ("YGNodeStyleSetPosition" yg-node-style-set-position) :void
(node yg-node-ref)
(edge :int)
(position :float))
(cffi:defcfun ("YGNodeStyleSetPositionPercent" yg-node-style-set-position-percent) :void
(node yg-node-ref)
(edge :int)
(position :float))
#+end_src
* Style Getters
#+begin_src lisp
(cffi:defcfun ("YGNodeStyleGetWidth" yg-node-style-get-width) yg-value
(node yg-node-const-ref))
(cffi:defcfun ("YGNodeStyleGetHeight" yg-node-style-get-height) yg-value
(node yg-node-const-ref))
(cffi:defcfun ("YGNodeStyleGetFlexDirection" yg-node-style-get-flex-direction) :int
(node yg-node-const-ref))
(cffi:defcfun ("YGNodeStyleGetAlignItems" yg-node-style-get-align-items) :int
(node yg-node-const-ref))
(cffi:defcfun ("YGNodeStyleGetJustifyContent" yg-node-style-get-justify-content) :int
(node yg-node-const-ref))
#+end_src
* Test Suite
#+begin_src lisp
(eval-when (:compile-toplevel :load-toplevel :execute)
(ql:quickload :fiveam :silent t))
(defpackage :cl-tui.yoga-ffi-tests
(:use :cl :fiveam)
(:import-from :cl-tui.yoga-ffi
#:load-yoga
#:yg-node-new
#:yg-node-free
#:yg-node-free-recursive
#:yg-node-insert-child
#:yg-node-get-child-count
#:yg-node-calculate-layout
#:yg-node-layout-get-left
#:yg-node-layout-get-top
#:yg-node-layout-get-width
#:yg-node-layout-get-height
#:yg-node-style-set-flex-direction
#:yg-node-style-set-justify-content
#:yg-node-style-set-align-items
#:yg-node-style-set-width
#:yg-node-style-set-height
#:yg-node-style-set-padding
#:yg-node-style-set-margin
#:yg-node-style-set-flex-grow
#:yg-node-style-set-position-type
#:yg-node-style-set-position
#:+yg-flex-direction-column+
#:+yg-flex-direction-row+
#:+yg-justify-center+
#:+yg-justify-flex-start+
#:+yg-align-stretch+
#:+yg-align-center+
#:+yg-edge-all+
#:+yg-edge-left+
#:+yg-edge-top+
#:+yg-edge-right+
#:+yg-edge-bottom+
#:+yg-position-type-relative+
#:+yg-position-type-absolute+
;; YGDirection constants (different from YGFlexDirection!)
#:+yg-direction-inherit+
#:+yg-direction-ltr+
#:+yg-direction-rtl+))
(in-package :cl-tui.yoga-ffi-tests)
(fiveam:def-suite yoga-ffi-suite
:description "Yoga FFI binding verification")
(fiveam:in-suite yoga-ffi-suite)
(fiveam:test test-node-create-free
"Contract: yg-node-new returns a non-null pointer; yg-node-free doesn't crash."
(let ((node (yg-node-new)))
(fiveam:is (not (cffi:null-pointer-p node)))
(yg-node-free node)
(fiveam:pass)))
(fiveam:test test-node-child-count
"Contract: yg-node-get-child-count returns 0 for a new node."
(let ((node (yg-node-new)))
(fiveam:is (= 0 (yg-node-get-child-count node)))
(yg-node-free node)))
(fiveam:test test-node-insert-child
"Contract: inserting a child increments the child count."
(let ((parent (yg-node-new))
(child (yg-node-new)))
(yg-node-insert-child parent child 0)
(fiveam:is (= 1 (yg-node-get-child-count parent)))
(yg-node-free-recursive parent)))
(fiveam:test test-layout-basic-column
"Contract: a column with two fixed-height children positions them vertically."
(let* ((root (yg-node-new)))
(yg-node-style-set-width root 100.0)
(yg-node-style-set-height root 200.0)
(yg-node-style-set-flex-direction root +yg-flex-direction-column+)
(let ((child1 (yg-node-new))
(child2 (yg-node-new)))
(yg-node-style-set-width child1 100.0)
(yg-node-style-set-height child1 50.0)
(yg-node-style-set-width child2 100.0)
(yg-node-style-set-height child2 50.0)
(yg-node-insert-child root child1 0)
(yg-node-insert-child root child2 1)
(yg-node-calculate-layout root 100.0 200.0 +yg-direction-ltr+)
(fiveam:is (= 0.0 (yg-node-layout-get-left child1)))
(fiveam:is (= 0.0 (yg-node-layout-get-top child1)))
(fiveam:is (= 50.0 (yg-node-layout-get-top child2)))
(yg-node-free-recursive root))))
(fiveam:test test-layout-basic-row
"Contract: a row with two fixed-width children positions them horizontally."
(let* ((root (yg-node-new)))
(yg-node-style-set-width root 200.0)
(yg-node-style-set-height root 100.0)
(yg-node-style-set-flex-direction root +yg-flex-direction-row+)
(let ((child1 (yg-node-new))
(child2 (yg-node-new)))
(yg-node-style-set-width child1 80.0)
(yg-node-style-set-height child1 50.0)
(yg-node-style-set-width child2 80.0)
(yg-node-style-set-height child2 50.0)
(yg-node-insert-child root child1 0)
(yg-node-insert-child root child2 1)
(yg-node-calculate-layout root 200.0 100.0 +yg-direction-ltr+)
(fiveam:is (= 0.0 (yg-node-layout-get-left child1)))
(fiveam:is (= 0.0 (yg-node-layout-get-top child1)))
(fiveam:is (= 80.0 (yg-node-layout-get-left child2)))
(yg-node-free-recursive root))))
(fiveam:test test-layout-flex-grow
"Contract: flex-grow distributes remaining space proportionally."
(let* ((root (yg-node-new)))
(yg-node-style-set-width root 200.0)
(yg-node-style-set-height root 100.0)
(yg-node-style-set-flex-direction root +yg-flex-direction-row+)
(let ((child1 (yg-node-new))
(child2 (yg-node-new)))
(yg-node-style-set-height child1 100.0)
(yg-node-style-set-flex-grow child1 1.0)
(yg-node-style-set-height child2 100.0)
(yg-node-style-set-flex-grow child2 2.0)
(yg-node-insert-child root child1 0)
(yg-node-insert-child root child2 1)
(yg-node-calculate-layout root 200.0 100.0 +yg-direction-ltr+)
(fiveam:is (< 0.0 (yg-node-layout-get-width child1)))
(fiveam:is (< 0.0 (yg-node-layout-get-width child2)))
(fiveam:is (= 200.0 (+ (yg-node-layout-get-width child1)
(yg-node-layout-get-width child2))))
(yg-node-free-recursive root))))
(fiveam:test test-layout-absolute-position
"Contract: an absolute child positions relative to its parent."
(let* ((root (yg-node-new)))
(yg-node-style-set-width root 300.0)
(yg-node-style-set-height root 300.0)
(let ((child (yg-node-new)))
(yg-node-style-set-width child 50.0)
(yg-node-style-set-height child 50.0)
(yg-node-style-set-position-type child +yg-position-type-absolute+)
(yg-node-style-set-position child +yg-edge-left+ 100.0)
(yg-node-style-set-position child +yg-edge-top+ 50.0)
(yg-node-insert-child root child 0)
(yg-node-calculate-layout root 300.0 300.0 +yg-direction-ltr+)
(fiveam:is (= 100.0 (yg-node-layout-get-left child)))
(fiveam:is (= 50.0 (yg-node-layout-get-top child)))
(fiveam:is (= 50.0 (yg-node-layout-get-width child)))
(fiveam:is (= 50.0 (yg-node-layout-get-height child)))
(yg-node-free-recursive root))))
(fiveam:test test-layout-padding
"Contract: padding reduces the available space for children."
(let* ((root (yg-node-new)))
(yg-node-style-set-width root 200.0)
(yg-node-style-set-height root 100.0)
(yg-node-style-set-padding root +yg-edge-all+ 10.0)
(let ((child (yg-node-new)))
(yg-node-style-set-width child 180.0)
(yg-node-style-set-height child 80.0)
(yg-node-insert-child root child 0)
(yg-node-calculate-layout root 200.0 100.0 +yg-direction-ltr+)
(fiveam:is (= 10.0 (yg-node-layout-get-left child)))
(fiveam:is (= 10.0 (yg-node-layout-get-top child)))
(yg-node-free-recursive root))))
(fiveam:test test-layout-nested
"Contract: nested containers produce correct leaf positions."
(let* ((root (yg-node-new)))
(yg-node-style-set-width root 400.0)
(yg-node-style-set-height root 400.0)
(yg-node-style-set-flex-direction root +yg-flex-direction-column+)
(let ((outer (yg-node-new)))
(yg-node-style-set-width outer 400.0)
(yg-node-style-set-height outer 200.0)
(yg-node-style-set-flex-direction outer +yg-flex-direction-row+)
(let ((inner (yg-node-new)))
(yg-node-style-set-width inner 100.0)
(yg-node-style-set-height inner 100.0)
(yg-node-insert-child outer inner 0)
(yg-node-insert-child root outer 0)
(yg-node-calculate-layout root 400.0 400.0 +yg-direction-ltr+)
(fiveam:is (= 0.0 (yg-node-layout-get-left inner)))
(fiveam:is (= 100.0 (yg-node-layout-get-width inner)))
(fiveam:is (= 100.0 (yg-node-layout-get-height inner)))
(yg-node-free-recursive root)))))
#+end_src