prose: split scrollbox-tabbar.org prose into per-module org files

Distribute the literate prose from the old combined scrollbox-tabbar.org
into three individual module org files:

- scrollbox.org: ScrollBox class, render, scrollbars, bug fixes,
  plus the combined test suite (tangles scrollbox-tabbar-tests.lisp)
- tabbar.org: TabBar class, navigation, keyboard handler, render
- container-package.org: Package definition and exports

The old scrollbox-tabbar.org is retained as a documentation archive
with all code blocks set to :tangle no and a redirecting note.

Fixes the draw-scrollbars code block to use the post-bugfix version
(with layout-node origin offset ox/oy), matching the working code.
All 13 test suites pass at 100%.
This commit is contained in:
Hermes Agent
2026-05-12 18:06:07 +00:00
parent a061d60898
commit 668966380e
7 changed files with 687 additions and 453 deletions

View File

@@ -1,8 +1,10 @@
(in-package #:cl-tty.container)
(defclass tab-bar (dirty-mixin)
((tabs :initform nil :initarg :tabs :accessor tab-bar-tabs :type list)
(active :initform nil :initarg :active :accessor tab-bar-active)
((tabs :initform nil :initarg :tabs
:accessor tab-bar-tabs :type list)
(active :initform nil :initarg :active
:accessor tab-bar-active)
(layout-node :initform (make-layout-node) :accessor tab-bar-layout-node)
(focusable :initform t :accessor tab-bar-focusable)))
@@ -10,38 +12,71 @@
(make-instance 'tab-bar :tabs (or tabs nil) :active active))
(defun tab-bar-add (tb id title)
(setf (tab-bar-tabs tb) (nconc (tab-bar-tabs tb) (list (list :id id :title title))))
(unless (tab-bar-active tb) (setf (tab-bar-active tb) id)) id)
"Add a tab with ID and TITLE. Sets as active if first tab."
(setf (tab-bar-tabs tb)
(nconc (tab-bar-tabs tb) (list (list :id id :title title))))
(unless (tab-bar-active tb)
(setf (tab-bar-active tb) id))
id)
(defmethod component-layout-node ((tb tab-bar)) (tab-bar-layout-node tb))
(defmethod component-layout-node ((tb tab-bar))
(tab-bar-layout-node tb))
(defun tab-bar-next (tb)
(let* ((tabs (tab-bar-tabs tb)) (current (tab-bar-active tb))
(ids (mapcar (lambda (tab) (getf tab :id)) tabs)) (pos (position current ids)))
(when pos (let ((next (nth (mod (1+ pos) (length ids)) ids)))
(setf (tab-bar-active tb) next) (mark-dirty tb)))))
"Move to next tab."
(let* ((tabs (tab-bar-tabs tb))
(current (tab-bar-active tb))
(ids (mapcar (lambda (tab) (getf tab :id)) tabs))
(pos (position current ids)))
(when pos
(let ((next (nth (mod (1+ pos) (length ids)) ids)))
(setf (tab-bar-active tb) next)
(mark-dirty tb)))))
(defun tab-bar-prev (tb)
(let* ((tabs (tab-bar-tabs tb)) (current (tab-bar-active tb))
(ids (mapcar (lambda (tab) (getf tab :id)) tabs)) (pos (position current ids)))
(when pos (let ((prev (nth (mod (1- pos) (length ids)) ids)))
(setf (tab-bar-active tb) prev) (mark-dirty tb)))))
"Move to previous tab."
(let* ((tabs (tab-bar-tabs tb))
(current (tab-bar-active tb))
(ids (mapcar (lambda (tab) (getf tab :id)) tabs))
(pos (position current ids)))
(when pos
(let ((prev (nth (mod (1- pos) (length ids)) ids)))
(setf (tab-bar-active tb) prev)
(mark-dirty tb)))))
(defun tab-bar-select (tb id) (setf (tab-bar-active tb) id) (mark-dirty tb))
(defun tab-bar-select (tb id)
"Select a tab by ID."
(setf (tab-bar-active tb) id)
(mark-dirty tb))
(defun tab-bar-handle-key (tb event)
(case (key-event-key event) (:left (tab-bar-prev tb) t) (:right (tab-bar-next tb) t) (t nil)))
"Handle a key-event on a TabBar. Returns T if handled."
(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)
(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))
(active-id (tab-bar-active tb)) (tabs (tab-bar-tabs tb)) (x-pos x))
(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))
(active-id (tab-bar-active tb))
(tabs (tab-bar-tabs tb))
(x-pos x))
(dolist (tab tabs)
(let* ((id (getf tab :id)) (title (getf tab :title))
(label (format nil " ~A " title)) (label-len (length label))
(let* ((id (getf tab :id))
(title (getf tab :title))
(label (format nil " ~A " title))
(label-len (length label))
(is-active (eql id active-id))
(fg (if is-active :accent :text-muted))
(bg (if is-active :background-element nil)))
(when (>= (+ x-pos label-len 2) w) (draw-text backend x-pos y "..." :text-muted nil) (return))
(draw-text backend x-pos y label fg bg) (incf x-pos (+ label-len 2)))))
(values))
;; Check if tab fits
(when (>= (+ x-pos label-len 2) (+ x w))
(draw-text backend x-pos y "..." :text-muted nil)
(return))
;; Draw tab
(draw-text backend x-pos y label fg bg)
(incf x-pos (+ label-len 2))))
(values)))