Merge pull request 'v0.3.0: Rendering pipeline — render dispatch, tree walk, dirty propagation' (#4) from feature/v0.3.0-rendering-engine into main
Reviewed-on: http://10.10.10.201:3001/amr/cl-tui/pulls/4
This commit was merged in pull request #4.
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
(asdf:defsystem :cl-tui
|
(asdf:defsystem :cl-tui
|
||||||
:description "Reusable Common Lisp Terminal UI Framework"
|
:description "Reusable Common Lisp Terminal UI Framework"
|
||||||
:author "Amr Gharbeia"
|
:author "Amr Gharbeia"
|
||||||
:version "0.2.0"
|
:version "0.3.0"
|
||||||
:license "TBD"
|
:license "TBD"
|
||||||
:depends-on (:fiveam)
|
:depends-on (:fiveam)
|
||||||
:components
|
:components
|
||||||
@@ -20,7 +20,8 @@
|
|||||||
((:file "package")
|
((:file "package")
|
||||||
(:file "dirty")
|
(:file "dirty")
|
||||||
(:file "box" :depends-on ("package"))
|
(: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))))
|
:in-order-to ((test-op (test-op :cl-tui-tests))))
|
||||||
|
|
||||||
(asdf:defsystem :cl-tui-tests
|
(asdf:defsystem :cl-tui-tests
|
||||||
@@ -36,6 +37,7 @@
|
|||||||
(:module "src/components"
|
(:module "src/components"
|
||||||
:components
|
:components
|
||||||
((:file "box-tests")
|
((:file "box-tests")
|
||||||
(:file "dirty-tests"))))
|
(:file "dirty-tests")
|
||||||
|
(:file "render-tests"))))
|
||||||
:perform (test-op (o c)
|
:perform (test-op (o c)
|
||||||
(uiop:symbol-call :cl-tui-backend-test '#:run-tests)))
|
(uiop:symbol-call :cl-tui-backend-test '#:run-tests)))
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
(in-package :cl-tui.box)
|
(in-package :cl-tui.box)
|
||||||
|
|
||||||
(defclass box ()
|
(defclass box (dirty-mixin)
|
||||||
((layout-node :initform (make-layout-node) :accessor box-layout-node
|
((layout-node :initform (make-layout-node) :accessor box-layout-node
|
||||||
:initarg :layout-node)
|
:initarg :layout-node)
|
||||||
(border-style :initform :single :initarg :border-style
|
(border-style :initform :single :initarg :border-style
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
;; Dirty tracking tests are in box-tests.lisp (same test suite)
|
;; Dirty tracking tests are in box-tests.lisp (same test suite)
|
||||||
(in-package :cl-tui-box-test)
|
(in-package :cl-tui-box-test)
|
||||||
|
(in-suite box-suite)
|
||||||
|
|
||||||
(test dirty-mixin-default-is-dirty
|
(test dirty-mixin-default-is-dirty
|
||||||
"A dirty-mixin starts as dirty"
|
"A dirty-mixin starts as dirty"
|
||||||
|
|||||||
@@ -19,5 +19,10 @@
|
|||||||
;; Utilities (for tests)
|
;; Utilities (for tests)
|
||||||
#:word-wrap #:split-string
|
#:word-wrap #:split-string
|
||||||
;; Dirty tracking
|
;; 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)
|
(in-package :cl-tui.box)
|
||||||
|
|||||||
48
src/components/render-tests.lisp
Normal file
48
src/components/render-tests.lisp
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
(in-package :cl-tui-box-test)
|
||||||
|
(in-suite box-suite)
|
||||||
|
|
||||||
|
(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))))
|
||||||
66
src/components/render.lisp
Normal file
66
src/components/render.lisp
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
(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 ((c t)) 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
|
:underline underline :reverse reverse :dim dim
|
||||||
:fg fg :bg bg))
|
:fg fg :bg bg))
|
||||||
|
|
||||||
(defclass text ()
|
(defclass text (dirty-mixin)
|
||||||
((layout-node :initform (make-layout-node) :accessor text-layout-node
|
((layout-node :initform (make-layout-node) :accessor text-layout-node
|
||||||
:initarg :layout-node)
|
:initarg :layout-node)
|
||||||
(content :initform "" :initarg :content :accessor text-content)
|
(content :initform "" :initarg :content :accessor text-content)
|
||||||
|
|||||||
Reference in New Issue
Block a user