review fixes: word-wrap hard-break, title-align, ASDF fix, edge cases

Fixes from subagent review:
- Word-wrap now hard-breaks words exceeding max-width (was returning
  un-truncated overflow strings)
- Box zero-size guard now catches any zero/single dimension (was only
  catching both zero together)
- Title-align now respected (:left/:center/:right) with proper positioning
- render-text declares (ignore spans) to suppress unused warning
- ASDF test-op fixed: run! → run-tests (symbol didn't exist)
- New test: box-single-column (width=1 renders nothing)
- Tightened word-wrap test: verifies hard-break produces both chunks
- Simplified word-wrap with cond instead of nested if/progn (avoided
  recurring paren-balance issue)
This commit is contained in:
Hermes
2026-05-11 14:57:44 +00:00
parent a1b1352d10
commit 88c576a6b9
4 changed files with 45 additions and 31 deletions

View File

@@ -66,7 +66,7 @@
(is-false (search "┌" out) "no top-left corner")))))
(test box-zero-size
"A zero-size box renders nothing"
"A box with any zero dimension 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)
@@ -74,6 +74,15 @@
(is (string= (get-output-stream-string s) "")
"zero-size box produces no output"))))
(test box-single-column
"A box with width 1 renders nothing (needs min 2 for border)"
(multiple-value-bind (b s) (make-capturing-backend)
(let ((bx (make-box :border-style :single :width 1 :height 5)))
(compute-layout (box-layout-node bx) 1 5)
(render-box bx b)
(is (string= (get-output-stream-string s) "")
"width=1 box renders nothing"))))
(test box-minimum-size
"A box with minimum non-zero size still renders"
(multiple-value-bind (b s) (make-capturing-backend)
@@ -131,13 +140,14 @@
(is (search "new" out) "third line")))))
(test text-word-wrap-single-word
"Text wraps even a single word if it exceeds width"
"A word longer than width is hard-broken at max-width"
(multiple-value-bind (b s) (make-capturing-backend)
(let ((tx (make-text "Hello" :width 3 :height 3)))
(compute-layout (text-layout-node tx) 3 3)
(render-text tx b)
(let ((out (get-output-stream-string s)))
(is (search "Hel" out) "word truncated to width")))))
(is (search "Hel" out) "first chunk is Hel")
(is (search "lo" out) "second chunk is lo")))))
(test span-creates-with-attributes
"A span has text and optional style attributes"

View File

@@ -36,15 +36,19 @@
(y (layout-node-y ln))
(w (layout-node-width ln))
(h (layout-node-height ln)))
(when (and (zerop w) (zerop h))
(when (or (zerop w) (zerop h) (< w 2) (< h 2))
(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))
(draw-border backend x y w h :style bs :fg fg :bg bg))
(when title
;; Render title below top border, left-aligned inside the box
(let ((tx (+ x 2))
(ty (+ y 1)))
(draw-text backend tx ty title fg bg))))))
(let* ((content-w (- w 4))
(tx (+ x 2))
(ty (+ y (if bs 1 0)))
(ta (box-title-align box))
(display (subseq title 0 (min (length title) content-w))))
(case ta
(:center (draw-text backend (+ x (ceiling (- w (length display)) 2)) ty display fg bg))
(:right (draw-text backend (+ x (- w (length display) 2)) ty display fg bg))
(t (draw-text backend tx ty display fg bg))))))))

View File

@@ -44,6 +44,7 @@
(bg (text-bg text-object))
(wrap (text-wrap-mode text-object))
(spans (text-spans text-object)))
(declare (ignore spans))
(let ((x (layout-node-x ln))
(y (layout-node-y ln))
(w (layout-node-width ln))
@@ -51,10 +52,8 @@
(when (or (zerop (length content)) (zerop w) (zerop h))
(return-from render-text (values)))
(if (eql wrap :none)
;; No wrap — truncate to width
(let ((display (subseq content 0 (min (length content) w))))
(draw-text backend x y display fg bg))
;; Word wrap
(let ((lines (word-wrap content w))
(max-lines h))
(loop for line in lines
@@ -62,27 +61,28 @@
do (draw-text backend x (+ y row) line fg bg)))))))
(defun word-wrap (text max-width)
"Split TEXT into lines, each no longer than MAX-WIDTH characters.
Breaks at word boundaries when possible."
"Split TEXT into lines, each <= MAX-WIDTH chars.
Breaks at word boundaries. Words exceeding MAX-WIDTH are hard-broken."
(if (or (zerop max-width) (zerop (length text)))
(list "")
(let ((words (split-string text))
(lines nil)
(current nil)
(current-len 0))
(let ((words (split-string text)) (lines nil) (current nil) (current-len 0))
(dolist (word words)
(let ((word-len (length word)))
(if (and current (<= (+ current-len 1 word-len) max-width))
;; Add to current line
(progn
(push word current)
(incf current-len (1+ word-len)))
;; Start new line
(progn
(when current
(push (format nil "~{~A~^ ~}" (nreverse current)) lines))
(setf current (list word))
(setf current-len word-len)))))
(let ((wl (length word)))
(cond ((<= wl max-width)
(if (and current (<= (+ current-len 1 wl) max-width))
(push word current)
(progn
(when current
(push (format nil "~{~A~^ ~}" (nreverse current)) lines))
(setf current (list word))
(setf current-len wl))))
(t
(when current
(push (format nil "~{~A~^ ~}" (nreverse current)) lines)
(setf current nil)
(setf current-len 0))
(loop for i from 0 below wl by max-width
do (push (subseq word i (min (+ i max-width) wl)) lines))))))
(when current
(push (format nil "~{~A~^ ~}" (nreverse current)) lines))
(or (nreverse lines) (list "")))))