v1.0.0 — Stable release + TUI support #8
@@ -10,7 +10,7 @@
|
||||
"src/components/dirty-tests.lisp"
|
||||
"src/components/render-tests.lisp"
|
||||
"src/components/theme-tests.lisp"
|
||||
"src/components/input-tests.lisp"
|
||||
"tests/input-tests.lisp"
|
||||
"tests/scrollbox-tabbar-tests.lisp"
|
||||
"tests/select-tests.lisp"
|
||||
"tests/markdown-tests.lisp"
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#:with-raw-terminal
|
||||
;; Event reading
|
||||
#:read-event
|
||||
#:utf8-decode
|
||||
;; Terminal resize flag
|
||||
#:*terminal-resized-p*
|
||||
;; TextInput
|
||||
|
||||
@@ -1,366 +1,9 @@
|
||||
(defpackage :cl-tty-input-test
|
||||
(:use :cl :fiveam :cl-tty.backend :cl-tty.box :cl-tty.layout :cl-tty.input)
|
||||
(:export #:run-tests))
|
||||
;; This file is deprecated. Tests moved to tests/input-tests.lisp.
|
||||
;; Kept as placeholder to prevent confusion with stale copies.
|
||||
(in-package :cl-tty-input-test)
|
||||
|
||||
(def-suite input-suite :description "Text input and keybinding tests")
|
||||
(in-suite input-suite)
|
||||
|
||||
(defun run-tests ()
|
||||
(warn "src/components/input-tests.lisp is deprecated. Use tests/input-tests.lisp instead.")
|
||||
(let ((result (run 'input-suite)))
|
||||
(fiveam:explain! result)
|
||||
(uiop:quit 0)))
|
||||
|
||||
;; ── Key Event Tests ─────────────────────────────────────────────
|
||||
|
||||
(test key-event-construction
|
||||
"A key-event can be created and queried."
|
||||
(let ((e (make-key-event :key :a :ctrl t :alt nil)))
|
||||
(is (eql (key-event-key e) :a))
|
||||
(is-true (key-event-ctrl e))
|
||||
(is-false (key-event-alt e))))
|
||||
|
||||
(test key-event-defaults
|
||||
"Fields default to NIL/nil."
|
||||
(let ((e (make-key-event :key :space)))
|
||||
(is (eql (key-event-key e) :space))
|
||||
(is-false (key-event-ctrl e))
|
||||
(is-false (key-event-alt e))
|
||||
(is-false (key-event-shift e))))
|
||||
|
||||
(test mouse-event-construction
|
||||
"A mouse-event can be created and queried."
|
||||
(let ((e (make-mouse-event :type :press :button :left :x 10 :y 5)))
|
||||
(is (eql (mouse-event-type e) :press))
|
||||
(is (eql (mouse-event-button e) :left))
|
||||
(is (= (mouse-event-x e) 10))
|
||||
(is (= (mouse-event-y e) 5))))
|
||||
|
||||
;; ── TextInput Tests ─────────────────────────────────────────────
|
||||
|
||||
(test text-input-empty
|
||||
"A newly created text-input has empty value and cursor at 0."
|
||||
(let ((in (make-text-input)))
|
||||
(is (string= (text-input-value in) ""))
|
||||
(is (= (text-input-cursor in) 0))))
|
||||
|
||||
(test text-input-insert-char
|
||||
"Inserting a character appends and moves cursor."
|
||||
(let ((in (make-text-input)))
|
||||
(handle-text-input in (make-key-event :key :a :code (char-code #\a)))
|
||||
(is (string= (text-input-value in) "a"))
|
||||
(is (= (text-input-cursor in) 1))))
|
||||
|
||||
(test text-input-insert-multiple
|
||||
"Inserting multiple characters works left to right."
|
||||
(let ((in (make-text-input)))
|
||||
(handle-text-input in (make-key-event :key :h :code (char-code #\h)))
|
||||
(handle-text-input in (make-key-event :key :e :code (char-code #\e)))
|
||||
(handle-text-input in (make-key-event :key :l :code (char-code #\l)))
|
||||
(handle-text-input in (make-key-event :key :l :code (char-code #\l)))
|
||||
(handle-text-input in (make-key-event :key :o :code (char-code #\o)))
|
||||
(is (string= (text-input-value in) "hello"))
|
||||
(is (= (text-input-cursor in) 5))))
|
||||
|
||||
(test text-input-backspace
|
||||
"Backspace removes the character before the cursor."
|
||||
(let ((in (make-text-input :value "ab" :cursor 2)))
|
||||
(handle-text-input in (make-key-event :key :backspace))
|
||||
(is (string= (text-input-value in) "a"))
|
||||
(is (= (text-input-cursor in) 1))))
|
||||
|
||||
(test text-input-backspace-at-start
|
||||
"Backspace at position 0 does nothing."
|
||||
(let ((in (make-text-input :value "ab" :cursor 0)))
|
||||
(handle-text-input in (make-key-event :key :backspace))
|
||||
(is (string= (text-input-value in) "ab"))
|
||||
(is (= (text-input-cursor in) 0))))
|
||||
|
||||
(test text-input-delete
|
||||
"Delete removes the character at the cursor."
|
||||
(let ((in (make-text-input :value "abc" :cursor 1)))
|
||||
(handle-text-input in (make-key-event :key :delete))
|
||||
(is (string= (text-input-value in) "ac"))
|
||||
(is (= (text-input-cursor in) 1))))
|
||||
|
||||
(test text-input-cursor-left-right
|
||||
"Cursor moves left and right."
|
||||
(let ((in (make-text-input :value "ab" :cursor 2)))
|
||||
(handle-text-input in (make-key-event :key :left))
|
||||
(is (= (text-input-cursor in) 1))
|
||||
(handle-text-input in (make-key-event :key :right))
|
||||
(is (= (text-input-cursor in) 2))))
|
||||
|
||||
(test text-input-cursor-bounds
|
||||
"Cursor cannot move past start or end."
|
||||
(let ((in (make-text-input :value "ab" :cursor 0)))
|
||||
(handle-text-input in (make-key-event :key :left))
|
||||
(is (= (text-input-cursor in) 0))
|
||||
(setf (text-input-cursor in) 2)
|
||||
(handle-text-input in (make-key-event :key :right))
|
||||
(is (= (text-input-cursor in) 2))))
|
||||
|
||||
(test text-input-home-end
|
||||
"Home moves to start, End moves to end."
|
||||
(let ((in (make-text-input :value "hello" :cursor 3)))
|
||||
(handle-text-input in (make-key-event :key :home))
|
||||
(is (= (text-input-cursor in) 0))
|
||||
(handle-text-input in (make-key-event :key :end))
|
||||
(is (= (text-input-cursor in) 5))))
|
||||
|
||||
(test text-input-max-length
|
||||
"Max-length prevents inserting beyond the limit."
|
||||
(let ((in (make-text-input :max-length 3)))
|
||||
(handle-text-input in (make-key-event :key :a :code (char-code #\a)))
|
||||
(handle-text-input in (make-key-event :key :b :code (char-code #\b)))
|
||||
(handle-text-input in (make-key-event :key :c :code (char-code #\c)))
|
||||
(handle-text-input in (make-key-event :key :d :code (char-code #\d)))
|
||||
(is (string= (text-input-value in) "abc"))))
|
||||
|
||||
(test text-input-placeholder
|
||||
"Placeholder is stored but does not affect value."
|
||||
(let ((in (make-text-input :placeholder "Type here...")))
|
||||
(is (string= (text-input-placeholder in) "Type here..."))
|
||||
(is (string= (text-input-value in) ""))))
|
||||
|
||||
(test text-input-on-submit
|
||||
"On-submit callback fires on Enter."
|
||||
(let ((result (list nil)))
|
||||
(let ((in (make-text-input :value "hello"
|
||||
:on-submit (lambda (v) (setf (car result) v)))))
|
||||
(handle-text-input in (make-key-event :key :enter))
|
||||
(is (string= (car result) "hello")))))
|
||||
|
||||
(test text-input-ctrl-a-e
|
||||
"Ctrl+A moves to home, Ctrl+E moves to end."
|
||||
(let ((in (make-text-input :value "abc" :cursor 2)))
|
||||
(handle-text-input in (make-key-event :key :a :ctrl t))
|
||||
(is (= (text-input-cursor in) 0))
|
||||
(handle-text-input in (make-key-event :key :e :ctrl t))
|
||||
(is (= (text-input-cursor in) 3))))
|
||||
|
||||
(test text-input-insert-in-middle
|
||||
"Inserting in the middle of text shifts rest right."
|
||||
(let ((in (make-text-input :value "ab" :cursor 1)))
|
||||
(handle-text-input in (make-key-event :key :x :code (char-code #\x)))
|
||||
(is (string= (text-input-value in) "axb"))
|
||||
(is (= (text-input-cursor in) 2))))
|
||||
|
||||
(test text-input-dirty-on-insert
|
||||
"Inserting marks the widget dirty."
|
||||
(let ((in (make-text-input)))
|
||||
(mark-clean in)
|
||||
(handle-text-input in (make-key-event :key :a :code (char-code #\a)))
|
||||
(is-true (dirty-p in))))
|
||||
|
||||
;; ── Textarea Tests ──────────────────────────────────────────────
|
||||
|
||||
(test textarea-empty
|
||||
"New textarea has empty value and cursor at (0,0)."
|
||||
(let ((a (make-textarea)))
|
||||
(is (string= (textarea-value a) ""))
|
||||
(is (= (textarea-cursor-row a) 0))
|
||||
(is (= (textarea-cursor-col a) 0))))
|
||||
|
||||
(test textarea-newline
|
||||
"Enter inserts a newline."
|
||||
(let ((a (make-textarea)))
|
||||
(handle-textarea-input a (make-key-event :key :a :code (char-code #\a)))
|
||||
(handle-textarea-input a (make-key-event :key :enter))
|
||||
(handle-textarea-input a (make-key-event :key :b :code (char-code #\b)))
|
||||
(is (string= (textarea-value a) "a
|
||||
b"))))
|
||||
|
||||
(test textarea-cursor-up-down
|
||||
"Cursor moves between lines maintaining column position."
|
||||
(let ((a (make-textarea :value "abc
|
||||
de
|
||||
fghi")))
|
||||
(setf (textarea-cursor-row a) 1)
|
||||
(setf (textarea-cursor-col a) 1)
|
||||
(handle-textarea-input a (make-key-event :key :up))
|
||||
(is (= (textarea-cursor-row a) 0))
|
||||
(is (= (textarea-cursor-col a) 1))
|
||||
(handle-textarea-input a (make-key-event :key :down))
|
||||
(is (= (textarea-cursor-row a) 1))
|
||||
(is (= (textarea-cursor-col a) 1))))
|
||||
|
||||
(test textarea-cursor-up-down-bounds
|
||||
"Cursor cannot move past first or last line."
|
||||
(let ((a (make-textarea :value "a
|
||||
b")))
|
||||
(handle-textarea-input a (make-key-event :key :up))
|
||||
(is (= (textarea-cursor-row a) 0))
|
||||
(setf (textarea-cursor-row a) 1)
|
||||
(handle-textarea-input a (make-key-event :key :down))
|
||||
(is (= (textarea-cursor-row a) 1))))
|
||||
|
||||
(test textarea-backspace-joins-lines
|
||||
"Backspace at start of a line joins with previous."
|
||||
(let ((a (make-textarea :value "hello
|
||||
world")))
|
||||
(setf (textarea-cursor-row a) 1)
|
||||
(setf (textarea-cursor-col a) 0)
|
||||
(handle-textarea-input a (make-key-event :key :backspace))
|
||||
(is (string= (textarea-value a) "helloworld"))))
|
||||
|
||||
(test textarea-undo
|
||||
"Ctrl+Z undoes the last edit."
|
||||
(let ((a (make-textarea)))
|
||||
(handle-textarea-input a (make-key-event :key :a :code (char-code #\a)))
|
||||
(handle-textarea-input a (make-key-event :key :z :ctrl t))
|
||||
(is (string= (textarea-value a) ""))))
|
||||
|
||||
(test textarea-undo-redo
|
||||
"Ctrl+Y redoes an undone edit."
|
||||
(let ((a (make-textarea)))
|
||||
(handle-textarea-input a (make-key-event :key :a :code (char-code #\a)))
|
||||
(handle-textarea-input a (make-key-event :key :z :ctrl t))
|
||||
(handle-textarea-input a (make-key-event :key :y :ctrl t))
|
||||
(is (string= (textarea-value a) "a"))))
|
||||
|
||||
;; ── Keybinding Tests ────────────────────────────────────────────
|
||||
;; These tests verify the keymap dispatch system works correctly
|
||||
;; when wired up. Note: dispatch-key-event is NOT called by the
|
||||
;; demo's event loop — users MUST call it explicitly in their own
|
||||
;; event loops if they want to use the defkeymap/dispatch-key-event
|
||||
;; system. See src/components/keybindings.lisp for details.
|
||||
;;
|
||||
;; Chords ((:ctrl+x :ctrl+s)) are not yet supported; only single
|
||||
;; key specs work. The *chord-timeout* variable and list-of-lists
|
||||
;; syntax are reserved for future implementation.
|
||||
|
||||
(test keymap-simple
|
||||
"A keymap dispatches to its handler on matching event."
|
||||
(let ((called nil))
|
||||
(setf (gethash :global *keymaps*)
|
||||
(make-keymap :name :global
|
||||
:bindings `((:ctrl+p . ,(lambda (e)
|
||||
(declare (ignore e))
|
||||
(setf called t))))))
|
||||
(is-true (dispatch-key-event (make-key-event :key :p :ctrl t)))
|
||||
(is-true called)))
|
||||
|
||||
(test keymap-no-match
|
||||
"Non-matching event returns nil."
|
||||
(let ((called nil))
|
||||
(setf (gethash :global *keymaps*)
|
||||
(make-keymap :name :global
|
||||
:bindings `((:ctrl+p . ,(lambda (e)
|
||||
(declare (ignore e))
|
||||
(setf called t))))))
|
||||
(is-false (dispatch-key-event (make-key-event :key :a)))
|
||||
(is-false called)))
|
||||
|
||||
(test keymap-fallback
|
||||
"Event not in local falls through to global."
|
||||
(let ((global-called nil))
|
||||
(setf (gethash :global *keymaps*)
|
||||
(make-keymap :name :global
|
||||
:bindings `((:ctrl+q . ,(lambda (e)
|
||||
(declare (ignore e))
|
||||
(setf global-called t))))))
|
||||
(dispatch-key-event (make-key-event :key :q :ctrl t))
|
||||
(is-true global-called)))
|
||||
|
||||
(test key-spec-simple
|
||||
"Keyword key-spec matches key+ctrl."
|
||||
(is-true (key-match-p :ctrl+p (make-key-event :key :p :ctrl t)))
|
||||
(is-false (key-match-p :ctrl+p (make-key-event :key :a :ctrl t)))
|
||||
(is-false (key-match-p :ctrl+p (make-key-event :key :p))))
|
||||
|
||||
(test key-spec-alt-modifier
|
||||
"Alt modifier is matched correctly."
|
||||
(is-true (key-match-p :alt+x (make-key-event :key :x :alt t)))
|
||||
(is-false (key-match-p :alt+x (make-key-event :key :x)))
|
||||
(is-false (key-match-p :alt+x (make-key-event :key :x :ctrl t))))
|
||||
|
||||
(test key-spec-shift-modifier
|
||||
"Shift modifier is matched correctly."
|
||||
(is-true (key-match-p :shift+tab (make-key-event :key :tab :shift t)))
|
||||
(is-false (key-match-p :shift+tab (make-key-event :key :tab))))
|
||||
|
||||
(test key-spec-plain
|
||||
"Plain key spec matches unmodified keys."
|
||||
(is-true (key-match-p :enter (make-key-event :key :enter)))
|
||||
(is-true (key-match-p :escape (make-key-event :key :escape)))
|
||||
(is-false (key-match-p :enter (make-key-event :key :escape))))
|
||||
|
||||
(test key-spec-list-form
|
||||
"List-form spec (:ctrl+p) matches same as keyword :ctrl+p."
|
||||
(is-true (key-match-p '(:ctrl+p) (make-key-event :key :p :ctrl t)))
|
||||
(is-false (key-match-p '(:ctrl+p) (make-key-event :key :a :ctrl t))))
|
||||
|
||||
(test dispatch-return-value-match
|
||||
"dispatch-key-event returns T on matching binding."
|
||||
(setf (gethash :global *keymaps*)
|
||||
(make-keymap :name :global
|
||||
:bindings `((:ctrl+p . ,(lambda (e) (declare (ignore e)))))))
|
||||
(is-true (dispatch-key-event (make-key-event :key :p :ctrl t))))
|
||||
|
||||
(test dispatch-return-value-no-match
|
||||
"dispatch-key-event returns NIL when no binding matches."
|
||||
(setf (gethash :global *keymaps*)
|
||||
(make-keymap :name :global
|
||||
:bindings `((:ctrl+p . ,(lambda (e) (declare (ignore e)))))))
|
||||
(is-false (dispatch-key-event (make-key-event :key :a))))
|
||||
|
||||
(test dispatch-empty-keymap
|
||||
"dispatch-key-event returns NIL on empty keymap."
|
||||
(setf (gethash :global *keymaps*) (make-keymap :name :global))
|
||||
(is-false (dispatch-key-event (make-key-event :key :a))))
|
||||
|
||||
(test dispatch-local-overrides-global
|
||||
"Local keymap takes priority over global."
|
||||
(let ((local-called nil) (global-called nil))
|
||||
(setf (gethash :local *keymaps*)
|
||||
(make-keymap :name :local
|
||||
:bindings `((:ctrl+p . ,(lambda (e)
|
||||
(declare (ignore e))
|
||||
(setf local-called t))))))
|
||||
(setf (gethash :global *keymaps*)
|
||||
(make-keymap :name :global
|
||||
:bindings `((:ctrl+p . ,(lambda (e)
|
||||
(declare (ignore e))
|
||||
(setf global-called t))))))
|
||||
(is-true (dispatch-key-event (make-key-event :key :p :ctrl t)))
|
||||
(is-true local-called)
|
||||
(is-false global-called)))
|
||||
|
||||
(test dispatch-multiple-bindings
|
||||
"dispatch-key-event finds the right binding among many."
|
||||
(let ((called nil))
|
||||
(setf (gethash :global *keymaps*)
|
||||
(make-keymap :name :global
|
||||
:bindings `((:ctrl+a . (lambda (e) (declare (ignore e))))
|
||||
(:ctrl+b . (lambda (e) (declare (ignore e))))
|
||||
(:ctrl+c . ,(lambda (e)
|
||||
(declare (ignore e))
|
||||
(setf called t)))
|
||||
(:ctrl+d . (lambda (e) (declare (ignore e)))))))
|
||||
(is-true (dispatch-key-event (make-key-event :key :c :ctrl t)))
|
||||
(is-true called)))
|
||||
|
||||
(test defkeymap-macro
|
||||
"defkeymap macro registers a keymap."
|
||||
(let ((called nil))
|
||||
(eval `(defkeymap :global
|
||||
(:ctrl+q ,(lambda (e) (declare (ignore e)) (setf called t)))))
|
||||
(dispatch-key-event (make-key-event :key :q :ctrl t))
|
||||
(is-true called)))
|
||||
|
||||
(test defkeymap-macro-with-list-spec
|
||||
"defkeymap macro works with list-form specs."
|
||||
(let ((called nil))
|
||||
(eval `(defkeymap :global
|
||||
((:ctrl+w) ,(lambda (e) (declare (ignore e)) (setf called t)))))
|
||||
(dispatch-key-event (make-key-event :key :w :ctrl t))
|
||||
(is-true called)))
|
||||
|
||||
;; cleanup after keybinding tests
|
||||
(test keybinding-cleanup-global
|
||||
"Clean up global keymap after testing."
|
||||
(remhash :global *keymaps*)
|
||||
(remhash :local *keymaps*)
|
||||
(is-false (gethash :global *keymaps*))
|
||||
(is-false (gethash :local *keymaps*)))
|
||||
|
||||
@@ -11,14 +11,6 @@
|
||||
while pos
|
||||
do (setf start (1+ pos))))
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; Global variables for rendering pipeline (set by application)
|
||||
;;; ---------------------------------------------------------------------------
|
||||
(defvar *current-backend* nil
|
||||
"The active backend used for rendering.")
|
||||
(defvar *current-theme* nil
|
||||
"The active theme used for semantic color resolution.")
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; Key event struct
|
||||
;;; ---------------------------------------------------------------------------
|
||||
@@ -286,6 +278,24 @@ key event rather than blocking indefinitely."
|
||||
(make-key-event :key :unknown
|
||||
:raw (format nil "~C~C" #\Esc ch)))))))))
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; UTF-8 decoder
|
||||
;;; ---------------------------------------------------------------------------
|
||||
(defun utf8-decode (bytes)
|
||||
"Decode a UTF-8 byte sequence to a code point, or nil if invalid."
|
||||
(case (length bytes)
|
||||
(2 (let ((b0 (first bytes)) (b1 (second bytes)))
|
||||
(when (and (<= #xc2 b0 #xdf) (<= #x80 b1 #xbf))
|
||||
(+ (ash (logand b0 #x1f) 6) (logand b1 #x3f)))))
|
||||
(3 (let ((b0 (first bytes)) (b1 (second bytes)) (b2 (third bytes)))
|
||||
(when (and (<= #xe0 b0 #xef) (<= #x80 b1 #xbf) (<= #x80 b2 #xbf))
|
||||
(+ (ash (logand b0 #x0f) 12) (ash (logand b1 #x3f) 6) (logand b2 #x3f)))))
|
||||
(4 (let ((b0 (first bytes)) (b1 (second bytes)) (b2 (third bytes)) (b3 (fourth bytes)))
|
||||
(when (and (<= #xf0 b0 #xf4) (<= #x80 b1 #xbf) (<= #x80 b2 #xbf) (<= #x80 b3 #xbf))
|
||||
(+ (ash (logand b0 #x07) 18) (ash (logand b1 #x3f) 12)
|
||||
(ash (logand b2 #x3f) 6) (logand b3 #x3f)))))
|
||||
(t nil)))
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; Top-level event reader
|
||||
;;; ---------------------------------------------------------------------------
|
||||
@@ -315,6 +325,29 @@ key event rather than blocking indefinitely."
|
||||
(let ((ch (code-char b)))
|
||||
(make-key-event :key (intern (string (string-upcase ch)) :keyword)
|
||||
:code b)))
|
||||
;; UTF-8 multi-byte sequence
|
||||
((>= b #xc2)
|
||||
(let* ((n (cond ((<= b #xdf) 2)
|
||||
((<= b #xef) 3)
|
||||
(t 4)))
|
||||
(bytes (list b)))
|
||||
(loop for i from 1 below n
|
||||
for b2 = (multiple-value-bind (byte reason)
|
||||
(read-raw-byte :timeout 0.5)
|
||||
(declare (ignore reason))
|
||||
byte)
|
||||
while (and b2 (<= #x80 b2 #xbf))
|
||||
do (push b2 bytes))
|
||||
(setf bytes (nreverse bytes))
|
||||
(if (= (length bytes) n)
|
||||
(let ((cp (utf8-decode bytes)))
|
||||
(if cp
|
||||
(make-key-event :key :codepoint :code cp
|
||||
:raw (map 'string #'code-char bytes))
|
||||
(make-key-event :key :unknown
|
||||
:raw (map 'string #'code-char bytes))))
|
||||
(make-key-event :key :unknown
|
||||
:raw (map 'string #'code-char bytes)))))
|
||||
(t
|
||||
(make-key-event :key :unknown :code b :raw (string (code-char b)))))))
|
||||
|
||||
|
||||
@@ -31,20 +31,22 @@
|
||||
|
||||
(defun render-screen (root backend)
|
||||
"Render the component tree ROOT using BACKEND.
|
||||
Computes layout for dirty branches, calls render on each component,
|
||||
and wraps output in synchronized updates. Uses the actual terminal
|
||||
dimensions from BACKEND rather than hardcoded defaults."
|
||||
Computes layout at the root level, then traverses children
|
||||
rendering each at their pre-computed positions. Uses the actual
|
||||
terminal dimensions from BACKEND rather than hardcoded defaults."
|
||||
(multiple-value-bind (w h) (backend-size backend)
|
||||
(begin-sync backend)
|
||||
(render-node root backend w h)
|
||||
(compute-layout (component-layout-node root) w h)
|
||||
(render-node root backend)
|
||||
(end-sync backend)))
|
||||
|
||||
(defun render-node (node backend w h)
|
||||
"Render a component NODE and its children."
|
||||
(compute-layout (component-layout-node node) w h)
|
||||
(defun render-node (node backend)
|
||||
"Render a component NODE and its children.
|
||||
Layout is computed once at the root by render-screen, so children
|
||||
just render at their pre-computed positions."
|
||||
(render node backend)
|
||||
(dolist (child (component-children node))
|
||||
(render-node child backend w h)))
|
||||
(render-node child backend)))
|
||||
|
||||
(defun available-width (component)
|
||||
"Return the available width for COMPONENT (or 80 as default)."
|
||||
|
||||
@@ -35,7 +35,7 @@ color roles resolve to hex at SGR generation time."
|
||||
(getf preset :dark)
|
||||
(getf preset :light)))
|
||||
;; Populate backend theme color map
|
||||
(theme-map (symbol-value (find-symbol "*THEME-COLORS*" :cl-tty.backend))))
|
||||
(theme-map cl-tty.backend:*theme-colors*))
|
||||
;; Set theme colors
|
||||
(loop for (role hex) on colors by #'cddr
|
||||
do (setf (theme-color theme role) hex)
|
||||
|
||||
@@ -58,6 +58,29 @@
|
||||
(is (eql #\A (cell-char (aref cells 6 6))) "inside scissor draws")
|
||||
(is (eql #\space (cell-char (aref cells 1 1))) "outside scissor is clipped"))))
|
||||
|
||||
(test flush-different-sized-fbs-handles-edge-cells
|
||||
"flush-framebuffer handles prev and curr framebuffers of different sizes
|
||||
without errors. Cells in the overlapping region are diffed; cells outside
|
||||
the overlap are silently ignored (no crash on array bounds)."
|
||||
(let* ((small-fb (make-framebuffer 5 5))
|
||||
(large-fb (make-framebuffer 10 10))
|
||||
(be (make-simple-backend :output-stream (make-string-output-stream))))
|
||||
;; Set a cell in the small one for a change in the overlapping region
|
||||
(setf (aref small-fb 0 0) (make-cell :char #\X :fg :red))
|
||||
;; diff-framebuffers should use min dimensions (5,5) — no crash
|
||||
(let ((changes (diff-framebuffers small-fb large-fb)))
|
||||
(is (= 1 (length changes)) "one cell changed in overlap region"))
|
||||
;; flush-framebuffer should also handle different sizes gracefully
|
||||
(let ((changed (flush-framebuffer small-fb large-fb be)))
|
||||
(is (= 1 changed) "flush reports 1 changed cell"))
|
||||
;; Reverse: large as prev, small as curr — extra cells in prev ignored
|
||||
(setf (aref large-fb 9 9) (make-cell :char #\Y :fg :blue))
|
||||
(let ((changes2 (diff-framebuffers large-fb small-fb)))
|
||||
(is (= 1 (length changes2)) "only overlapping region diffed (smaller bounds)"))
|
||||
;; flush should also work with shrunk framebuffer
|
||||
(let ((changed2 (flush-framebuffer large-fb small-fb be)))
|
||||
(is (= 1 changed2) "flush with shrunk fb reports 1 changed cell"))))
|
||||
|
||||
(test flush-fb-copies-to-backend
|
||||
(let* ((real-be (make-simple-backend :output-stream (make-string-output-stream)))
|
||||
(fb (make-framebuffer-backend)))
|
||||
|
||||
@@ -36,6 +36,28 @@
|
||||
(is (= (mouse-event-x e) 10))
|
||||
(is (= (mouse-event-y e) 5))))
|
||||
|
||||
;; ── UTF-8 Decode Tests ──────────────────────────────────────────
|
||||
|
||||
(test utf8-decode-latin1-supplement
|
||||
"0xC3 0xA9 (é) decodes to code point 233."
|
||||
(is (= (cl-tty.input:utf8-decode '(#xc3 #xa9)) 233)))
|
||||
|
||||
(test utf8-decode-euro-sign
|
||||
"0xE2 0x82 0xAC (€) decodes to code point 8364."
|
||||
(is (= (cl-tty.input:utf8-decode '(#xe2 #x82 #xac)) 8364)))
|
||||
|
||||
(test utf8-decode-emoji
|
||||
"0xF0 0x9F 0x92 0xA9 (💩) decodes to code point 128169."
|
||||
(is (= (cl-tty.input:utf8-decode '(#xf0 #x9f #x92 #xa9)) 128169)))
|
||||
|
||||
(test utf8-decode-invalid-short
|
||||
"Invalid byte 0x80 alone returns nil."
|
||||
(is-false (cl-tty.input:utf8-decode '(#x80))))
|
||||
|
||||
(test utf8-decode-invalid-overlong
|
||||
"Overlong 2-byte sequence 0xC0 0x80 returns nil."
|
||||
(is-false (cl-tty.input:utf8-decode '(#xc0 #x80))))
|
||||
|
||||
;; ── TextInput Tests ─────────────────────────────────────────────
|
||||
|
||||
(test text-input-empty
|
||||
|
||||
Reference in New Issue
Block a user