% Copyright 2026 Adi Edelhaus
%
% terminaltrees is licensed under the LaTeX Project Public
% License, version 1.3c or any later version.
%
% Maintenance status: maintained
% Current maintainer: Adi Edelhaus
% Issue reports: https://github.com/edel67/terminaltrees/issues
%
% The files comprising this work are listed in manifest.txt.
% See LICENSE for the full license terms.

\NeedsTeXFormat{LaTeX2e}
\ProvidesPackage{terminaltrees}[2026/10/06 v1.0.0 Child-centered terminal trees]
\RequirePackage[edges]{forest}
\RequirePackage{needspace}

\forestset{
  declare count register={terminaltrees index},
  declare dimen register={leaf pitch},
  declare dimen register={terminaltrees fork},
  declare dimen register={terminaltrees bend},
  declare dimen register={terminaltrees arrow inset},
  declare toks register={roundness},
  declare dimen={terminaltrees radius}{0pt},
  terminal tree/.style={
    baseline=(current bounding box.north),
    leaf pitch=0pt,
    roundness=1,
    for tree={
      anchor=center,
      align=center,
      parent anchor=south,
      child anchor=north,
      edge={thin, ->, >=stealth},
      forked edge',
    },
    where n children=0{tier=terminal}{},
    before drawing tree={
      % Leave room for the arrow tip so the shaft does not reverse direction.
      TeX={% Discard whitespace emitted when PGF first instantiates the arrow.
        \setbox0=\hbox\bgroup
          \tikzset{thin}\pgfarrowtotallength{stealth}%
          % Anchors extend beyond the label boxes at both ends.
          \edef\pgfmathresult{\the\csname pgf@xa\endcsname}%
          \pgfmathparse{\pgfmathresult+2*\pgfkeysvalueof{/pgf/outer ysep}}%
          \edef\pgfmathresult{\egroup
            \noexpand\forestset{terminaltrees arrow inset=\pgfmathresult pt}}%
          \pgfmathresult
      },
      % Each parent must use its children's final positions.
      terminaltrees index=0,
      for leaves={
        x/.pgfmath={terminaltrees_index()},
        terminaltrees index+=1,
      },
      for tree children-first={
        if n children=0{}{x/.pgfmath={(x("!1")+x("!l"))/2}},
      },
      % Keep every label inside its subtree's columns, including padding.
      % Child-centered parents need clearance to the nearer column boundary.
      for tree={
        leaf pitch/.pgfmath={max(leaf_pitch(),
          (max(abs(min_x()),abs(max_x()))+s_sep()/2)/
          min(x()-x("!{first leaf'}")+0.5,x("!{last leaf'}")-x()+0.5))},
      },
      for tree={x/.pgfmath={x()*leaf_pitch()}},
      % Keep forks and bends in the shared clearance above the child labels.
      for tree={
        if n children=0{}{%
          terminaltrees fork/.pgfmath={
            (y()+min_y()-aggregate_max("y()+max_y()","children"))/2},
          % Use one radius for sibling branches so their departure curves coincide.
          terminaltrees bend/.pgfmath={max(0,min(
            roundness()*terminaltrees_fork(),
            terminaltrees_fork()-terminaltrees_arrow_inset()))},
          for children={
            if={x()!=x("!u")}{
              terminaltrees bend/.pgfmath={min(terminaltrees_bend(),abs(x()-x("!u"))/2)},
            }{},
          },
          for children={
            fork sep/.register=terminaltrees fork,
            terminaltrees radius/.pgfmath={ifthenelse(x()==x("!u"),0,terminaltrees_bend())},
            edge+={rounded corners=\forestoption{terminaltrees radius}},
          },
        },
      },
    },
  },
}

\ExplSyntaxOn
\msg_new:nnn { terminaltrees } { invalid-roundness }
  { roundness~must~be~a~unitless~number~from~0~to~1. }
