Files
cl-tty/org/box-renderable.org
Hermes 811d51a4f2 Rename cl-tui -> cl-tty, v0.9.0: Dialog System + Toast
Rename: cl-tty avoids naming collision with Quicklisp's cl-tui (naryl/cl-tui,
a cl-charms-based ncurses library). Our project is pure escape-sequence CL.

v0.9.0 adds:
- Dialog base class: modal overlay with backdrop, centered panel, size
  variants (:small/:medium/:large), stack-based management
- Dialog subclasses: alert, confirm, select-dialog, prompt-dialog
- Toast notifications: transient, top-right corner, auto-dismiss,
  colored variants (info/success/warning/error)
- 78 tests total, 100% passing

ASDF: read-time package references (+fiveam:+) replaced with
find-symbol so .asd loads without FiveAM pre-loaded
2026-05-11 19:55:37 +00:00

5.8 KiB

cl-tty Box Renderable — v0.2.0

Box Renderable

The Box renderable draws a bordered rectangle with optional title and background fill. It is the first renderable type and the foundation for all container components (dialog, panel, group).

A Box has a layout-node slot for positioning via the layout engine. Its render-box method dispatches through the backend protocol.

Contract

  • (make-box &key border-style title title-align fg bg) → box Create a Box with optional border style, title, and colors.
  • (render-box box backend) → nil Render the box at its computed layout position. Draws background fill, border, and title if configured.
  • (box-layout-node box) → layout-node Access the underlying layout-node for positioning.

Tests

(defpackage :cl-tty-box-test
  (:use :cl :fiveam :cl-tty.backend :cl-tty.layout)
  (:export #:run-tests))
(in-package :cl-tty-box-test)

(def-suite box-suite :description "Box renderable tests")
(in-suite box-suite)

(defun run-tests ()
  (let ((result (run 'box-suite)))
    (fiveam:explain! result)
    (uiop:quit 0)))

(defun make-capturing-backend ()
  (let* ((s (make-string-output-stream))
         (b (make-modern-backend :output-stream s)))
    (values b s)))

(test box-creates-with-defaults
  "A box created with no arguments has reasonable defaults"
  (let ((b (make-box)))
    (is (typep b 'box))
    (is (typep (box-layout-node b) 'layout-node))))

(test box-renders-border
  "A box with border draws border characters"
  (multiple-value-bind (b s) (make-capturing-backend)
    (let ((bx (make-box :border-style :single :width 10 :height 5)))
      (compute-layout (box-layout-node bx) 10 5)
      (render-box bx b)
      (let ((out (get-output-stream-string s)))
        (is (search "┌" out) "top-left corner")
        (is (search "┐" out) "top-right corner")
        (is (search "└" out) "bottom-left corner")
        (is (search "┘" out) "bottom-right corner")))))

(test box-renders-background
  "A box with background color fills interior"
  (multiple-value-bind (b s) (make-capturing-backend)
    (let ((bx (make-box :bg :red :width 5 :height 3)))
      (compute-layout (box-layout-node bx) 5 3)
      (render-box bx b)
      (let ((out (get-output-stream-string s)))
        ;; Should contain SGR background escape for red
        (is (search "48;2;255;0;0" out) "SGR background should be red")
        (is (search "┌" out) "border with background")))))

(test box-renders-title
  "A box with title renders the title text"
  (multiple-value-bind (b s) (make-capturing-backend)
    (let ((bx (make-box :title "Hello" :width 12 :height 3)))
      (compute-layout (box-layout-node bx) 12 3)
      (render-box bx b)
      (let ((out (get-output-stream-string s)))
        (is (search "Hello" out) "title text should appear")))))

(test box-without-border
  "A box with border-style nil draws no border"
  (multiple-value-bind (b s) (make-capturing-backend)
    (let ((bx (make-box :border-style nil :bg :red :width 5 :height 3)))
      (compute-layout (box-layout-node bx) 5 3)
      (render-box bx b)
      (let ((out (get-output-stream-string s)))
        (is (search "48;2;255;0;0" out) "background still renders")
        ;; No border chars
        (is-false (search "┌" out) "no top-left corner")))))

(test box-zero-size
  "A zero-size box renders nothing"
  (multiple-value-bind (b s) (make-capturing-backend)
    (let ((bx (make-box :border-style :single :width 0 :height 0)))
      (compute-layout (box-layout-node bx) 0 0)
      (render-box bx b)
      (is (string= (get-output-stream-string s) "")
          "zero-size box produces no output"))))

(test box-minimum-size
  "A box with minimum non-zero size still renders"
  (multiple-value-bind (b s) (make-capturing-backend)
    (let ((bx (make-box :border-style :single :width 2 :height 2)))
      (compute-layout (box-layout-node bx) 2 2)
      (render-box bx b)
      (let ((out (get-output-stream-string s)))
        (is (search "┌" out) "2x2 box still has borders")))))

Implementation

(in-package :cl-tty.box)

(defclass box ()
  ((layout-node :initform (make-layout-node) :accessor box-layout-node
     :initarg :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)))

(defun make-box (&key (border-style :single) title
                  (title-align :left) fg bg
                  width height)
  (make-instance 'box
    :border-style border-style
    :title title
    :title-align title-align
    :fg fg
    :bg bg
    :layout-node (make-layout-node
                   :width width
                   :height height
                   :direction :column)))

(defun render-box (box backend)
  "Render BOX at its computed layout position using BACKEND."
  (let ((ln (box-layout-node box))
        (bs (box-border-style box))
        (title (box-title box))
        (fg (box-fg box))
        (bg (box-bg box)))
    (let ((x (layout-node-x ln))
          (y (layout-node-y ln))
          (w (layout-node-width ln))
          (h (layout-node-height ln)))
      (when (and (zerop w) (zerop h))
        (return-from render-box (values)))
      (when bg
        (draw-rect backend x y w h :bg bg))
      (when bs
        (draw-border backend x y w h
                     :style bs :fg fg :bg bg
                     :title title
                     :title-align (box-title-align box)))
      (when (and title bs)
        ;; Title is rendered by draw-border — nothing extra needed
        (values)))))