- Box class with border-style, title, fg/bg slots - render-box dispatches through backend protocol - draw-border for borders, draw-rect for background - draw-text for title below top border - 7 tests: defaults, border, background, title, no-border, zero-size, minimum-size - 13 assertions, 100% GREEN - ASDF updated with src/components module - modern-backend now accepts :output-stream initarg
4.4 KiB
v0.2.0: Renderables — Box and Text
Implementation plan for the first two renderable component types.
Goal: Create Box (border+background+title) and Text (styled wrapping text) renderables that render through the backend protocol.
Architecture: Each renderable is a CLOS class with a layout-node slot for positioning. The render method dispatches through the backend protocol (draw-text, draw-border, draw-rect). Tests capture backend output via string streams.
Files created:
org/box-renderable.org— Box class, render method (literate source)org/text-renderable.org— Text class, render method, inline spans (literate source)org/dirty-tracking.org— Dirty flag system (literate source)src/components/box.lisp— tangledsrc/components/text.lisp— tangledsrc/components/dirty.lisp— tangled
Files modified:
cl-tui.asd— add component modulesdocs/ROADMAP.org— mark v0.2.0 tasks DONE
Task 1: Box renderable
Objective: Box class that draws borders, fills backgrounds, and renders titles.
Files:
- Create:
org/box-renderable.org - Create:
src/components/box.lisp(extracted) - Modify:
cl-tui.asd— add components module
Box class:
(defclass box ()
((layout-node :initarg :layout-node :accessor box-layout-node)
(border-style :initform :single :initarg :border-style :accessor box-border-style)
(title :initform nil :initarg :title :accessor box-title)
(title-align :initform :left :initarg :title-align :accessor box-title-align)
(fg :initform nil :initarg :fg :accessor box-fg)
(bg :initform nil :initarg :bg :accessor box-bg)))
render-box method: Renders at computed layout position using backend's draw-border, draw-rect, draw-text. Delegates to the backend — no escape sequences directly.
Tests:
- Create box with border, verify draw-border was called with correct params
- Create box with title, verify title positioning
- Create box with background fill
- Edge cases: box with 0 width/height, no border style, very long title
Task 2: Text renderable
Objective: Text class that renders strings at layout position with word-wrap.
Files:
- Create:
org/text-renderable.org - Create:
src/components/text.lisp(extracted)
Text class:
(defclass text ()
((layout-node :initarg :layout-node :accessor text-layout-node)
(content :initarg :content :accessor text-content)
(fg :initform nil :initarg :fg :accessor text-fg)
(bg :initform nil :initarg :bg :accessor text-bg)
(wrap-mode :initform :word :initarg :wrap-mode :accessor text-wrap-mode)
(spans :initform nil :initarg :spans :accessor text-spans)))
render-text method:
- Get layout position (x, y, width, height)
- If wrap-mode is :none, truncate to width
- If wrap-mode is :word, word-wrap (break on whitespace)
- Draw each line via backend's draw-text
- Apply span attributes (bold, italic, etc.) per segment
Inline spans:
(defclass span ()
((text :initarg :text :accessor span-text)
(bold :initform nil :initarg :bold :accessor span-bold)
(italic :initform nil :initarg :italic :accessor span-italic)
(underline :initform nil :initarg :underline :accessor span-underline)))
Tests:
- Text renders string at correct position
- Word-wrap breaks at word boundaries
- Truncation mode clips at width
- Spans apply style attributes per segment
- Empty string rendering
- Single character
- String shorter than width (no wrapping needed)
Task 3: Dirty tracking
Objective: Lightweight dirty-flag system for incremental rendering.
Files:
- Create:
org/dirty-tracking.org - Create:
src/components/dirty.lisp(extracted)
(defgeneric mark-dirty (component))
(defgeneric dirty-p (component))
(defgeneric mark-clean (component))
Default methods mark/check a dirty slot on the component. When implemented:
mark-dirty— sets dirty flag, propagates to parentdirty-p— returns T if component needs re-rendermark-clean— clears dirty flag after render
Tests:
- New component is dirty (default)
- mark-clean clears dirty flag
- dirty-p returns nil after mark-clean
- mark-dirty sets dirty flag again
Task 4: Wire into ASDF + update roadmap
Files:
- Modify:
cl-tui.asd— add:module "components"to both main and test systems - Modify:
docs/ROADMAP.org— mark v0.2.0 tasks DONE
Run full test suite: All 72 existing tests + new component tests: 100% GREEN.