From 88c576a6b9e2ced412a50c2433b7603f0f238693 Mon Sep 17 00:00:00 2001 From: Hermes Date: Mon, 11 May 2026 14:57:44 +0000 Subject: [PATCH] review fixes: word-wrap hard-break, title-align, ASDF fix, edge cases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- cl-tui.asd | 2 +- src/components/box-tests.lisp | 16 +++++++++++--- src/components/box.lisp | 18 ++++++++++------ src/components/text.lisp | 40 +++++++++++++++++------------------ 4 files changed, 45 insertions(+), 31 deletions(-) diff --git a/cl-tui.asd b/cl-tui.asd index d93fd8a..dd940e8 100644 --- a/cl-tui.asd +++ b/cl-tui.asd @@ -38,4 +38,4 @@ ((:file "box-tests") (:file "dirty-tests")))) :perform (test-op (o c) - (uiop:symbol-call :cl-tui-backend-test '#:run!))) + (uiop:symbol-call :cl-tui-backend-test '#:run-tests))) diff --git a/src/components/box-tests.lisp b/src/components/box-tests.lisp index a4d95e2..a2aa701 100644 --- a/src/components/box-tests.lisp +++ b/src/components/box-tests.lisp @@ -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" diff --git a/src/components/box.lisp b/src/components/box.lisp index 83a0d6a..f85b20d 100644 --- a/src/components/box.lisp +++ b/src/components/box.lisp @@ -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)))))))) diff --git a/src/components/text.lisp b/src/components/text.lisp index 915bfea..6678f67 100644 --- a/src/components/text.lisp +++ b/src/components/text.lisp @@ -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 "")))))