#+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