Subject: bug#37667: 27.0.50; Tab Bar display problems with more than 5 tabs

From: Juri Linkov <[email-masked]>
Date: Tue, 22 Oct 2019 01:20:42 +0300

>> But maybe tab-button granularity is fine.  The only problem is that
>> I still studying the code to understand where to begin.  Could you suggest
>> in what function to implement all this scrolling?
>
> I think you will need a new function.  A tab-line is generated from a
> Lisp string, and we don't have code for hscrolling screen lines
> produced from strings (as opposed to from buffer text).
>
> The general idea is to identify the first (leftmost) tab you want to
> be visible, and then make changes in display_mode_line to start
> display from that tab.  I think the first part is the more complex of
> these two.  format-mode-line shows an example of how to generate
> display derived from Lisp and C strings without displaying anything;
> you could use that for finding the X coordinate (in it.current_x) of
> the beginning of the Nth tab's button.  Each click on the right fringe
> increases N, each click on the left decreases it.  The X coordinate
> you compute is the value to use for it.first_visible_x in
> display_mode_line.

This is so overwhelming that I'm still struggling with understanding the
details of the display engine.  If hscrolling was already implemented
for the header-line, we could just copy the existing solution to the
tab-line.

So I looked how packages cope with the requirement of keeping hscrolling
of the header-line in sync with hscrolling of the buffer, and found that
the 'proced' package uses so simple solution that we could just do
the same.  This is what it does:

(defun proced-header-line ()
  "Return header line for Proced buffer."
  (list (propertize " "
                    'display
                    (list 'space :align-to
                          (line-number-display-width 'columns)))
        (if (<= (window-hscroll) (length proced-header-line))
            (replace-regexp-in-string ;; preserve text properties
             "\\(%\\)" "\\1\\1"
             (substring proced-header-line (window-hscroll))))))

i.e. it just cuts scrolled content from the beginning.

Adapting the same idea to the tab-line provides the workable and
reliable solution with just a dozen lines of Lisp code.  You can
evaluate this, and everything works smoothly:

(advice-add 'tab-line-format :around
            (lambda (orig-fun)
              (let ((tabs (funcall orig-fun))
                    (hscroll (window-parameter nil 'hscroll)))
                (if hscroll (nthcdr hscroll tabs) tabs)))
            '((name . tab-line-format-hscroll)))

(defun tab-line-hscroll (&optional arg window)
  (let* ((hscroll (window-parameter window 'hscroll)))
    (set-window-parameter
     window 'hscroll (max 0 (min (+ (or hscroll 0) (or arg 1))
                                 (1- (length (funcall tab-line-tabs-function))))))))

(defun tab-line-hscroll-left (&optional arg mouse-event)
  (interactive (list current-prefix-arg last-nonmenu-event))
  (tab-line-hscroll arg (and (listp mouse-event) (posn-window (event-start mouse-event))))
  (force-mode-line-update))

(defun tab-line-hscroll-right (&optional arg mouse-event)
  (interactive (list current-prefix-arg last-nonmenu-event))
  (tab-line-hscroll (- (or arg 1)) (and (listp mouse-event) (posn-window (event-start mouse-event))))
  (force-mode-line-update))

(global-set-key [tab-line mouse-4] 'tab-line-hscroll-left)
(global-set-key [tab-line mouse-5] 'tab-line-hscroll-right)
(global-set-key [tab-line wheel-up] 'tab-line-hscroll-left)
(global-set-key [tab-line wheel-down] 'tab-line-hscroll-right)

Thus mouse-wheel hscrolls the tab-line exactly like mouse-wheel
hscrolls tabs in Firefox.

Also these two commands can be used via M-x, and can be bound
to some key that could used via a transitive keymap, e.g.
some prefix initiates a key sequence, then consecutive
<right> keys continue hscrolling.

Later advice-add will be moved into the body of tab-line-format.