\forestset{
  roundness/.code={
    \pgfmathparse{#1}
    \ifpgfmathunitsdeclared
      \msg_fatal:nn { terminaltrees } { invalid-roundness }
    \fi
    \fp_compare:nTF {\pgfmathresult >= 0 && \pgfmathresult <= 1}
      { \forestrlet{roundness}\pgfmathresult }
      { \msg_fatal:nn { terminaltrees } { invalid-roundness } }
  },
}
\box_new:N \l__terminaltrees_box
\dim_new:N \g__terminaltrees_font_dim
\dim_new:N \l__terminaltrees_min_font_dim
\dim_new:N \l__terminaltrees_height_dim
\fp_new:N \l__terminaltrees_scale_fp
\keys_define:nn { terminaltrees / fit }
  { minimum~label~size .dim_set:N = \l__terminaltrees_min_font_dim }
\msg_new:nnn { terminaltrees } { too-small }
  { Fitting~requires~#1pt~node~text,~below~the~#2~minimum.~
    Increase~an~undersized~node~font,~wrap~labels,~or~split~the~tree~manually;~
    automatic~pagination~is~not~implemented. }
\msg_new:nnn { terminaltrees } { invalid-context }
  { fittedterminaltree~requires~normal~document~flow,~not~a~box~or~float. }
\msg_new:nnn { terminaltrees } { invalid-size }
  { The~minimum~label~size~and~available~page~dimensions~must~be~positive. }
\cs_new_protected:Npn \__terminaltrees_record_font:
  { \dim_gset:Nn \g__terminaltrees_font_dim
      { \dim_min:nn { \g__terminaltrees_font_dim } { \f@size pt } } }
\cs_new_protected:Npn \__terminaltrees_fit:nn #1#2
  {
    \mode_if_inner:TF
      { \msg_fatal:nn { terminaltrees } { invalid-context } }
      {
        \par
        % Use the document's \footnotesize as the default minimum.
        \group_begin:
          \footnotesize
          \dim_gset:Nn \g__terminaltrees_font_dim { \f@size pt }
        \group_end:
        \dim_set_eq:NN \l__terminaltrees_min_font_dim \g__terminaltrees_font_dim
        \keys_set:nn { terminaltrees / fit } {#1}
        \dim_set:Nn \l__terminaltrees_height_dim { \textheight - \baselineskip }
        \bool_lazy_or:nnTF
          { \dim_compare_p:n { \l__terminaltrees_min_font_dim <= 0pt } }
          { \dim_compare_p:n { \dim_min:nn {\linewidth}
                {\l__terminaltrees_height_dim} <= 0pt } }
          { \msg_fatal:nn { terminaltrees } { invalid-size } }
          {
            % Typeset once to avoid evaluating labels twice.
            % The node hook runs after TikZ selects its font.
            \dim_gset:Nn \g__terminaltrees_font_dim { \maxdimen }
            \hbox_set:Nn \l__terminaltrees_box
              { \begin{forest}
                  terminal~tree,
                  for~tree={execute~at~begin~node={\__terminaltrees_record_font:}},
                  #2
                \end{forest} }
            \fp_set:Nn \l__terminaltrees_scale_fp
              % Round down at TeX's 1/65536 scale precision to avoid overflow.
              { floor(65536 * min(1,
                  \dim_to_fp:n {\linewidth} /
                    \dim_to_fp:n {\box_wd:N \l__terminaltrees_box},
                  \dim_to_fp:n {\l__terminaltrees_height_dim} /
                    \dim_to_fp:n {\box_ht:N \l__terminaltrees_box +
                                  \box_dp:N \l__terminaltrees_box})) / 65536 }
            \fp_compare:nTF
              { \l__terminaltrees_scale_fp * \dim_to_fp:n {\g__terminaltrees_font_dim}
                  < \dim_to_fp:n {\l__terminaltrees_min_font_dim} - 0.0001 }
              { \msg_fatal:nnxx { terminaltrees } { too-small }
                  { \fp_eval:n { round(\l__terminaltrees_scale_fp *
                      \dim_to_fp:n {\g__terminaltrees_font_dim},2) } }
                  { \dim_use:N \l__terminaltrees_min_font_dim } }
              {
                \box_scale:Nnn \l__terminaltrees_box
                  {\fp_use:N \l__terminaltrees_scale_fp}
                  {\fp_use:N \l__terminaltrees_scale_fp}
                % Reserve space at the chosen scale instead of shrinking to
                % the remaining page space.
                \Needspace*{\dim_eval:n {\box_ht:N \l__terminaltrees_box +
                  \box_dp:N \l__terminaltrees_box + \baselineskip}}
                \noindent\makebox[\linewidth][c]{\box_use:N \l__terminaltrees_box}\par
              }
          }
      }
  }
% Parenthesized options cannot consume Forest's opening root bracket.
\NewDocumentEnvironment{fittedterminaltree}{ D(){} +b }
  { \__terminaltrees_fit:nn {#1}{#2} } {}
\ExplSyntaxOff
\endinput
