\version "2.23.10"

#(define (get-markup-width grob text)
  (let* ((layout (ly:grob-layout grob))
         (props (ly:grob-alist-chain
                 grob
                 (ly:output-def-lookup layout 'text-font-defaults))))
    (interval-length
     (ly:stencil-extent
      (interpret-markup layout props (markup text))
      X))))

#(define (text-spanner-minimum-length grob padding)
  (let* ((details (ly:grob-property grob 'bound-details))
         (lprops (assoc-get 'left details '()))
         (ltext (assoc-get 'text lprops '()))
         (lwidth (get-markup-width grob ltext))
         (rprops (assoc-get 'right details '()))
         (rtext (assoc-get 'text rprops '()))
         (rwidth (get-markup-width grob rtext))
         (minlength (+ lwidth rwidth padding)))
    ;; (pretty-print (list lwidth rwidth minlength))
    (ly:grob-set-property! grob 'minimum-length minlength)
    ;; TODO ideally we could set a property `minimum-length-before-break'
    (ly:grob-set-property! grob 'minimum-length-after-break rwidth)))

transitionSpanner =
#(define-music-function
   (padding from to)
   ((number? 2) markup? markup?)
   (_i "Set up a TextSpanner to indicate transition from one technique to another.
        Spanner is forced to a @code{minimum-length} of the combined widths of
        the markups @var{from} and @var{to} plus the extra value of @var{padding}.
        @var{to} markup is right-aligned.")
   ;; TODO recalculate minimum-length when broken, otherwise individual parts are stretched unnecessarily
   ;; PROBLEM: springs-and-rods has already run by the time the spanner is broken
   #{
     \override TextSpanner.style = #'dashed-line
     \override TextSpanner.dash-fraction = 0.35
     \override TextSpanner.dash-period = 1.2
     \override TextSpanner.bound-details.left.text = \markup \concat { #from \hspace #0.5 }
     \override TextSpanner.bound-details.right.text = \markup \halign #RIGHT \concat { \hspace #0.5 #to }
     \override TextSpanner.bound-details.left-broken.text = ##f
     \override TextSpanner.bound-details.right-broken.text = ##f
     \override TextSpanner.bound-details.left.padding = #0
     \override TextSpanner.bound-details.left.attach-dir = #CENTER
     \override TextSpanner.bound-details.right.padding = #0
     \override TextSpanner.bound-details.right.attach-dir = #RIGHT
     \override TextSpanner.bound-details.left.stencil-offset = #'(0 . -0.5)
     \override TextSpanner.bound-details.right.stencil-offset = #'(0 . -0.5)
     \override TextSpanner.bound-details.right.arrow = ##t
     \override TextSpanner.before-line-breaking = #(lambda (grob) (text-spanner-minimum-length grob padding))
     \override TextSpanner.springs-and-rods = #ly:spanner::set-spacing-rods
   #})

%{ %% EXAMPLES AND TESTS
{
  c'1
  \once \transitionSpanner "sul pont" "sul tasto"
  \once \override TextSpanner.before-line-breaking = ##f
  c'4\startTextSpan^\markup \with-color #red \box \column { "without adjustment" "BAD" }
  bes'4
  bes'4
  f''4\stopTextSpan
  \repeat unfold 16 c'16
  \once \transitionSpanner "sul pont" "sul tasto"
  c'4\startTextSpan^\markup \with-color #blue \box \column { "with adjustment" "GOOD" }
  bes'4
  bes'4
  f''4\stopTextSpan
  c'1
  c'1
  \repeat unfold 16 c'16
  c'4 4
  \once \transitionSpanner "sul pont" "sul tasto"
  c'4\startTextSpan^\markup \with-color #red \box \column { "but too long" "when broken" "BAD" }
  bes'4
  \break
  bes'4
  f''4\stopTextSpan
  c'2
  \repeat unfold 16 c'16
  c'4 4
  \once \transitionSpanner "sul pont" "sul tasto"
  \once \override TextSpanner.before-line-breaking = ##f
  c'4\startTextSpan^\markup \with-color #blue \box \column { "without" "adjustment" "GOOD" }
  bes'4
  \break
  bes'4
  f''4\stopTextSpan
  c'2
  \repeat unfold 24 c'16
  2
}
%}
