- 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
128 lines
4.4 KiB
Markdown
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.
|