% Package   : scaletextbullet -- Resize the \textbullet without changing its
% vertical center
% Copyright : 2024 (c) Oliver Beery <beeryoliver@gmail.com>
% CTAN      : https://ctan.org/pkg/scaletextbullet
% Repository: https://github.com/beeryoliver/scaletextbullet
% License   : The LaTeX Project Public License 1.3c

% LaTeX2e version 2023-11-01 added \IfExplAtLeastTF.
\NeedsTeXFormat{LaTeX2e}[2023-11-01]
\ProvidesExplPackage
  {scaletextbullet}
  {2024-11-12}
  {1.0.1}
  {Resize the \noexpand\textbullet without changing its vertical center.}

% l3kernel version 2023-10-10 added many 'e'-variants.
\IfExplAtLeastTF { 2023-10-10 } { }
  {
    \msg_new:nnn { scaletextbullet } { expl3-out-of-date }
      {
        The~ scaletextbullet~ package~ could~ not~ load.~
        This~ package~ requires~
        L3~ programming~ layer~ version~ 2023-10-10~ or~ later.
      }
    \msg_critical:nn { scaletextbullet } { expl3-out-of-date }
  }

\tl_new:N \l__scaletextbullet_factor_tl
\tl_set:Nn \l__scaletextbullet_factor_tl { 0.4 }

% Used to speed up \ScaleTextBullets{2}, \ScaleTextBullets{3}, and
% \ScaleTextBullets{4}.
\fp_const:Nn \c__scaletextbullet_ii_fp  { 2 ^ -0.5 }
\fp_const:Nn \c__scaletextbullet_iii_fp { 3 ^ -0.5 }
\fp_const:Nn \c__scaletextbullet_iv_fp  { 0.5 }

% Scratch variables
\int_new:N \l__scaletextbullet_tmp_int
\fp_new:N  \l__scaletextbullet_tmp_fp
\box_new:N \l__scaletextbullet_tmp_box

