Files
cl-tty/docs/plans/2026-05-11-v0.2.0-box-and-text.md
Hermes a5f8e6c9d4 v0.2.0: Box renderable — border, background, and title
- 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
2026-05-11 14:41:38 +00:00

128 lines
4.4 KiB
Markdown

# 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` — tangled
- `src/components/text.lisp` — tangled
- `src/components/dirty.lisp` — tangled
**Files modified:**
- `cl-tui.asd` — add component modules
- `docs/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:**
```lisp
(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:**
```lisp
(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:**
1. Get layout position (x, y, width, height)
2. If wrap-mode is :none, truncate to width
3. If wrap-mode is :word, word-wrap (break on whitespace)
4. Draw each line via backend's draw-text
5. Apply span attributes (bold, italic, etc.) per segment
**Inline spans:**
```lisp
(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)
```lisp
(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 parent
- `dirty-p` — returns T if component needs re-render
- `mark-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.