Compare commits
3 Commits
main
...
feature/v0
| Author | SHA1 | Date | |
|---|---|---|---|
| 56682d0cc2 | |||
| 7191606227 | |||
| f135b56a1a |
11
cl-tui.asd
Normal file
11
cl-tui.asd
Normal file
@@ -0,0 +1,11 @@
|
||||
(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")))
|
||||
@@ -23,22 +23,28 @@ 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.
|
||||
|
||||
*** TODO Yoga FFI binding
|
||||
*** DONE 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
|
||||
|
||||
*** TODO Layout primitives
|
||||
*** DONE 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
|
||||
@@ -56,11 +62,14 @@ 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
|
||||
|
||||
*** TODO Layout composable API
|
||||
*** DONE 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:
|
||||
|
||||
|
||||
194
lisp/layout-composable.lisp
Normal file
194
lisp/layout-composable.lisp
Normal file
@@ -0,0 +1,194 @@
|
||||
(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))))))
|
||||
511
lisp/layout-primitives.lisp
Normal file
511
lisp/layout-primitives.lisp
Normal file
@@ -0,0 +1,511 @@
|
||||
(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)))))
|
||||
631
lisp/yoga-ffi.lisp
Normal file
631
lisp/yoga-ffi.lisp
Normal file
@@ -0,0 +1,631 @@
|
||||
(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)))))
|
||||
248
org/layout-composable.org
Normal file
248
org/layout-composable.org
Normal file
@@ -0,0 +1,248 @@
|
||||
#+TITLE: Layout Composable API
|
||||
#+STARTUP: content
|
||||
#+FILETAGS: :cl-tui:layout-composable:v010:
|
||||
|
||||
* Layout Composable API
|
||||
|
||||
Convenience macros for building layout trees declaratively. Each macro
|
||||
creates a =layout-node=, applies style properties, and inserts child nodes.
|
||||
|
||||
Dependencies: =layout-primitives=.
|
||||
|
||||
** Contract
|
||||
|
||||
- =(vbox &key width height flex-grow flex-shrink flex-basis align justify gap
|
||||
padding margin children ...)= → layout-node ::
|
||||
Creates a column-direction container. All =children= are
|
||||
layout-node instances, or plain non-node values are skipped.
|
||||
- =(hbox &key width height flex-grow flex-shrink flex-basis align justify gap
|
||||
padding margin children ...)= → layout-node ::
|
||||
Creates a row-direction container.
|
||||
- =(overlay base child &key top right bottom left)= → layout-node ::
|
||||
Creates a container with a relative base and an absolute-positioned child
|
||||
overlaid on top.
|
||||
- =(spacer &key grow)= → layout-node ::
|
||||
Creates an empty flexible spacer that fills available space.
|
||||
|
||||
* Package
|
||||
|
||||
#+begin_src lisp
|
||||
(eval-when (:compile-toplevel :load-toplevel :execute)
|
||||
(ql:quickload :cffi :silent t)
|
||||
(ql:quickload :trivial-garbage :silent t))
|
||||
|
||||
(defpackage :cl-tui.layout-composable
|
||||
(:use :cl :cl-tui.layout-primitives)
|
||||
(:export
|
||||
#:vbox
|
||||
#:hbox
|
||||
#:overlay
|
||||
#:spacer))
|
||||
|
||||
(in-package :cl-tui.layout-composable)
|
||||
#+end_src
|
||||
|
||||
* Internal Helpers
|
||||
|
||||
#+begin_src lisp
|
||||
(defun apply-common-props (node &key width height flex-grow flex-shrink flex-basis
|
||||
align justify gap padding margin
|
||||
&allow-other-keys)
|
||||
"Apply the shared style properties to a layout-node."
|
||||
(when (or width height)
|
||||
(layout-node-set-dimension node (or width 0) (or height 0)))
|
||||
(when (or flex-grow flex-shrink flex-basis)
|
||||
(layout-node-set-flex node :grow flex-grow :shrink flex-shrink :basis flex-basis))
|
||||
(when align
|
||||
(apply #'layout-node-set-align node align))
|
||||
(when justify
|
||||
(layout-node-set-justify node justify))
|
||||
(when gap
|
||||
(apply #'layout-node-set-gap node gap))
|
||||
(when padding
|
||||
(apply #'layout-node-set-padding node padding))
|
||||
(when margin
|
||||
(apply #'layout-node-set-margin node margin)))
|
||||
|
||||
(defun add-children (parent children)
|
||||
"Add each child in CHILDREN to PARENT. Non-node values are skipped."
|
||||
(dolist (child children)
|
||||
(when (typep child 'layout-node)
|
||||
(layout-node-add-child parent child))))
|
||||
|
||||
(defun make-props-list (args)
|
||||
"Extract all properties except :children from ARGS plist."
|
||||
(loop for (k v) on args by #'cddr
|
||||
unless (eq k :children)
|
||||
append (list k v)))
|
||||
#+end_src
|
||||
|
||||
* vbox — Vertical Box
|
||||
|
||||
#+begin_src lisp
|
||||
(defmacro vbox (&rest args &key children &allow-other-keys)
|
||||
"Create a column-direction container with CHILDREN stacked vertically."
|
||||
(declare (ignore children))
|
||||
(let* ((node (gensym "VBOX"))
|
||||
(props (make-props-list args)))
|
||||
`(let ((,node (make-layout-node)))
|
||||
(layout-node-set-direction ,node :column)
|
||||
(apply #'cl-tui.layout-composable::apply-common-props ,node ',props)
|
||||
(cl-tui.layout-composable::add-children ,node (list ,@children))
|
||||
,node)))
|
||||
#+end_src
|
||||
|
||||
* hbox — Horizontal Box
|
||||
|
||||
#+begin_src lisp
|
||||
(defmacro hbox (&rest args &key children &allow-other-keys)
|
||||
"Create a row-direction container with CHILDREN laid out horizontally."
|
||||
(declare (ignore children))
|
||||
(let* ((node (gensym "HBOX"))
|
||||
(props (make-props-list args)))
|
||||
`(let ((,node (make-layout-node)))
|
||||
(layout-node-set-direction ,node :row)
|
||||
(apply #'cl-tui.layout-composable::apply-common-props ,node ',props)
|
||||
(cl-tui.layout-composable::add-children ,node (list ,@children))
|
||||
,node)))
|
||||
#+end_src
|
||||
|
||||
* overlay — Absolute Overlay
|
||||
|
||||
#+begin_src lisp
|
||||
(defmacro overlay (base child &key top right bottom left)
|
||||
"Create a container with BASE as the relative foundation and CHILD
|
||||
positioned absolutely on top."
|
||||
(let ((node (gensym "OVERLAY")))
|
||||
`(let ((,node (make-layout-node)))
|
||||
(layout-node-set-position ,node :relative)
|
||||
(layout-node-add-child ,node ,base)
|
||||
(layout-node-set-position ,child :absolute
|
||||
,@(when top `(:top ,top))
|
||||
,@(when right `(:right ,right))
|
||||
,@(when bottom `(:bottom ,bottom))
|
||||
,@(when left `(:left ,left)))
|
||||
(layout-node-add-child ,node ,child)
|
||||
,node)))
|
||||
#+end_src
|
||||
|
||||
* spacer — Flex Spacer
|
||||
|
||||
#+begin_src lisp
|
||||
(defun spacer (&key (grow 0))
|
||||
"Create an empty spacer node that fills available space via flex-grow."
|
||||
(let ((node (make-layout-node)))
|
||||
(when (> grow 0)
|
||||
(layout-node-set-flex node :grow grow))
|
||||
node))
|
||||
#+end_src
|
||||
|
||||
* Test Suite
|
||||
|
||||
#+begin_src lisp
|
||||
(eval-when (:compile-toplevel :load-toplevel :execute)
|
||||
(ql:quickload :fiveam :silent t))
|
||||
|
||||
(defpackage :cl-tui.layout-composable-tests
|
||||
(:use :cl :fiveam)
|
||||
(:import-from :cl-tui.layout-composable
|
||||
#:vbox #:hbox #:overlay #:spacer)
|
||||
(:import-from :cl-tui.layout-primitives
|
||||
#:layout-node #:layout-node-ptr #:layout-calculate
|
||||
#:make-layout-node #:layout-node-set-dimension)
|
||||
(:import-from :cl-tui.yoga-ffi
|
||||
#:yg-node-layout-get-left
|
||||
#:yg-node-layout-get-top
|
||||
#:yg-node-layout-get-width
|
||||
#:yg-node-layout-get-height
|
||||
#:yg-node-get-child))
|
||||
|
||||
(in-package :cl-tui.layout-composable-tests)
|
||||
|
||||
(defun node-x (n) (yg-node-layout-get-left (layout-node-ptr n)))
|
||||
(defun node-y (n) (yg-node-layout-get-top (layout-node-ptr n)))
|
||||
(defun node-w (n) (yg-node-layout-get-width (layout-node-ptr n)))
|
||||
(defun node-h (n) (yg-node-layout-get-height (layout-node-ptr n)))
|
||||
(defun child-x (p) (yg-node-layout-get-left p))
|
||||
(defun child-y (p) (yg-node-layout-get-top p))
|
||||
(defun child-w (p) (yg-node-layout-get-width p))
|
||||
(defun child-h (p) (yg-node-layout-get-height p))
|
||||
|
||||
(defun nth-child (node n)
|
||||
(yg-node-get-child (layout-node-ptr node) n))
|
||||
|
||||
(defun layout-dummy (w h)
|
||||
(let ((n (make-layout-node)))
|
||||
(layout-node-set-dimension n w h)
|
||||
n))
|
||||
|
||||
(fiveam:def-suite layout-composable-suite
|
||||
:description "Composable API macro verification")
|
||||
(fiveam:in-suite layout-composable-suite)
|
||||
|
||||
(fiveam:test test-vbox-stacks-children
|
||||
"Contract: vbox stacks children vertically."
|
||||
(let* ((root (vbox :width 100 :height 200
|
||||
:children ((layout-dummy 100 50)
|
||||
(layout-dummy 100 50)))))
|
||||
(layout-calculate root 100 200)
|
||||
(let ((c1 (nth-child root 0))
|
||||
(c2 (nth-child root 1)))
|
||||
(fiveam:is (= 0.0 (child-y c1)))
|
||||
(fiveam:is (= 50.0 (child-y c2))))))
|
||||
|
||||
(fiveam:test test-hbox-lays-out-horizontally
|
||||
"Contract: hbox places children horizontally."
|
||||
(let* ((root (hbox :width 200 :height 100
|
||||
:children ((layout-dummy 80 50)
|
||||
(layout-dummy 80 50)))))
|
||||
(layout-calculate root 200 100)
|
||||
(let ((c1 (nth-child root 0))
|
||||
(c2 (nth-child root 1)))
|
||||
(fiveam:is (= 0.0 (child-x c1)))
|
||||
(fiveam:is (= 80.0 (child-x c2))))))
|
||||
|
||||
(fiveam:test test-spacer-flex-grow
|
||||
"Contract: spacer with flex-grow expands to fill space."
|
||||
(let* ((root (hbox :width 200 :height 100
|
||||
:children ((layout-dummy 50 50)
|
||||
(spacer :grow 1)
|
||||
(layout-dummy 50 50)))))
|
||||
(layout-calculate root 200 100)
|
||||
(let ((c1 (nth-child root 0))
|
||||
(c2 (nth-child root 1))
|
||||
(c3 (nth-child root 2)))
|
||||
(fiveam:is (= 0.0 (child-x c1)))
|
||||
(fiveam:is (= 50.0 (child-w c1)))
|
||||
(fiveam:is (= 100.0 (child-w c2))))))
|
||||
|
||||
(fiveam:test test-overlay-absolute-position
|
||||
"Contract: overlay positions an absolute child over a relative base."
|
||||
(let* ((base (layout-dummy 100 100))
|
||||
(child (layout-dummy 30 30))
|
||||
(root (overlay base child :top 10 :left 20)))
|
||||
(layout-calculate root 200 200)
|
||||
(fiveam:is (= 20.0 (node-x child)))
|
||||
(fiveam:is (= 10.0 (node-y child)))))
|
||||
|
||||
(fiveam:test test-vbox-align-justify
|
||||
"Contract: vbox accepts align and justify keywords."
|
||||
(let* ((root (vbox :width 200 :height 100
|
||||
:align (:items :center)
|
||||
:justify :center
|
||||
:children ((layout-dummy 50 50)))))
|
||||
(layout-calculate root 200 100)
|
||||
(let ((c (nth-child root 0)))
|
||||
(fiveam:is (= 25.0 (child-y c)))
|
||||
(fiveam:is (= 75.0 (child-x c))))))
|
||||
|
||||
(fiveam:test test-vbox-padding
|
||||
"Contract: vbox padding offsets children."
|
||||
(let* ((root (vbox :width 200 :height 100
|
||||
:padding (:all 10)
|
||||
:children ((layout-dummy 180 80)))))
|
||||
(layout-calculate root 200 100)
|
||||
(let ((c (nth-child root 0)))
|
||||
(fiveam:is (= 10.0 (child-x c)))
|
||||
(fiveam:is (= 10.0 (child-y c))))))
|
||||
#+end_src
|
||||
608
org/layout-primitives.org
Normal file
608
org/layout-primitives.org
Normal file
@@ -0,0 +1,608 @@
|
||||
#+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
|
||||
755
org/yoga-ffi.org
Normal file
755
org/yoga-ffi.org
Normal file
@@ -0,0 +1,755 @@
|
||||
#+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
|
||||
Reference in New Issue
Block a user