Follows the literate programming workflow: Overview → Contract → Tests → Implement → Tangle → Test (GREEN) render.org covers render.lisp + render-tests.lisp (component protocol, render dispatch, dirty propagation) theme.org covers theme.lisp + theme-tests.lisp (theme class, presets, color resolution) package.org covers package.lisp (cl-tty.box defpackage)
273 lines
9.5 KiB
Org Mode
273 lines
9.5 KiB
Org Mode
#+TITLE: Render Dispatch and Pipeline
|
|
#+STARTUP: content
|
|
#+FILETAGS: :cl-tty:components:
|
|
|
|
* Overview
|
|
|
|
The render module provides the generic function dispatch that connects
|
|
the component tree to the backend. Every component type defines its own
|
|
~render~ method; this module defines the common protocol and the
|
|
top-level orchestration functions.
|
|
|
|
Three responsibilities live here:
|
|
|
|
1. **Component protocol** — generic functions for navigating the
|
|
component tree (~component-children~, ~component-parent~,
|
|
~component-layout-node~)
|
|
|
|
2. **Render pipeline** — ~render-screen~ ties layout computation to
|
|
rendering, using the backend's actual terminal dimensions rather
|
|
than hardcoded values. ~render-node~ walks the tree.
|
|
|
|
3. **Dirty propagation** — ~propagate-dirty~ marks a component and all
|
|
its ancestors for re-render. This is what makes the incremental
|
|
pipeline efficient: only changed branches get re-processed.
|
|
|
|
* Contract
|
|
|
|
** ~component-layout-node component~ → layout-node or nil
|
|
|
|
Return the layout node associated with ~component~. Specialized per
|
|
component type (~box~, ~text~).
|
|
|
|
** ~component-children component~ → list or nil
|
|
|
|
Return child components. Default method returns ~nil~ (leaf components).
|
|
|
|
** ~component-parent component~ → component or nil
|
|
|
|
Return the parent component. Default method returns ~nil~.
|
|
|
|
** ~render component backend~
|
|
|
|
Render ~component~ at its computed position using ~backend~. Default
|
|
method is a no-op. Specialized per component type.
|
|
|
|
** ~render-screen root backend~
|
|
|
|
Full render pipeline: query backend size, compute layout, render tree,
|
|
wrapped in DECICM sync (~begin-sync~/~end-sync~).
|
|
|
|
** ~render-node node backend~
|
|
|
|
Render ~node~ and all descendants recursively. ~render-screen~ calls
|
|
this once layout is computed.
|
|
|
|
** ~available-width / available-height component~ → integer
|
|
|
|
Return the computed width/height from the component's layout node, or
|
|
80/24 as fallback.
|
|
|
|
** ~propagate-dirty component~
|
|
|
|
Mark ~component~ and every ancestor dirty. Walks up via
|
|
~component-parent~.
|
|
|
|
* Tests
|
|
|
|
#+BEGIN_SRC lisp :tangle ../src/components/render-tests.lisp
|
|
(in-package :cl-tty-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))))
|
|
#+END_SRC
|
|
|
|
* Implementation
|
|
|
|
** Component protocol
|
|
|
|
These three generic functions form the tree navigation API. They're
|
|
separated from ~render~ because layout and dirty propagation also
|
|
need to traverse the tree.
|
|
|
|
#+BEGIN_SRC lisp :tangle ../src/components/render.lisp
|
|
(in-package :cl-tty.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)))
|
|
#+END_SRC
|
|
|
|
Each component type defines its own ~component-layout-node~ method
|
|
that returns its internal layout node. The default method (on ~t~)
|
|
would return ~nil~, but since every component in cl-tty has a layout
|
|
node, we don't provide one — new component types must add their own
|
|
method.
|
|
|
|
#+BEGIN_SRC lisp :tangle ../src/components/render.lisp
|
|
(defgeneric component-children (component)
|
|
(:documentation "Return the children of COMPONENT, or nil.")
|
|
(:method ((c t)) nil))
|
|
#+END_SRC
|
|
|
|
Leaf components (~box~, ~text~) have no children. Container components
|
|
(~scrollbox~, ~tabbar~) override this to return their child list.
|
|
|
|
#+BEGIN_SRC lisp :tangle ../src/components/render.lisp
|
|
(defgeneric component-parent (component)
|
|
(:documentation "Return the parent of COMPONENT, or nil.")
|
|
(:method ((c t)) nil))
|
|
#+END_SRC
|
|
|
|
Parent links are set by the container when adding children. They're
|
|
used by ~propagate-dirty~ to walk up the tree.
|
|
|
|
** Render dispatch
|
|
|
|
#+BEGIN_SRC lisp :tangle ../src/components/render.lisp
|
|
;; ── Rendering Pipeline ────────────────────────────────────────
|
|
|
|
(defgeneric render (component backend)
|
|
(:documentation "Render COMPONENT at its computed position using BACKEND.")
|
|
(:method ((c t) backend)
|
|
(declare (ignore backend))
|
|
(values)))
|
|
#+END_SRC
|
|
|
|
The ~render~ generic is the central dispatch point. Every component
|
|
type that can be drawn defines a method on ~render~. The default
|
|
method is a no-op so that non-renderable objects (or components still
|
|
under development) don't cause errors.
|
|
|
|
#+BEGIN_SRC lisp :tangle ../src/components/render.lisp
|
|
(defmethod render ((bx box) backend)
|
|
(render-box bx backend))
|
|
|
|
(defmethod render ((tx text) backend)
|
|
(render-text tx backend))
|
|
#+END_SRC
|
|
|
|
Box and text are the two built-in renderable types. Their ~render~
|
|
methods delegate to the specific rendering functions defined in
|
|
~box.lisp~ and ~text.lisp~.
|
|
|
|
** Screen-level orchestration
|
|
|
|
#+BEGIN_SRC lisp :tangle ../src/components/render.lisp
|
|
(defun render-screen (root backend)
|
|
"Render the component tree ROOT using BACKEND.
|
|
Computes layout at the root level, then traverses children
|
|
rendering each at their pre-computed positions. Uses the actual
|
|
terminal dimensions from BACKEND rather than hardcoded defaults."
|
|
(multiple-value-bind (w h) (backend-size backend)
|
|
(begin-sync backend)
|
|
(compute-layout (component-layout-node root) w h)
|
|
(render-node root backend)
|
|
(end-sync backend)))
|
|
#+END_SRC
|
|
|
|
~render-screen~ is the entry point for rendering a full frame. It
|
|
queries the terminal size at render time (not at startup), so the
|
|
layout adapts to window resizes automatically.
|
|
|
|
The DECICM sync pair (~begin-sync~/~end-sync~) wraps the entire
|
|
frame in a synchronized update: the terminal buffers all escape
|
|
sequences and flushes them atomically. This prevents partial-frame
|
|
flicker.
|
|
|
|
#+BEGIN_SRC lisp :tangle ../src/components/render.lisp
|
|
(defun render-node (node backend)
|
|
"Render a component NODE and its children.
|
|
Layout is computed once at the root by render-screen, so children
|
|
just render at their pre-computed positions."
|
|
(render node backend)
|
|
(dolist (child (component-children node))
|
|
(render-node child backend)))
|
|
#+END_SRC
|
|
|
|
Tree walk: render this node, then recurse into children. The layout
|
|
was already computed by ~render-screen~, so each node's position and
|
|
size are available from its ~layout-node~.
|
|
|
|
** Utility accessors
|
|
|
|
#+BEGIN_SRC lisp :tangle ../src/components/render.lisp
|
|
(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)))
|
|
#+END_SRC
|
|
|
|
These accessors provide a clean API for components that need to know
|
|
their allocated space. They return the computed dimensions from the
|
|
layout node, which was set by ~compute-layout~ during ~render-screen~.
|
|
|
|
The fallback values (80x24) match the terminal default when no layout
|
|
node exists — typically during initialization or testing without a
|
|
backenπd.
|
|
|
|
** Dirty propagation
|
|
|
|
#+BEGIN_SRC lisp :tangle ../src/components/render.lisp
|
|
;; ── 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))))
|
|
#+END_SRC
|
|
|
|
Recursive walk up the parent chain. When a text input receives a
|
|
keystroke, it marks itself dirty, then its parent scrollbox, then the
|
|
containing box, then the root — triggering recomputation and
|
|
re-rendering of everything that might have changed.
|
|
|
|
This is the key to incremental rendering: only dirty branches are
|
|
re-processed. The ~render~ methods check ~dirty-p~ early and return
|
|
immediately for clean components (handled in each component's render,
|
|
not here).
|