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:
@@ -38,4 +38,4 @@
|
|||||||
((:file "box-tests")
|
((:file "box-tests")
|
||||||
(:file "dirty-tests"))))
|
(:file "dirty-tests"))))
|
||||||
:perform (test-op (o c)
|
:perform (test-op (o c)
|
||||||
(uiop:symbol-call :cl-tui-backend-test '#:run!)))
|
(uiop:symbol-call :cl-tui-backend-test '#:run-tests)))
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
(is-false (search "┌" out) "no top-left corner")))))
|
(is-false (search "┌" out) "no top-left corner")))))
|
||||||
|
|
||||||
(test box-zero-size
|
(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)
|
(multiple-value-bind (b s) (make-capturing-backend)
|
||||||
(let ((bx (make-box :border-style :single :width 0 :height 0)))
|
(let ((bx (make-box :border-style :single :width 0 :height 0)))
|
||||||
(compute-layout (box-layout-node bx) 0 0)
|
(compute-layout (box-layout-node bx) 0 0)
|
||||||
@@ -74,6 +74,15 @@
|
|||||||
(is (string= (get-output-stream-string s) "")
|
(is (string= (get-output-stream-string s) "")
|
||||||
"zero-size box produces no output"))))
|
"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
|
(test box-minimum-size
|
||||||
"A box with minimum non-zero size still renders"
|
"A box with minimum non-zero size still renders"
|
||||||
(multiple-value-bind (b s) (make-capturing-backend)
|
(multiple-value-bind (b s) (make-capturing-backend)
|
||||||
@@ -131,13 +140,14 @@
|
|||||||
(is (search "new" out) "third line")))))
|
(is (search "new" out) "third line")))))
|
||||||
|
|
||||||
(test text-word-wrap-single-word
|
(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)
|
(multiple-value-bind (b s) (make-capturing-backend)
|
||||||
(let ((tx (make-text "Hello" :width 3 :height 3)))
|
(let ((tx (make-text "Hello" :width 3 :height 3)))
|
||||||
(compute-layout (text-layout-node tx) 3 3)
|
(compute-layout (text-layout-node tx) 3 3)
|
||||||
(render-text tx b)
|
(render-text tx b)
|
||||||
(let ((out (get-output-stream-string s)))
|
(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
|
(test span-creates-with-attributes
|
||||||
"A span has text and optional style attributes"
|
"A span has text and optional style attributes"
|
||||||
|
|||||||
@@ -36,15 +36,19 @@
|
|||||||
(y (layout-node-y ln))
|
(y (layout-node-y ln))
|
||||||
(w (layout-node-width ln))
|
(w (layout-node-width ln))
|
||||||
(h (layout-node-height 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)))
|
(return-from render-box (values)))
|
||||||
(when bg
|
(when bg
|
||||||
(draw-rect backend x y w h :bg bg))
|
(draw-rect backend x y w h :bg bg))
|
||||||
(when bs
|
(when bs
|
||||||
(draw-border backend x y w h
|
(draw-border backend x y w h :style bs :fg fg :bg bg))
|
||||||
:style bs :fg fg :bg bg))
|
|
||||||
(when title
|
(when title
|
||||||
;; Render title below top border, left-aligned inside the box
|
(let* ((content-w (- w 4))
|
||||||
(let ((tx (+ x 2))
|
(tx (+ x 2))
|
||||||
(ty (+ y 1)))
|
(ty (+ y (if bs 1 0)))
|
||||||
(draw-text backend tx ty title fg bg))))))
|
(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))))))))
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
(bg (text-bg text-object))
|
(bg (text-bg text-object))
|
||||||
(wrap (text-wrap-mode text-object))
|
(wrap (text-wrap-mode text-object))
|
||||||
(spans (text-spans text-object)))
|
(spans (text-spans text-object)))
|
||||||
|
(declare (ignore spans))
|
||||||
(let ((x (layout-node-x ln))
|
(let ((x (layout-node-x ln))
|
||||||
(y (layout-node-y ln))
|
(y (layout-node-y ln))
|
||||||
(w (layout-node-width ln))
|
(w (layout-node-width ln))
|
||||||
@@ -51,10 +52,8 @@
|
|||||||
(when (or (zerop (length content)) (zerop w) (zerop h))
|
(when (or (zerop (length content)) (zerop w) (zerop h))
|
||||||
(return-from render-text (values)))
|
(return-from render-text (values)))
|
||||||
(if (eql wrap :none)
|
(if (eql wrap :none)
|
||||||
;; No wrap — truncate to width
|
|
||||||
(let ((display (subseq content 0 (min (length content) w))))
|
(let ((display (subseq content 0 (min (length content) w))))
|
||||||
(draw-text backend x y display fg bg))
|
(draw-text backend x y display fg bg))
|
||||||
;; Word wrap
|
|
||||||
(let ((lines (word-wrap content w))
|
(let ((lines (word-wrap content w))
|
||||||
(max-lines h))
|
(max-lines h))
|
||||||
(loop for line in lines
|
(loop for line in lines
|
||||||
@@ -62,27 +61,28 @@
|
|||||||
do (draw-text backend x (+ y row) line fg bg)))))))
|
do (draw-text backend x (+ y row) line fg bg)))))))
|
||||||
|
|
||||||
(defun word-wrap (text max-width)
|
(defun word-wrap (text max-width)
|
||||||
"Split TEXT into lines, each no longer than MAX-WIDTH characters.
|
"Split TEXT into lines, each <= MAX-WIDTH chars.
|
||||||
Breaks at word boundaries when possible."
|
Breaks at word boundaries. Words exceeding MAX-WIDTH are hard-broken."
|
||||||
(if (or (zerop max-width) (zerop (length text)))
|
(if (or (zerop max-width) (zerop (length text)))
|
||||||
(list "")
|
(list "")
|
||||||
(let ((words (split-string text))
|
(let ((words (split-string text)) (lines nil) (current nil) (current-len 0))
|
||||||
(lines nil)
|
|
||||||
(current nil)
|
|
||||||
(current-len 0))
|
|
||||||
(dolist (word words)
|
(dolist (word words)
|
||||||
(let ((word-len (length word)))
|
(let ((wl (length word)))
|
||||||
(if (and current (<= (+ current-len 1 word-len) max-width))
|
(cond ((<= wl max-width)
|
||||||
;; Add to current line
|
(if (and current (<= (+ current-len 1 wl) max-width))
|
||||||
(progn
|
|
||||||
(push word current)
|
(push word current)
|
||||||
(incf current-len (1+ word-len)))
|
|
||||||
;; Start new line
|
|
||||||
(progn
|
(progn
|
||||||
(when current
|
(when current
|
||||||
(push (format nil "~{~A~^ ~}" (nreverse current)) lines))
|
(push (format nil "~{~A~^ ~}" (nreverse current)) lines))
|
||||||
(setf current (list word))
|
(setf current (list word))
|
||||||
(setf current-len word-len)))))
|
(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
|
(when current
|
||||||
(push (format nil "~{~A~^ ~}" (nreverse current)) lines))
|
(push (format nil "~{~A~^ ~}" (nreverse current)) lines))
|
||||||
(or (nreverse lines) (list "")))))
|
(or (nreverse lines) (list "")))))
|
||||||
|
|||||||
Reference in New Issue
Block a user