#+TITLE: TabBar #+STARTUP: content #+FILETAGS: :cl-tty:container: * Overview TabBar handles horizontal tab navigation with keyboard support. Tabs are rendered as labeled items; the active tab is highlighted. ~tab-bar~ inherits ~dirty-mixin~ and implements the component protocol (~render~, ~component-layout-node~) so it integrates with the rendering pipeline and layout engine. ** Contract ~(tab-bar &key tabs active-tab)~ → tab-bar TABS is a list of ~(id title)~ plists. ~(tab-bar-active tb)~ / ~(setf tab-bar-active)~ — currently active tab id. ~(tab-bar-tabs tb)~ — list of tab plists. ~(tab-bar-add tb id title)~ — add a tab. Returns the tab id. ~(render ((tb tab-bar) backend))~ — renders tab row, active tab highlighted, inactive tabs dimmed. * Implementation ** TabBar class ~tab-bar~ stores a list of tab plists ~((:id :tab1 :title "One") ...)~ and the currently active tab id. ~tab-bar-add~ creates a new tab with the given id and title, returns the id. #+BEGIN_SRC lisp :tangle ../src/components/tabbar.lisp (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) (layout-node :initform (make-layout-node) :accessor tab-bar-layout-node) (focusable :initform t :accessor tab-bar-focusable))) (defun make-tab-bar (&key tabs active) (make-instance 'tab-bar :tabs (or tabs nil) :active active)) (defun tab-bar-add (tb id title) "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) #+END_SRC ** TabBar: component protocol #+BEGIN_SRC lisp :tangle ../src/components/tabbar.lisp (defmethod component-layout-node ((tb tab-bar)) (tab-bar-layout-node tb)) #+END_SRC ** TabBar: navigation ~tab-bar-next~ and ~tab-bar-prev~ cycle through tabs. ~tab-bar-select~ activates a tab by id. ~tab-bar-handle-key~ dispatches key events (Left/Right to navigate, optional Enter to select). #+BEGIN_SRC lisp :tangle ../src/components/tabbar.lisp (defun tab-bar-next (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) "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) "Select a tab by ID." (setf (tab-bar-active tb) id) (mark-dirty tb)) #+END_SRC ** TabBar: keyboard handler ~tab-bar-handle-key~ dispatches Left → previous tab, Right → next tab. Returns T if the key was handled, NIL otherwise (for composability with the keybinding system). #+BEGIN_SRC lisp :tangle ../src/components/tabbar.lisp (defun tab-bar-handle-key (tb event) "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))) #+END_SRC ** TabBar: rendering ~render~ iterates tabs, drawing each as ~[ Title ]~ with the active tab highlighted (bold, accent color) and inactive tabs dimmed. Tabs are separated by two spaces. The available width comes from the layout node. If tabs overflow, they are truncated with an ellipsis. #+BEGIN_SRC lisp :tangle ../src/components/tabbar.lisp (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)) (dolist (tab tabs) (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))) ;; 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))) #+END_SRC * Tests TabBar tests are part of the combined scrollbox-tabbar test suite defined in ~org/scrollbox.org~ (tangled to ~tests/scrollbox-tabbar-tests.lisp~).