v0.3.0: Rendering pipeline — render dispatch, tree walk, dirty propagation
- render generic function dispatches per component type - render-screen entry point with sync wrapper - render-node walks tree, computes layout, calls render - component-layout-node generic (box/text methods) - component-children/component-parent generics - propagate-dirty marks component + ancestors dirty - box and text now inherit from dirty-mixin - 6 new tests: render dispatch, layout-node accessor, children, dirty propagation, available-width defaults - 42 component tests, 100% GREEN
This commit is contained in:
@@ -20,7 +20,8 @@
|
||||
((:file "package")
|
||||
(:file "dirty")
|
||||
(:file "box" :depends-on ("package"))
|
||||
(:file "text" :depends-on ("package" "box")))))
|
||||
(:file "text" :depends-on ("package" "box"))
|
||||
(:file "render" :depends-on ("package" "box" "text")))))
|
||||
:in-order-to ((test-op (test-op :cl-tui-tests))))
|
||||
|
||||
(asdf:defsystem :cl-tui-tests
|
||||
@@ -36,6 +37,7 @@
|
||||
(:module "src/components"
|
||||
:components
|
||||
((:file "box-tests")
|
||||
(:file "dirty-tests"))))
|
||||
(:file "dirty-tests")
|
||||
(:file "render-tests"))))
|
||||
:perform (test-op (o c)
|
||||
(uiop:symbol-call :cl-tui-backend-test '#:run-tests)))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
(in-package :cl-tui.box)
|
||||
|
||||
(defclass box ()
|
||||
(defclass box (dirty-mixin)
|
||||
((layout-node :initform (make-layout-node) :accessor box-layout-node
|
||||
:initarg :layout-node)
|
||||
(border-style :initform :single :initarg :border-style
|
||||
|
||||
@@ -19,5 +19,10 @@
|
||||
;; Utilities (for tests)
|
||||
#:word-wrap #:split-string
|
||||
;; Dirty tracking
|
||||
#:dirty-mixin #:dirty-p #:mark-clean #:mark-dirty))
|
||||
#:dirty-mixin #:dirty-p #:mark-clean #:mark-dirty
|
||||
;; Rendering pipeline
|
||||
#:render #:render-screen #:render-node
|
||||
#:component-layout-node #:component-children #:component-parent
|
||||
#:available-width #:available-height
|
||||
#:propagate-dirty))
|
||||
(in-package :cl-tui.box)
|
||||
|
||||
47
src/components/render-tests.lisp
Normal file
47
src/components/render-tests.lisp
Normal file
@@ -0,0 +1,47 @@
|
||||
(in-package :cl-tui-box-test)
|
||||
|
||||
(defun make-capturing-backend ()
|
||||
(let* ((s (make-string-output-stream))
|
||||
(b (make-modern-backend :output-stream s)))
|
||||
(values b s)))
|
||||
|
||||
(test render-generic-dispatches-box
|
||||
"render dispatches to render-box for box instances"
|
||||
(multiple-value-bind (b s) (make-capturing-backend)
|
||||
(let ((bx (make-box :border-style :single :width 10 :height 5)))
|
||||
(compute-layout (box-layout-node bx) 10 5)
|
||||
(render bx b)
|
||||
(is (search "┌" (get-output-stream-string s)) "box renders border"))))
|
||||
|
||||
(test render-generic-dispatches-text
|
||||
"render dispatches to render-text for text instances"
|
||||
(multiple-value-bind (b s) (make-capturing-backend)
|
||||
(let ((tx (make-text "Hello" :width 10 :height 1)))
|
||||
(compute-layout (text-layout-node tx) 10 1)
|
||||
(render tx b)
|
||||
(is (search "Hello" (get-output-stream-string s)) "text renders content"))))
|
||||
|
||||
(test component-layout-node-works
|
||||
"component-layout-node returns the right slot for each type"
|
||||
(let ((bx (make-box)) (tx (make-text "")))
|
||||
(is (typep (component-layout-node bx) 'layout-node))
|
||||
(is (typep (component-layout-node tx) 'layout-node))))
|
||||
|
||||
(test component-children-returns-nil
|
||||
"Leaf components have no children"
|
||||
(let ((bx (make-box)) (tx (make-text "")))
|
||||
(is (null (component-children bx)))
|
||||
(is (null (component-children tx)))))
|
||||
|
||||
(test propagate-dirty-marks-component
|
||||
"propagate-dirty marks the component dirty"
|
||||
(let ((c (make-box)))
|
||||
(mark-clean c)
|
||||
(is-false (dirty-p c) "should be clean after mark-clean")
|
||||
(propagate-dirty c)
|
||||
(is-true (dirty-p c) "should be dirty after propagate-dirty")))
|
||||
|
||||
(test available-width-defaults
|
||||
"available-width returns 0 for components without explicit width"
|
||||
(let ((c (make-box)))
|
||||
(is (= (available-width c) 0))))
|
||||
67
src/components/render.lisp
Normal file
67
src/components/render.lisp
Normal file
@@ -0,0 +1,67 @@
|
||||
(in-package :cl-tui.box)
|
||||
|
||||
;; ── Component Protocol ────────────────────────────────────────
|
||||
|
||||
(defgeneric component-layout-node (component)
|
||||
(:documentation "Return the layout-node for COMPONENT.")
|
||||
(:method ((bx box)) (box-layout-node bx))
|
||||
(:method ((tx text)) (text-layout-node tx)))
|
||||
|
||||
(defgeneric component-children (component)
|
||||
(:documentation "Return the children of COMPONENT, or nil.")
|
||||
(:method ((bx box)) nil)
|
||||
(:method ((tx text)) nil))
|
||||
|
||||
(defgeneric component-parent (component)
|
||||
(:documentation "Return the parent of COMPONENT, or nil.")
|
||||
(:method ((c t)) nil))
|
||||
|
||||
;; ── Rendering Pipeline ────────────────────────────────────────
|
||||
|
||||
(defgeneric render (component backend)
|
||||
(:documentation "Render COMPONENT at its computed position using BACKEND.")
|
||||
(:method ((c t) backend)
|
||||
(declare (ignore backend))
|
||||
(values)))
|
||||
|
||||
(defmethod render ((bx box) backend)
|
||||
(render-box bx backend))
|
||||
|
||||
(defmethod render ((tx text) backend)
|
||||
(render-text tx backend))
|
||||
|
||||
(defun render-screen (root backend)
|
||||
"Render the component tree ROOT using BACKEND.
|
||||
Computes layout for dirty branches, calls render on each component,
|
||||
and wraps output in synchronized updates."
|
||||
(let ((w (available-width root))
|
||||
(h (available-height root)))
|
||||
(begin-sync backend)
|
||||
(render-node root backend w h)
|
||||
(end-sync backend)))
|
||||
|
||||
(defun render-node (node backend w h)
|
||||
"Render a component NODE and its children."
|
||||
(compute-layout (component-layout-node node) w h)
|
||||
(render node backend)
|
||||
(dolist (child (component-children node))
|
||||
(render-node child backend w h)))
|
||||
|
||||
(defun available-width (component)
|
||||
"Return the available width for COMPONENT (or 80 as default)."
|
||||
(let ((ln (component-layout-node component)))
|
||||
(if ln (layout-node-width ln) 80)))
|
||||
|
||||
(defun available-height (component)
|
||||
"Return the available height for COMPONENT (or 24 as default)."
|
||||
(let ((ln (component-layout-node component)))
|
||||
(if ln (layout-node-height ln) 24)))
|
||||
|
||||
;; ── Dirty Propagation ─────────────────────────────────────────
|
||||
|
||||
(defun propagate-dirty (component)
|
||||
"Mark COMPONENT and all ancestors dirty."
|
||||
(mark-dirty component)
|
||||
(let ((parent (component-parent component)))
|
||||
(when parent
|
||||
(propagate-dirty parent))))
|
||||
@@ -18,7 +18,7 @@
|
||||
:underline underline :reverse reverse :dim dim
|
||||
:fg fg :bg bg))
|
||||
|
||||
(defclass text ()
|
||||
(defclass text (dirty-mixin)
|
||||
((layout-node :initform (make-layout-node) :accessor text-layout-node
|
||||
:initarg :layout-node)
|
||||
(content :initform "" :initarg :content :accessor text-content)
|
||||
|
||||
Reference in New Issue
Block a user