====== rip_scrollbar.js ======
rip_scrollbar.js is a loadable JavaScript file that provides a class, RIPScrollbar, which is a reusable scrollbar widget for [[https://breakintochat.com/wiki/Remote_Imaging_Protocol_(RIP)|RIP]] terminals on Synchronet.  It renders a graphical scrollbar with arrow buttons, a proportional track, and a draggable thumb, all using RIPScrip vector drawing primitives.

RIPScrollbar supports both **vertical** and **horizontal** orientations.  By default the scrollbar is vertical (with up/down arrow buttons and a thumb that moves vertically).  Setting the ''horizontal'' property to ''true'' before calling ''computeLayout()'' switches to horizontal mode, where the arrow buttons become left/right arrows and the thumb moves horizontally.

RIPScrollbar is a pure rendering/layout component — it does not handle user input directly.  Instead, the consumer (such as [[custom:javascript:lib:rip_lightbar_menu|RIPLightbarMenu]] or a custom dialog) is responsible for reading keyboard and mouse input and calling the appropriate methods to update the scroll position and redraw the thumb.

To create a new RIPScrollbar object:
  var sb = new RIPScrollbar(x, y, width, height);
The x, y, width, and height parameters are in RIP pixel coordinates (the RIP graphics viewport is 640x350 pixels).

===== Dependencies =====
rip_scrollbar.js requires the following loadable module:
  * rip_lib.js — for generating RIPScrip command strings

===== Orientation =====
The scrollbar defaults to vertical mode.  To create a horizontal scrollbar, set the ''horizontal'' property to ''true'' before calling ''computeLayout()'':
  var sb = new RIPScrollbar(50, 330, 400, 14);
  sb.horizontal = true;
  sb.computeLayout();
In horizontal mode:
  * The arrow buttons appear at the **left** and **right** edges instead of the top and bottom.
  * The ''arrowHeight'' property controls the **width** of each arrow button (instead of the height).
  * The thumb moves along the X axis instead of the Y axis.
  * ''getThumbGeometry()'' returns ''{ x, w }'' (left pixel and width) instead of ''{ y, h }'' (top pixel and height).
  * The thumb drag mouse region uses ''$X$'' (the mouse X coordinate at release time) instead of ''$Y$''.
  * The ''mouseChars.scrollUp'' and ''mouseChars.scrollDn'' characters correspond to "scroll left" and "scroll right" respectively.
  * The ''mouseChars.trackPgUp'' and ''mouseChars.trackPgDn'' characters correspond to "page left" and "page right" respectively.

The consumer handles the mouse input the same way regardless of orientation — the character codes are identical.  The only difference is which coordinate (X or Y) follows the ''thumbDrag'' character.

===== How it works =====
The typical usage pattern is:

  - Create a RIPScrollbar with the desired position and size.
  - Optionally set ''horizontal'' to ''true'' for a horizontal scrollbar.
  - Set the scroll state: totalItems, visibleCount, and topIndex.
  - Call ''computeLayout()'' to calculate the internal geometry.
  - Call ''buildFullRIP()'' to get the RIP command string for the complete scrollbar, and send it to the terminal.
  - Call ''buildMouseRegionsRIP()'' to get the RIP mouse region commands, and send those as well.  This registers clickable areas for the arrow buttons, track areas, and the thumb.
  - When the user scrolls (via keyboard or mouse clicks), update topIndex, then call ''buildThumbRIP()'' and ''buildMouseRegionsRIP()'' to efficiently redraw just the thumb and update the mouse regions.

The scrollbar's mouse regions send high-byte character codes when clicked (configurable via the mouseChars property).  The consumer reads these codes from console input and maps them to scroll actions:
  * **scrollUp** (default 0xFE): Scroll up/left one item (up or left arrow clicked)
  * **scrollDn** (default 0xFD): Scroll down/right one item (down or right arrow clicked)
  * **trackPgUp** (default 0xFC): Page up/left (track area before the thumb clicked)
  * **trackPgDn** (default 0xFB): Page down/right (track area after the thumb clicked)
  * **thumbDrag** (default 0xFA): Thumb drag — followed by the $Y$ text variable (vertical mode) or $X$ text variable (horizontal mode), giving the mouse coordinate at release time as a decimal digit string.  The consumer reads this coordinate and computes the new scroll position proportionally.

**RIPScrollbar methods**
^ Name                  ^ Returns  ^ Usage                                                 ^ Description^
| computeLayout         | void     | RIPScrollbar.computeLayout()                          | Computes the internal geometry (arrow button bounds, track bounds) from the current pos, size, arrowHeight, and orientation.  Must be called before any build methods, and again if the scrollbar's position, size, or orientation changes. |
| setScrollState        | void     | RIPScrollbar.setScrollState(totalItems, visibleCount, topIndex) | Sets the scroll state (total number of items, number of visible items, and the index of the first visible item).  Call this before building RIP commands so the thumb size and position are correct. |
| buildFullRIP          | string   | RIPScrollbar.buildFullRIP()                           | Returns a RIP command string that draws the complete scrollbar: both arrow buttons (with beveled 3D appearance), the track background, and the thumb.  Does NOT include mouse regions — call buildMouseRegionsRIP() separately. |
| buildThumbRIP         | string   | RIPScrollbar.buildThumbRIP()                          | Returns a RIP command string that redraws just the track and thumb.  Used for efficient updates when only the scroll position has changed (no need to redraw the arrow buttons). |
| buildMouseRegionsRIP  | string   | RIPScrollbar.buildMouseRegionsRIP()                   | Returns a RIP command string that defines mouse regions for the scrollbar's clickable areas.  In vertical mode: up/down arrows, track above/below thumb, and thumb (with $Y$).  In horizontal mode: left/right arrows, track left/right of thumb, and thumb (with $X$).  Note: this does NOT call RIPKillMouseFields() — the consumer must kill existing fields before registering new ones if needed. |
| getThumbGeometry      | object   | RIPScrollbar.getThumbGeometry()                       | Returns an object describing the current thumb position and size.  In vertical mode: ''{ y, h }'' (top pixel and height).  In horizontal mode: ''{ x, w }'' (left pixel and width). |

**RIPScrollbar properties**
^ Name           ^ Type    ^ Description^
| pos            | object  | An object with x and y properties for the upper-left corner of the scrollbar, in RIP pixel coordinates. |
| size           | object  | An object with width and height properties for the scrollbar dimensions, in RIP pixels. |
| horizontal     | boolean | Orientation of the scrollbar.  ''false'' (default) for vertical, ''true'' for horizontal.  Must be set before calling computeLayout(). |
| colors         | object  | An object containing RIP color numbers (0-15, as defined in rip_lib.js) for the scrollbar's visual components.  Properties: track (track background), thumb (thumb body), arrowBg (arrow button background), arrowFg (arrow triangle foreground), bevelBright (bright edge of 3D bevel), bevelDark (dark edge of 3D bevel). |
| arrowHeight    | number  | Size of each arrow button along the scrollbar's primary axis (pixels).  For vertical mode this is the height of each up/down button; for horizontal mode this is the width of each left/right button.  Defaults to 14. |
| totalItems     | number  | Total number of items being scrolled through.  Set by the consumer. |
| visibleCount   | number  | Number of items visible at once.  Set by the consumer. |
| topIndex       | number  | Index of the first visible item (scroll position).  Set by the consumer and updated on scroll. |
| mouseChars     | object  | Object containing the character codes sent when mouse regions are clicked.  Properties: scrollUp (default 0xFE), scrollDn (default 0xFD), trackPgUp (default 0xFC), trackPgDn (default 0xFB), thumbDrag (default 0xFA).  In horizontal mode, scrollUp/scrollDn correspond to left/right, and trackPgUp/trackPgDn correspond to page-left/page-right.  These can be changed to avoid conflicts when multiple scrollbars or other mouse-aware widgets are on screen simultaneously. |

To change the scrollbar colors:
  sb.colors.track = RIP_COLOR_BLACK;
  sb.colors.thumb = RIP_COLOR_LT_CYAN;
  sb.colors.arrowBg = RIP_COLOR_LT_GRAY;
  sb.colors.arrowFg = RIP_COLOR_BLACK;
  sb.colors.bevelBright = RIP_COLOR_WHITE;
  sb.colors.bevelDark = RIP_COLOR_DK_GRAY;

====== Example usage (vertical) ======
  load("rip_scrollbar.js");
  load("rip_lib.js");

  // Helper to send a batch of RIP commands
  function sendRIP(cmds) {
    console.write("\r!" + cmds + RIPGotoXYNumeric(0, 0) + "\r\n");
  }

  // Create a vertical scrollbar at pixel position (600, 70), 14 pixels wide, 260 pixels tall
  var sb = new RIPScrollbar(600, 70, 14, 260);
  sb.setScrollState(100, 20, 0);  // 100 total items, 20 visible, starting at item 0
  sb.computeLayout();

  // Draw the scrollbar and register mouse regions
  sendRIP(sb.buildFullRIP() + sb.buildMouseRegionsRIP());

  // In your input loop, when the user scrolls:
  sb.topIndex = 5;  // User scrolled down to show items 5-24
  sendRIP(sb.buildThumbRIP() + sb.buildMouseRegionsRIP());

====== Example usage (horizontal) ======
  load("rip_scrollbar.js");
  load("rip_lib.js");

  function sendRIP(cmds) {
    console.write("\r!" + cmds + RIPGotoXYNumeric(0, 0) + "\r\n");
  }

  // Create a horizontal scrollbar at pixel position (50, 330), 500 pixels wide, 14 pixels tall
  var sb = new RIPScrollbar(50, 330, 500, 14);
  sb.horizontal = true;  // Switch to horizontal mode
  sb.setScrollState(200, 50, 0);  // 200 total items, 50 visible, starting at item 0
  sb.computeLayout();

  // Draw and register mouse regions (same API as vertical)
  sendRIP(sb.buildFullRIP() + sb.buildMouseRegionsRIP());

====== Handling mouse input ======
When the user clicks on the scrollbar's mouse regions, SyncTerm sends the corresponding character code.  Here is an example of handling the mouse input in a getkey() loop.  The handling code is the same for vertical and horizontal scrollbars — only the coordinate variable (Y vs X) differs for the thumb drag:

  var key = console.getkey(K_NOCRLF | K_NOECHO | K_NOSPIN);
  var ch = key.charCodeAt(0);

  if (ch === sb.mouseChars.scrollUp) {
    if (topIndex > 0) --topIndex;
  }
  else if (ch === sb.mouseChars.scrollDn) {
    if (topIndex < maxTopIndex) ++topIndex;
  }
  else if (ch === sb.mouseChars.trackPgUp) {
    topIndex = Math.max(0, topIndex - visibleCount);
  }
  else if (ch === sb.mouseChars.trackPgDn) {
    topIndex = Math.min(maxTopIndex, topIndex + visibleCount);
  }
  else if (ch === sb.mouseChars.thumbDrag) {
    // Read the coordinate digits that follow.  For vertical scrollbars this is
    // the $Y$ value; for horizontal scrollbars this is the $X$ value.
    var coordStr = "";
    for (var d = 0; d < 4; ++d) {
      var dc = console.inkey(0, 500);
      if (dc === "") break;
      coordStr += dc;
    }
    var mouseCoord = parseInt(coordStr, 10);
    if (!isNaN(mouseCoord)) {
      var sbL = sb._layout;
      if (sb.horizontal) {
        // Horizontal: map X coordinate to scroll position
        var thumb = sb.getThumbGeometry();
        var thumbRange = sbL.trackWidth - thumb.w;
        if (thumbRange > 0) {
          var clampedX = Math.max(sbL.trackLeft, Math.min(sbL.trackRight, mouseCoord));
          var relX = clampedX - sbL.trackLeft;
          topIndex = Math.round(relX * maxTopIndex / thumbRange);
          topIndex = Math.max(0, Math.min(maxTopIndex, topIndex));
        }
      }
      else {
        // Vertical: map Y coordinate to scroll position
        var thumb = sb.getThumbGeometry();
        var thumbRange = sbL.trackHeight - thumb.h;
        if (thumbRange > 0) {
          var clampedY = Math.max(sbL.trackTop, Math.min(sbL.trackBot, mouseCoord));
          var relY = clampedY - sbL.trackTop;
          topIndex = Math.round(relY * maxTopIndex / thumbRange);
          topIndex = Math.max(0, Math.min(maxTopIndex, topIndex));
        }
      }
    }
  }

  // After updating topIndex, redraw the thumb and mouse regions
  sb.topIndex = topIndex;
  sendRIP(sb.buildThumbRIP() + sb.buildMouseRegionsRIP());

====== Example code ======
For real-world examples where RIPScrollbar is used, see:\\
  * [[custom:javascript:lib:rip_lightbar_menu|RIPLightbarMenu]] (rip_lightbar_menu.js): Creates a vertical RIPScrollbar internally to provide scrolling for the menu's item list.
  * Digital Distortion File Lister ([[https://gitlab.synchro.net/main/sbbs/-/blob/master/xtrn/DDFileLister/ddfilelister.js|ddfilelister.js]]): Uses RIPScrollbar in its showRIPTextDialog() function for scrolling file information, help screens, and batch download queue dialogs in RIP mode.

===== See Also =====
  * [[custom:javascript:lib:rip_lightbar_menu|rip_lightbar_menu.js]] — the lightbar menu that uses RIPScrollbar
  * [[custom:javascript:lib:dd_lightbar_menu|dd_lightbar_menu.js]] — the ANSI counterpart menu class
  * [[:custom:javascript:lib:|custom:javascript:lib index]]

{{tag>}}