literate: create org/dirty.org as proof of literate programming workflow
org/dirty.org is now the source of truth for dirty.lisp and dirty-tests.lisp. The process: Overview → Contract → Tests → Implement → Tangle → Test (GREEN) Hand-written .lisp files were deleted and regenerated from org alone to prove the pipeline works.
This commit is contained in:
112
org/dirty.org
Normal file
112
org/dirty.org
Normal file
@@ -0,0 +1,112 @@
|
||||
#+TITLE: Dirty Tracking
|
||||
#+STARTUP: content
|
||||
#+FILETAGS: :cl-tty:components:
|
||||
|
||||
* Overview
|
||||
|
||||
The dirty tracking module provides a mixin class and protocol for
|
||||
marking components as needing re-render. This is the foundation of
|
||||
the incremental rendering pipeline.
|
||||
|
||||
Without dirty tracking, every frame would re-render every component.
|
||||
With it, only components that changed (and their ancestors, for layout
|
||||
recomputation) get re-processed. The makes the difference between a
|
||||
60fps terminal UI and a flickering mess.
|
||||
|
||||
This module is intentionally minimal: a single mixin class and two
|
||||
generic functions. The complexity lives in the propagation logic
|
||||
(see ~render.lisp~), but the dirty state itself is trivial.
|
||||
|
||||
* Contract
|
||||
|
||||
** ~dirty-mixin~
|
||||
|
||||
A class that adds a ~dirty~ slot. Components that need dirty tracking
|
||||
inherit from this.
|
||||
|
||||
- ~(dirty-p component)~ — returns ~t~ if the component needs re-render,
|
||||
~nil~ if it's up-to-date. New instances start dirty (~t~).
|
||||
|
||||
** ~mark-clean~
|
||||
|
||||
- ~(mark-clean component)~ — sets dirty to ~nil~. Called after rendering.
|
||||
- Specialized on ~dirty-mixin~; default method is a no-op.
|
||||
|
||||
** ~mark-dirty~
|
||||
|
||||
- ~(mark-dirty component)~ — sets dirty to ~t~. Called when the component's
|
||||
state changes (user typed a character, selection changed, etc.).
|
||||
- Specialized on ~dirty-mixin~; default method is a no-op.
|
||||
|
||||
* Tests
|
||||
|
||||
#+BEGIN_SRC lisp :tangle ../src/components/dirty-tests.lisp
|
||||
;; Dirty tracking tests are in box-tests.lisp (same test suite)
|
||||
(in-package :cl-tty-box-test)
|
||||
(in-suite box-suite)
|
||||
|
||||
(test dirty-mixin-default-is-dirty
|
||||
"A dirty-mixin starts as dirty"
|
||||
(let ((c (make-instance 'dirty-mixin)))
|
||||
(is-true (dirty-p c) "new component should be dirty")))
|
||||
|
||||
(test mark-clean-clears-dirty
|
||||
"mark-clean sets dirty to nil"
|
||||
(let ((c (make-instance 'dirty-mixin)))
|
||||
(mark-clean c)
|
||||
(is-false (dirty-p c) "after mark-clean, should not be dirty")))
|
||||
|
||||
(test mark-dirty-sets-dirty
|
||||
"mark-dirty sets dirty to t"
|
||||
(let ((c (make-instance 'dirty-mixin)))
|
||||
(mark-clean c)
|
||||
(mark-dirty c)
|
||||
(is-true (dirty-p c) "after mark-dirty, should be dirty again")))
|
||||
#+END_SRC
|
||||
|
||||
* Implementation
|
||||
|
||||
The entire module is a class and two generic functions. The design
|
||||
choice: make this a separate mixin rather than part of the base
|
||||
~component~ class. This lets non-UI objects (layout nodes, render
|
||||
commands) opt into dirty tracking without inheriting from component.
|
||||
|
||||
#+BEGIN_SRC lisp :tangle ../src/components/dirty.lisp
|
||||
(in-package :cl-tty.box)
|
||||
|
||||
;; ── Dirty Tracking ─────────────────────────────────────────────
|
||||
|
||||
(defclass dirty-mixin ()
|
||||
((dirty :initform t :accessor dirty-p)))
|
||||
#+END_SRC
|
||||
|
||||
The ~initform t~ is critical: new components are dirty by default so
|
||||
the first render pass doesn't skip them. If this default were ~nil~,
|
||||
new components would be invisible until something explicitly marked
|
||||
them dirty.
|
||||
|
||||
#+BEGIN_SRC lisp :tangle ../src/components/dirty.lisp
|
||||
(defgeneric mark-clean (component)
|
||||
(:method ((c dirty-mixin))
|
||||
(setf (dirty-p c) nil)))
|
||||
#+END_SRC
|
||||
|
||||
~mark-clean~ is called at the end of a render cycle. The default
|
||||
method (for non-dirty-mixin components) is a no-op — they have no
|
||||
dirty state to clear.
|
||||
|
||||
#+BEGIN_SRC lisp :tangle ../src/components/dirty.lisp
|
||||
(defgeneric mark-dirty (component)
|
||||
(:method ((c dirty-mixin))
|
||||
(setf (dirty-p c) t)))
|
||||
#+END_SRC
|
||||
|
||||
~mark-dirty~ is called whenever the component's visual state changes.
|
||||
Together with ~propagate-dirty~ in the render pipeline, this ensures
|
||||
that when a text input gains a character, not just the input component
|
||||
but its containing box, tab, and screen all get re-rendered.
|
||||
|
||||
These are generic functions (not plain functions) so other mixins or
|
||||
base classes can provide their own methods. The ~:method~ on
|
||||
~dirty-mixin~ provides the default implementation for anything that
|
||||
includes this mixin.
|
||||
Reference in New Issue
Block a user