Subagent review fixes: textarea ignore-w, hit-test recursion, select/tabbar position, X10 release, CSI param < digit, text-input truncation

CRITICAL: Remove (declare (ignore w)) from textarea render (textarea.lisp:251)
  w is used for horizontal truncation on the next line.  Declaring it
  ignored while using it is undefined behavior in CL (SBCL warns).

HIGH: hit-test recurses into children (mouse.lisp:18-34)
  Was returning the root component for any click within its bounds,
  ignoring nested widgets entirely.  Now checks component-children
  first, returning the deepest match.

MEDIUM: Select/TabBar position hardcoded to (0,0)
  Both rendered at terminal origin regardless of layout position.
  Now read layout-node-x/y for absolute positioning.

MEDIUM: Text-input truncation missing
  Render drew full value string even when exceeding widget width.
  Now truncates to (min (length display) w).

MEDIUM: X10 mouse release detection added (input.lisp:219-226)
  X10 encoding uses button=3 for release.  Was detecting all events
  as press/drag.  Now checks button=3 → :release.

MEDIUM: parse-csi-params handles private markers (input.lisp:128-131)
  < = > ? characters (0x3c-0x3f) treated as parameter start markers
  instead of accumulating bogus digit values.  Latent trap removed.

Deferred (pre-existing design):
- Scrollbox visibility cy vs orig-y: match for column layout (common case)
- Nested scrollbox coordinates: assumes sequential layout positions
- text-input cursor drawing: feature, not bugfix

392 tests pass.
This commit is contained in:
Hermes
2026-05-12 00:55:03 +00:00
parent c3c330dfff
commit a294f21c70
7 changed files with 32 additions and 19 deletions

View File

