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