#+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 ** Test helper: make-capturing-backend Before any render test can run, we need a backend that captures output to a string stream instead of writing to the real terminal. This helper creates a ~modern-backend~ with a ~string-output-stream~ and returns both, so tests can inspect what was rendered. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/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))) #+END_SRC ** Test: render dispatches to box method Verifies that calling ~render~ on a ~box~ instance invokes the box rendering path, which draws border characters (e.g. ┌). This confirms generic dispatch works for the box type and that the border rendering pipeline is intact. A regression here would mean ~render-box~ is not being called or produces no output. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/components/render-tests.lisp (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")))) #+END_SRC ** Test: render dispatches to text method Verifies that calling ~render~ on a ~text~ instance invokes the text rendering path, which outputs the string content. This confirms generic dispatch works for the text type and that text content is correctly emitted to the backend. A regression would mean ~render-text~ is not being called. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/components/render-tests.lisp (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")))) #+END_SRC ** Test: component-layout-node returns layout-node The ~component-layout-node~ generic is the bridge between the component layer and the layout layer. Every renderable component must have an associated layout node. This test confirms that both ~box~ and ~text~ return a ~layout-node~ instance from their ~component-layout-node~ method. A failure here means a component type is missing its method or the slot accessor is wrong. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/components/render-tests.lisp (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)))) #+END_SRC ** Test: component-children returns nil for leaves Leaf components (~box~, ~text~) have no children by definition. The default method on ~t~ returns ~nil~. This test ensures that neither box nor text accidentally inherits or defines a method that returns non-nil, which would break the tree-walk in ~render-node~ by causing infinite recursion or rendering phantom children. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/components/render-tests.lisp (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))))) #+END_SRC ** Test: propagate-dirty marks component dirty ~propagate-dirty~ is the entry point for the incremental rendering pipeline. When a component changes (e.g. a keystroke in a text input), it calls ~propagate-dirty~ to ensure the frame is re-rendered. This test verifies that calling ~propagate-dirty~ on a clean component sets it dirty. Without this, components that mutate would never trigger a re-render and the display would become stale. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/components/render-tests.lisp (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"))) #+END_SRC ** Test: available-width defaults ~available-width~ reads the computed width from the component's layout node. When a component hasn't been laid out (no explicit width set), the layout node's width defaults to 0. This test verifies that ~available-width~ returns 0 for a freshly created box without layout computation. This matters because container components use ~available-width~ to position children — getting a sensible default prevents division-by-zero or garbled layouts during initialization. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/components/render-tests.lisp (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. *** component-layout-node The ~component-layout-node~ generic returns the ~layout-node~ instance for a given component. Every component that participates in layout and rendering must have a layout node — it stores the computed position and size after layout passes. The generic is defined with two specific methods for the built-in component types. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/components/render.lisp (in-package :cl-tty.box) ;; ── Component Protocol ──────────────────────────────────────── (defgeneric component-layout-node (component) (:documentation "Return the layout-node for COMPONENT.")) #+END_SRC Each component type returns its internal layout node slot. This method specializes on ~box~ and returns the ~box-layout-node~ slot value. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/components/render.lisp (defmethod component-layout-node ((bx box)) (box-layout-node bx)) #+END_SRC The ~text~ component stores its layout node in the ~text-layout-node~ slot. Both methods return the same type (~layout-node~), so the layout engine can operate uniformly regardless of component type. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/components/render.lisp (defmethod component-layout-node ((tx text)) (text-layout-node tx)) #+END_SRC *** component-children Leaf components (~box~, ~text~) have no children. Container components (~scrollbox~, ~tabbar~) override this to return their child list. The default method on ~t~ returns ~nil~, so new component types are automatically treated as leaves unless they explicitly override. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/components/render.lisp (defgeneric component-children (component) (:documentation "Return the children of COMPONENT, or nil.") (:method ((c t)) nil)) #+END_SRC *** component-parent Parent links are set by the container when adding children. They're used by ~propagate-dirty~ to walk up the tree. The default method on ~t~ returns ~nil~, which acts as the termination condition for the recursive dirty walk — when ~component-parent~ returns ~nil~, we've reached the root. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/components/render.lisp (defgeneric component-parent (component) (:documentation "Return the parent of COMPONENT, or nil.") (:method ((c t)) nil)) #+END_SRC ** Render dispatch *** render generic The ~render~ generic is the central dispatch point for the rendering pipeline. Every component type that can be drawn defines a method on ~render~. The default method on ~t~ is a no-op so that non-renderable objects (or components still under development) don't cause errors when the tree walk reaches them. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/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 *** render method for box Boxes are rendered with border characters. The ~render~ method delegates to the ~render-box~ function defined in ~box.lisp~, which handles the actual drawing of border lines and corners. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/components/render.lisp (defmethod render ((bx box) backend) (render-box bx backend)) #+END_SRC *** render method for text Text components render their content string at the computed position. The ~render~ method delegates to ~render-text~ from ~text.lisp~, which writes the string with appropriate escape sequences for positioning. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/components/render.lisp (defmethod render ((tx text) backend) (render-text tx backend)) #+END_SRC ** Screen-level orchestration *** render-screen ~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, preventing partial-frame flicker. The pipeline is: (1) query backend pixel/dimension size, (2) begin sync, (3) compute layout at the root, (4) walk the tree rendering each node, (5) end sync. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/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-node 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~. The recursion is depth-first: parents are drawn before children, which matters for z-ordering (the parent's background is drawn first, children overlay on top). #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/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 ** Utility accessors *** available-width Returns the computed width from the component's layout node. The layout node's width is set by ~compute-layout~ during ~render-screen~, so this reflects the actual allocated space — not the requested width. The fallback of 80 matches the default terminal width when no layout node exists (during initialization or testing without a backend). #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/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))) #+END_SRC *** available-height Returns the computed height from the component's layout node. Like ~available-width~, this reflects post-layout allocated space. The fallback of 24 matches the default terminal height. These accessors provide a clean API for components that need to know their allocated space during rendering, avoiding direct access to layout nodes. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/components/render.lisp (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 ** Dirty propagation *** propagate-dirty 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). The recursion terminates when ~component-parent~ returns ~nil~ (the root component has no parent). #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/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