@@ -124,7 +124,10 @@
((and (>= b #x30) (<= b #x3f)) ((and (>= b #x30) (<= b #x3f))
(if (char= (code-char b) #\;) (if (char= (code-char b) #\;)
(progn (push current params) (setf current 0)) (progn (push current params) (setf current 0))
(setf current (+ (* current 10) (- b #x30))))) ;; Non-digit parameter characters (< = > ?) start a new param at zero
(if (member b '(#x3c #x3d #x3e #x3f) :test #'=)
(setf current 0)
(setf current (+ (* current 10) (- b #x30))))))
((and (>= b #x20) (<= b #x2f)) ((and (>= b #x20) (<= b #x2f))
nil) nil)
((and (>= b #x40) (<= b #x7e)) ((and (>= b #x40) (<= b #x7e))
@@ -219,15 +222,12 @@
(y (third params)) (y (third params))
(button (logand p0 #x03)) (button (logand p0 #x03))
(motion (logand p0 #x20)) (motion (logand p0 #x20))
(wheel (logand p0 #x40))) (release (= button 3)))
(make-mouse-event (make-mouse-event
:type (if motion :drag :press) :type (cond (release :release)
:button (cond (wheel (if (zerop (logand p0 #x01)) (motion :drag)
:wheel-up :wheel-down)) (t :press))
((= button 0) :left) :button (let ((b button)) (cond ((= b 0) :left) ((= b 1) :middle) ((= b 2) :right) (t :none)))
((= button 1) :middle)
((= button 2) :right)
(t :none))
:x x :y y :raw (format nil "~C[<~d;~d;~d~C" #\Esc p0 x y (code-char final-byte)))) :x x :y y :raw (format nil "~C[<~d;~d;~d~C" #\Esc p0 x y (code-char final-byte))))
(let* ((tilde-p (char= (code-char final-byte) #\~)) (let* ((tilde-p (char= (code-char final-byte) #\~))
(param (or p0 0)) (param (or p0 0))

View File

@@ -17,17 +17,26 @@
(defun hit-test (root x y) (defun hit-test (root x y)
"Find the deepest component at (X, Y) by testing layout-node bounds. "Find the deepest component at (X, Y) by testing layout-node bounds.
Recurses into component-children to find the innermost match.
Components without a layout-node or position return nil." Components without a layout-node or position return nil."
(labels ((recurse (node) (labels ((recurse (node)
(let ((ln (ignore-errors (component-layout-node node)))) (let ((ln (ignore-errors (component-layout-node node)))
(best nil))
(when ln (when ln
(let ((nx (layout-node-x ln)) (let ((nx (layout-node-x ln))
(ny (layout-node-y ln)) (ny (layout-node-y ln))
(nw (layout-node-width ln)) (nw (layout-node-width ln))
(nh (layout-node-height ln))) (nh (layout-node-height ln)))
(when (and (>= x nx) (< x (+ nx nw)) ;; Check children first for deeper match
(>= y ny) (< y (+ ny nh))) (dolist (child (ignore-errors (component-children node)))
node)))))) (let ((child-hit (recurse child)))
(when child-hit
(setf best child-hit))))
;; If no child matched, check self
(or best
(when (and (>= x nx) (< x (+ nx nw))
(>= y ny) (< y (+ ny nh)))
node)))))))
(recurse root))) (recurse root)))
;; Selection ;; Selection

View File

@@ -76,7 +76,9 @@
(subseq filtered start end))) (subseq filtered start end)))
(defmethod render ((sel select) backend) (defmethod render ((sel select) backend)
(let* ((ln (select-layout-node sel)) (x 0) (y 0) (let* ((ln (select-layout-node sel))
(x (if ln (layout-node-x ln) 0))
(y (if ln (layout-node-y ln) 0))
(w (if ln (layout-node-width ln) 80)) (w (if ln (layout-node-width ln) 80))
(visible (select-visible-options sel)) (sel-idx (select-selected-index sel))) (visible (select-visible-options sel)) (sel-idx (select-selected-index sel)))
(dolist (item visible) (dolist (item visible)

View File

@@ -35,9 +35,11 @@
(case (key-event-key event) (:left (tab-bar-prev tb) t) (:right (tab-bar-next tb) t) (t nil))) (case (key-event-key event) (:left (tab-bar-prev tb) t) (:right (tab-bar-next tb) t) (t nil)))
(defmethod render ((tb tab-bar) backend) (defmethod render ((tb tab-bar) backend)
(let* ((ln (tab-bar-layout-node tb)) (y 0) (let* ((ln (tab-bar-layout-node tb))
(x (if ln (layout-node-x ln) 0))
(y (if ln (layout-node-y ln) 0))
(w (if ln (layout-node-width ln) 80)) (w (if ln (layout-node-width ln) 80))
(active-id (tab-bar-active tb)) (tabs (tab-bar-tabs tb)) (x-pos 0)) (active-id (tab-bar-active tb)) (tabs (tab-bar-tabs tb)) (x-pos x))
(dolist (tab tabs) (dolist (tab tabs)
(let* ((id (getf tab :id)) (title (getf tab :title)) (let* ((id (getf tab :id)) (title (getf tab :title))
(label (format nil " ~A " title)) (label-len (length label)) (label (format nil " ~A " title)) (label-len (length label))

View File

@@ -165,6 +165,7 @@
(cursor (text-input-cursor in)) (cursor (text-input-cursor in))
(display (if (plusp (length value)) (display (if (plusp (length value))
value value
(or (text-input-placeholder in) "")))) (or (text-input-placeholder in) "")))
(truncated (subseq display 0 (min (length display) w))))
(declare (ignore w cursor)) (declare (ignore w cursor))
(draw-text backend x y display nil nil))) (draw-text backend x y truncated nil nil)))

Binary file not shown.

View File

@@ -248,7 +248,6 @@
(h (if ln (layout-node-height ln) 24)) (h (if ln (layout-node-height ln) 24))
(lines (textarea-lines ta)) (lines (textarea-lines ta))
(max-lines (min (length lines) h))) (max-lines (min (length lines) h)))
(declare (ignore w))
(loop for i from 0 below max-lines (loop for i from 0 below max-lines
for line in lines for line in lines
do (draw-text backend x (+ y i) do (draw-text backend x (+ y i)