literate: restructure all 19 org files with per-function blocks and prose
Every function, defclass, defstruct, defgeneric, defmethod, defmacro, defvar, and defparameter in every org file now has its own #+BEGIN_SRC block with literate prose above it explaining the design reasoning. Block counts before → after: package.org: 1 → 7 container-package.org: 1 → 1 (prose expanded) dirty.org: 4 → 6 render.org: 10 → 25 theme.org: 6 → 19 box-renderable.org: 9 → 29 scrollbox.org: 8 → 26 tabbar.org: 5 → 10 backend-protocol.org: 8 → 66 modern-backend.org: 17 → 53 detection.org: 4 → 6 layout-engine.org: 9 → 36 framebuffer.org: 8 → 37 markdown-renderer.org:13 → 38 dialog.org: 17 → 23 (merged dual structure) mouse.org: 4 → 25 select.org: 12 → 30 slot.org: 4 → 12 text-input.org: 11 → 53 Total: ~153 blocks → ~502 blocks Bugs fixed during restructuring: - render.org: stray π character typo (backenπd → backend) - modern-backend.org: sgr-attr missing closing paren + #+END_SRC - detection.org: invalid #\Esc character reference - select.org: extra closing paren in select-visible-options All 13 test suites pass at 100%.
This commit is contained in:
231
org/render.org
231
org/render.org
@@ -65,6 +65,13 @@ Mark ~component~ and every ancestor dirty. Walks up via
|
||||
|
||||
* 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 ../src/components/render-tests.lisp
|
||||
(in-package :cl-tty-box-test)
|
||||
(in-suite box-suite)
|
||||
@@ -73,7 +80,17 @@ Mark ~component~ and every ancestor dirty. Walks up via
|
||||
(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 ../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)
|
||||
@@ -81,7 +98,17 @@ Mark ~component~ and every ancestor dirty. Walks up via
|
||||
(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 ../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)
|
||||
@@ -89,19 +116,51 @@ Mark ~component~ and every ancestor dirty. Walks up via
|
||||
(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 ../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 ../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 ../src/components/render-tests.lisp
|
||||
(test propagate-dirty-marks-component
|
||||
"propagate-dirty marks the component dirty"
|
||||
(let ((c (make-box)))
|
||||
@@ -109,7 +168,19 @@ Mark ~component~ and every ancestor dirty. Walks up via
|
||||
(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 ../src/components/render-tests.lisp
|
||||
(test available-width-defaults
|
||||
"available-width returns 0 for components without explicit width"
|
||||
(let ((c (make-box)))
|
||||
@@ -124,22 +195,46 @@ 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 ../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)))
|
||||
(:documentation "Return the layout-node for COMPONENT."))
|
||||
#+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.
|
||||
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 ../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 ../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 ../src/components/render.lisp
|
||||
(defgeneric component-children (component)
|
||||
@@ -147,8 +242,13 @@ method.
|
||||
(:method ((c t)) nil))
|
||||
#+END_SRC
|
||||
|
||||
Leaf components (~box~, ~text~) have no children. Container components
|
||||
(~scrollbox~, ~tabbar~) override this to return their child list.
|
||||
*** 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 ../src/components/render.lisp
|
||||
(defgeneric component-parent (component)
|
||||
@@ -156,11 +256,16 @@ Leaf components (~box~, ~text~) have no children. Container components
|
||||
(: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
|
||||
|
||||
*** 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 ../src/components/render.lisp
|
||||
;; ── Rendering Pipeline ────────────────────────────────────────
|
||||
|
||||
@@ -171,25 +276,43 @@ used by ~propagate-dirty~ to walk up the tree.
|
||||
(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.
|
||||
*** 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 ../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 ../src/components/render.lisp
|
||||
(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
|
||||
|
||||
*** 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 ../src/components/render.lisp
|
||||
(defun render-screen (root backend)
|
||||
"Render the component tree ROOT using BACKEND.
|
||||
@@ -203,14 +326,13 @@ methods delegate to the specific rendering functions defined in
|
||||
(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.
|
||||
*** render-node
|
||||
|
||||
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.
|
||||
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 ../src/components/render.lisp
|
||||
(defun render-node (node backend)
|
||||
@@ -222,34 +344,53 @@ flicker.
|
||||
(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
|
||||
|
||||
*** 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 ../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 ../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
|
||||
|
||||
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
|
||||
|
||||
*** 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 ../src/components/render.lisp
|
||||
;; ── Dirty Propagation ─────────────────────────────────────────
|
||||
|
||||
@@ -260,13 +401,3 @@ backenπd.
|
||||
(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).
|
||||
|
||||
Reference in New Issue
Block a user