% Lowers the \textbullet to the baseline, scales it by a factor of #2, and
% then raises it back to the vertical center.
% I have referenced code by the great egreg:
% https://tex.stackexchange.com/questions/620507/how-to-resize-textbullet-without-the-bullet-moving-down
\dim_new:N \l__scaletextbullet_bottom_dim % bottom of \textbullet
\dim_new:N \l__scaletextbullet_center_dim % center of \textbullet
\cs_new_protected:Npn \__scaletextbullet_box_scale:Nn #1#2
  {
    \hbox_set:Nn #1 { \textbullet }
    \dim_set:Nn \l__scaletextbullet_bottom_dim
      { \box_ht:N #1 - \l__scaletextbullet_factor_tl \box_wd:N #1 }
    \dim_set:Nn \l__scaletextbullet_center_dim
      { ( \box_ht:N #1 + \l__scaletextbullet_bottom_dim ) / 2 }
    \hbox_set:Nn #1
      {
        \box_move_down:nn { \l__scaletextbullet_bottom_dim } { \box_use:N #1 }
      }
    \box_scale:Nnn #1 {#2} {#2}
    \hbox_set:Nn #1
      {
        \box_move_up:nn { \l__scaletextbullet_center_dim - 0.5 \box_ht:N #1 }
          { \box_use:N #1 }
      }
  }

\msg_new:nnn { scaletextbullet } { factor-out-of-bounds }
  { Invalid~ \iow_char:N \\textbullet~ factor~ '#1'~ \msg_line_context:. }
\msg_new:nnn { scaletextbullet } { scale-out-of-bounds }
  { Invalid~ scale~ factor~ '#1'~ \msg_line_context:. }
\msg_new:nnn { scaletextbullet } { count-out-of-bounds }
  {
    Invalid~ number~ of~ \iow_char:N \\textbullet s~ '#1'~ \msg_line_context:.
  }

% DOCUMENT COMMANDS

\NewDocumentCommand \SetTextBulletFactor { m }
  { \__scaletextbullet_set_factor:n {#1} }
\cs_new_protected:Npn \__scaletextbullet_set_factor:n #1
  {
    \fp_set:Nn \l__scaletextbullet_tmp_fp {#1}
    \fp_compare:nTF { \c_zero_fp < \l__scaletextbullet_tmp_fp <= \c_one_fp }
      {
        \tl_set:Ne \l__scaletextbullet_factor_tl
          { \fp_use:N \l__scaletextbullet_tmp_fp }
      }
      {
        \msg_error:nne { scaletextbullet } { factor-out-of-bounds }
          { \fp_use:N \l__scaletextbullet_tmp_fp }
      }
  }

\NewDocumentCommand \ScaleTextBullet { m }
  { \mode_leave_vertical: \__scaletextbullet_scaletextbullet:n {#1} }
\cs_new_protected:Npn \__scaletextbullet_scaletextbullet:n #1
  {
    \fp_set:Nn \l__scaletextbullet_tmp_fp {#1}
    \fp_compare:nNnTF \l__scaletextbullet_tmp_fp > \c_zero_fp
      {
        \__scaletextbullet_box_scale:Nn \l__scaletextbullet_tmp_box
          { \l__scaletextbullet_tmp_fp }
        \box_use_drop:N \l__scaletextbullet_tmp_box
      }
      {
        \fp_compare:nNnF \l__scaletextbullet_tmp_fp = \c_zero_fp
          {
            \msg_error:nne { scaletextbullet } { scale-out-of-bounds }
              { \fp_use:N \l__scaletextbullet_tmp_fp }
          }
      }
  }

\NewDocumentCommand \ScaleTextBullets { o m }
  {
    \mode_leave_vertical:
    \IfNoValueTF {#1}
      { \__scaletextbullet_scaletextbullets:n {#2} }
      { \__scaletextbullet_scaletextbullets:nn {#1} {#2} }
  }
\cs_new_protected:Npn \__scaletextbullet_scaletextbullets:n #1
  {
    \int_set:Nn \l__scaletextbullet_tmp_int {#1}
    \int_compare:nNnTF \l__scaletextbullet_tmp_int > 0
      {
        \int_case:nnF { \l__scaletextbullet_tmp_int }
          {
            { 1 }
            { \fp_set_eq:NN \l__scaletextbullet_tmp_fp \c_one_fp }
            { 2 }
            {
              \fp_set_eq:NN \l__scaletextbullet_tmp_fp
                \c__scaletextbullet_ii_fp
            }
            { 3 }
            {
              \fp_set_eq:NN \l__scaletextbullet_tmp_fp
                \c__scaletextbullet_iii_fp
            }
            { 4 }
            {
              \fp_set_eq:NN \l__scaletextbullet_tmp_fp
                \c__scaletextbullet_iv_fp
            }
          }
          {
            \fp_set:Nn \l__scaletextbullet_tmp_fp
              { \l__scaletextbullet_tmp_int ^ -0.5 }
          }
        \__scaletextbullet_box_scale:Nn \l__scaletextbullet_tmp_box
          { \l__scaletextbullet_tmp_fp }
        \prg_replicate:nn { \l__scaletextbullet_tmp_int }
          { \box_use:N \l__scaletextbullet_tmp_box }
      }
      {
        \int_compare:nNnF \l__scaletextbullet_tmp_int = 0
          {
            \msg_error:nnV { scaletextbullet } { count-out-of-bounds }
              \l__scaletextbullet_tmp_int
          }
      }
  }
\cs_new_protected:Npn \__scaletextbullet_scaletextbullets:nn #1#2
  {
    \fp_set:Nn \l__scaletextbullet_tmp_fp {#1}
    \fp_compare:nNnTF \l__scaletextbullet_tmp_fp > \c_zero_fp
      {
        \int_set:Nn \l__scaletextbullet_tmp_int {#2}
        \int_compare:nNnTF \l__scaletextbullet_tmp_int > 0
          {
            \__scaletextbullet_box_scale:Nn \l__scaletextbullet_tmp_box
              { \l__scaletextbullet_tmp_fp }
            \prg_replicate:nn { \l__scaletextbullet_tmp_int }
              { \box_use:N \l__scaletextbullet_tmp_box }
          }
          {
            \int_compare:nNnF \l__scaletextbullet_tmp_int = 0
              {
                \msg_error:nnV { scaletextbullet } { count-out-of-bounds }
                  \l__scaletextbullet_tmp_int
              }
          }
      }
      {
        \fp_compare:nNnF \l__scaletextbullet_tmp_fp = \c_zero_fp
          {
            \msg_error:nne { scaletextbullet } { scale-out-of-bounds }
              { \fp_use:N \l__scaletextbullet_tmp_fp }
          }
      }
  }

\NewDocumentCommand \scaletextbulletdebug { }
  { \mode_leave_vertical: \__scaletextbullet_debug: }
% I have referenced code by the great egreg:
% https://tex.stackexchange.com/questions/620507/how-to-resize-textbullet-without-the-bullet-moving-down
\cs_new_protected:Npn \__scaletextbullet_debug:
  {
    \int_step_inline:nn { 15 }
      {
        \fp_set:Nn \l__scaletextbullet_tmp_fp { ##1 ^ -0.5 }
        \__scaletextbullet_box_scale:Nn \l__scaletextbullet_tmp_box
          { \l__scaletextbullet_tmp_fp }
        \box_use_drop:N \l__scaletextbullet_tmp_box
      }
    \,
    \group_begin:
      \dim_set:Nn \fboxrule { 0.1pt }
      \dim_zero:N \fboxsep
      \framebox [ \l__scaletextbullet_factor_tl \width ] { \textbullet }
    \group_end:
  }