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%.
154 lines
4.8 KiB
Org Mode
154 lines
4.8 KiB
Org Mode
#+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~).
|