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

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 — 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:

(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:

  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:

(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 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.