====== rip_lightbar_menu.js ======
rip_lightbar_menu.js is a loadable JavaScript file that provides a class, RIPLightbarMenu, which is a lightbar menu class for use in JavaScript scripts for Synchronet on [[https://breakintochat.com/wiki/Remote_Imaging_Protocol_(RIP)|RIP]]-capable terminals.  It uses pixel-based RIP graphics commands rather than ANSI text, drawing the menu and scrollbar using RIPScrip vector drawing primitives.

Some functionality of the RIPLightbarMenu class was modeled after [[custom:javascript:lib:ddlightbarmenu.js|DDLightbarMenu]] (in dd_lightbar_menu.js) so that it supports many of the same features and is at least somewhat API-compatible with DDLightbarMenu.  This means callers can often use the same code to drive either menu class (for example, [[https://gitlab.synchro.net/main/sbbs/-/blob/master/xtrn/DDFileLister/ddfilelister.js|Digital Distortion File Lister]] uses DDLightbarMenu for ANSI terminals and RIPLightbarMenu for RIP terminals, sharing much of the same callback and configuration logic).  Shared features include the callback functions (OnItemSelect, ValidateSelectItem, OnItemNav), hotkey management, multi-select mode, the exitOnItemSelect flag, and overrideable NumItems()/GetItem() for dynamic item lists.

To create a new RIPLightbarMenu object:
  var ripMenu = new RIPLightbarMenu(x, y, width, height, useRIPBorder);
The x, y, width, and height parameters are in RIP pixel coordinates (the RIP graphics viewport is 640x350 pixels).  The useRIPBorder parameter is an optional boolean that controls whether a stylized 3D bevel border is drawn around the menu (defaults to true).

The RIPLightbarMenu class requires a RIP-capable terminal (such as SyncTerm).  It uses the [[custom:javascript:lib:rip_scrollbar|RIPScrollbar]] class (in rip_scrollbar.js) for scrollbar widgets:
  * A **vertical scrollbar** appears automatically on the right side when the number of items exceeds the visible area (i.e., more items than can fit vertically).
  * A **horizontal scrollbar** appears automatically at the bottom when any item's text is wider than the visible menu area.  This allows the user to scroll left/right to see the full text of wide items.  When horizontal scrolling is not needed (all items fit within the menu width), no horizontal scrollbar is shown and no space is reserved for it.
Both scrollbars are only shown when needed.  When both are present, the menu correctly accounts for the space each scrollbar occupies (the vertical scrollbar narrows the item area, and the horizontal scrollbar reduces the number of visible item rows by one).

The menu supports keyboard navigation (Up, Down, PageUp, PageDown, Home, End, Enter, ESC) as well as Left/Right arrow keys for horizontal scrolling when items are wider than the menu.  Mouse interaction is also supported: clicking on menu items to select them, and clicking on either scrollbar's arrow buttons, track areas, and thumb for vertical or horizontal scrolling.

The user can navigate the list using the up & down arrows, PageUp, PageDown, Home, and End keys.  The Enter key selects an item.  The ESC key exits the menu and returns null.  When a scrollbar is needed, users can click the scrollbar's arrow buttons to scroll one item, click the track above/below the thumb for page up/down, or drag the thumb to a new position.

Menu items can have hotkeys set (via Add(), SetItemHotkey(), or AddItemHotkey()) that select the item immediately when the corresponding key is pressed.  Hotkeys are case-insensitive by default (controlled by the hotkeyCaseSensitive property).

After showing the menu and getting a value from the user (using the GetVal() function), the lastUserInput property will have the user's last keypress.

===== Dependencies =====
rip_lightbar_menu.js requires the following loadable modules:
  * sbbsdefs.js
  * key_defs.js
  * rip_lib.js — for generating RIPScrip command strings
  * [[custom:javascript:lib:rip_scrollbar|rip_scrollbar.js]] — for the scrollbar widget

**RIPLightbarMenu methods**
^ Name                          ^ Returns  ^ Usage                                        ^ Description^
| Add                           | void     | RIPLightbarMenu.Add(text, retval, hotkey)    | Adds an item to the menu with given display text, return value, and optional hotkey character(s).  The retval defaults to the current item count if not specified.  The hotkey parameter can be a single character or a string of multiple characters — each character is a separate hotkey. |
| Remove                        | void     | RIPLightbarMenu.Remove(idx)                  | Removes an item from the menu by its index. |
| RemoveAllItems                | void     | RIPLightbarMenu.RemoveAllItems()             | Removes all items from the menu. |
| NumItems                      | number   | RIPLightbarMenu.NumItems()                   | Returns the number of items in the menu.  This function can be replaced for dynamic item lists (see "Replacing NumItems() and GetItem()" below). |
| GetItem                       | object   | RIPLightbarMenu.GetItem(idx)                 | Returns the item object at the given index.  Can be replaced for dynamic item lists. |
| MakeItemWithRetval             | object   | RIPLightbarMenu.MakeItemWithRetval(retval)   | Creates and returns a default item object with the specified return value (for use in custom GetItem functions). |
| MakeItemWithTextAndRetval      | object   | RIPLightbarMenu.MakeItemWithTextAndRetval(text, retval) | Creates and returns a default item object with the specified text and return value (for use in custom GetItem functions). |
| SetItemHotkey                 | void     | RIPLightbarMenu.SetItemHotkey(idx, hotkey)   | Sets a hotkey for a menu item, replacing any existing hotkeys. |
| AddItemHotkey                 | void     | RIPLightbarMenu.AddItemHotkey(idx, hotkey)   | Adds an additional hotkey for a menu item without removing existing hotkeys. |
| RemoveItemHotkey              | void     | RIPLightbarMenu.RemoveItemHotkey(idx, hotkey) | Removes a specific hotkey from a menu item. |
| RemoveAllItemHotkeys          | void     | RIPLightbarMenu.RemoveAllItemHotkeys()       | Removes all hotkeys from all menu items. |
| SetSelectedItemIdx            | void     | RIPLightbarMenu.SetSelectedItemIdx(idx)      | Sets the currently selected (highlighted) item index before calling GetVal(). |
| GetVal                        | various  | RIPLightbarMenu.GetVal(pDefaultRetVal, pSelectedItemIndexes) | Draws the menu and enters the input loop.  Returns the selected item's return value, or null if the user aborts.  In multi-select mode, the selections are stored in the selectedItemIndexes object.  The optional pDefaultRetVal specifies an item to pre-select by its return value. |
| AddAdditionalQuitKeys         | void     | RIPLightbarMenu.AddAdditionalQuitKeys(keys)  | Adds characters from the given string as additional quit keys (in addition to ESC). |
| ClearAdditionalQuitKeys       | void     | RIPLightbarMenu.ClearAdditionalQuitKeys()    | Clears the list of additional quit keys. |
| QuitKeysIncludes              | boolean  | RIPLightbarMenu.QuitKeysIncludes(key)        | Returns whether the given key is in the additional quit keys. |
| AddAdditionalSelectItemKeys   | void     | RIPLightbarMenu.AddAdditionalSelectItemKeys(keys) | Adds characters as additional select-item keys (in addition to Enter). |
| ClearAdditionalSelectItemKeys | void     | RIPLightbarMenu.ClearAdditionalSelectItemKeys() | Clears the list of additional select-item keys. |
| SelectItemKeysIncludes        | boolean  | RIPLightbarMenu.SelectItemKeysIncludes(key)  | Returns whether the given key is in the additional select-item keys. |
| ValidateSelectItem            | boolean  | RIPLightbarMenu.ValidateSelectItem(retval)   | Called before an item is selected.  Return false to prevent selection.  Override this for custom validation logic. |
| OnItemSelect                  | void     | RIPLightbarMenu.OnItemSelect(retval, selected) | Called after an item is selected (or toggled in multi-select mode).  Override this to perform actions on selection. |
| OnItemNav                     | void     | RIPLightbarMenu.OnItemNav(oldIdx, newIdx)    | Called when the user navigates to a different item.  Override this to update detail panes or other UI elements. |

**RIPLightbarMenu properties**
^ Name                                      ^ Type    ^ Description^
| pos                                       | object  | An object with x and y properties for the upper-left corner position of the menu, in RIP pixel coordinates. |
| size                                      | object  | An object with width and height properties for the menu dimensions, in RIP pixels. |
| colors                                    | object  | An object containing RIP color numbers for various parts of the menu.  Properties: itemBg, itemFg, selectedBg, selectedFg, borderSurface, bevelBright, bevelDark, scrollTrack, scrollThumb, scrollArrowBg, scrollArrowFg, contentBg.  These use the RIP color constants (0-15) defined in rip_lib.js. |
| useRIPBorder                              | boolean | Whether to draw a stylized 3D bevel border around the menu.  Defaults to true. |
| selectedItemIdx                           | number  | The index of the currently highlighted item (0-based).  Can be read after GetVal() returns, or set before calling GetVal(). |
| topItemIdx                                | number  | The index of the first visible item (vertical scroll position). |
| leftCharIdx                               | number  | The index of the first visible character in each item's text (horizontal scroll position).  Only meaningful when a horizontal scrollbar is active.  Defaults to 0. |
| itemHeight                                | number  | Height of each menu item row in pixels.  Defaults to 16 (for the 8x8 default RIP font). |
| textYOffset                               | number  | Y offset for text within an item row, in pixels.  Defaults to 4. |
| textXOffset                               | number  | X offset for text from the left edge of the content area, in pixels.  Defaults to 4. |
| exitOnItemSelect                          | boolean | When true (default), GetVal returns immediately after the user selects an item.  When false, the menu stays visible after selection — useful with OnItemSelect for performing actions without leaving the menu. |
| lastUserInput                             | string  | The user's last keypress from the most recent GetVal() call. |
| multiSelect                               | boolean | Whether multi-select mode is enabled.  Defaults to false.  When true, users can toggle items on/off with Space or mouse click. |
| maxNumSelections                          | number  | Maximum number of selections in multi-select mode.  -1 or 0 means unlimited.  Defaults to -1. |
| multiSelectItemChar                       | string  | The character displayed next to selected items in multi-select mode.  Defaults to the CP437 check mark. |
| selectedItemIndexes                       | object  | Object whose keys are selected item indexes (used in multi-select mode). |
| enterAndSelectKeysAddsMultiSelectItem     | boolean | When true and in multi-select mode, pressing Enter also adds the current item to the selection before returning.  Defaults to false. |
| hotkeyCaseSensitive                       | boolean | Whether hotkeys are case-sensitive.  Defaults to false. |
| callOnItemNavOnStartup                    | boolean | Whether to call OnItemNav() when the menu is first displayed (with old index -1).  Defaults to false. |
| noVerticalScrollbar                       | boolean | When true, suppresses the vertical scrollbar even if there are more items than can fit vertically.  Items beyond the visible area will simply not be reachable by scrolling.  Defaults to false (vertical scrollbar is shown when needed). |
| noHorizontalScrollbar                     | boolean | When true, suppresses the horizontal scrollbar even if items are wider than the visible area.  Wide item text will be clipped to the menu width.  Defaults to false (horizontal scrollbar is shown when needed). |
| additionalQuitKeys                        | string  | Characters treated as additional quit keys beyond ESC. |
| additionalSelectItemKeys                  | string  | Characters treated as additional select-item keys beyond Enter. |

To change the colors used for the menu items and border, modify the properties of the colors object:
  ripMenu.colors.itemBg = RIP_COLOR_BLACK;
  ripMenu.colors.itemFg = RIP_COLOR_WHITE;
  ripMenu.colors.selectedBg = RIP_COLOR_BLUE;
  ripMenu.colors.selectedFg = RIP_COLOR_WHITE;
  ripMenu.colors.borderSurface = RIP_COLOR_LT_GRAY;

====== Example usage ======
  load("rip_lightbar_menu.js");
  // Create a menu at pixel position (15, 64) with width 611 and height 286, with border
  var ripMenu = new RIPLightbarMenu(15, 64, 611, 286, true);
  // Add 20 items to the menu
  for (var i = 0; i < 20; ++i)
    ripMenu.Add("Item " + +(i+1), i+1);
  // Set up hotkey "s" for the 2nd item
  ripMenu.SetItemHotkey(1, "s");
  // Show the menu and get the chosen item from the user
  var val = ripMenu.GetVal();
  // val contains the return value of the selected item, or null if the user aborted

  // Disabling auto-exit on selection (to use with OnItemSelect callback):
  ripMenu.exitOnItemSelect = false;
  ripMenu.OnItemSelect = function(pRetval, pSelected) {
    // Do something when an item is selected without leaving the menu
  };

====== Replacing NumItems() and GetItem() ======
For large or dynamic lists, you can replace NumItems() and GetItem() so the menu references external data without copying it into the menu's internal array.  This is the same pattern used by [[custom:javascript:lib:dd_lightbar_menu|DDLightbarMenu]].\\
\\
Example (assuming ripMenu is a RIPLightbarMenu object and myList is an external data array):
  ripMenu.NumItems = function() {
    return myList.length;
  };
  ripMenu.GetItem = function(pIdx) {
    var item = this.MakeItemWithRetval(pIdx);
    item.text = myList[pIdx].displayText;
    return item;
  };

====== Replacing ValidateSelectItem() ======
Override ValidateSelectItem() to control whether a user can select a given item:
  ripMenu.ValidateSelectItem = function(pItemRetval) {
    if (someCondition(pItemRetval))
      return true;
    else
      return false;
  };

====== Replacing OnItemSelect() ======
Override OnItemSelect() to perform an action when an item is selected or toggled:
  ripMenu.OnItemSelect = function(pItemRetval, pSelected) {
    // pSelected is true when the item is selected, false when de-selected
    // (de-selection only occurs in multi-select mode)
  };

====== Replacing OnItemNav() ======
Override OnItemNav() to react when the user navigates to a different item (e.g., to update a detail pane):
  ripMenu.OnItemNav = function(pOldItemIdx, pNewItemIdx) {
    // Update a description area based on the newly highlighted item
  };

====== Differences from DDLightbarMenu ======
While RIPLightbarMenu is API-compatible with DDLightbarMenu for many common operations, there are some differences due to the fundamentally different display technology:
  * **Coordinates are in RIP pixels** (640x350 viewport), not ANSI text cells.
  * **No numbered mode** — RIPLightbarMenu always uses lightbar-style navigation (since it requires a RIP terminal).
  * **No ANSI fallback** — DDLightbarMenu can fall back to numbered text input for non-ANSI terminals; RIPLightbarMenu requires RIP.
  * **Border is a 3D bevel** rendered with RIP graphics, not ANSI box-drawing characters.
  * **Scrollbar is a graphical widget** using [[custom:javascript:lib:rip_scrollbar|RIPScrollbar]], with arrow buttons, a proportional thumb, and mouse support — not a text-based scrollbar.
  * **Mouse support is always enabled** — RIP mouse regions are always registered for menu items and the scrollbar.
  * DDLightbarMenu features such as Draw(), Erase(), DrawPartial(), DrawPartialAbs(), SetBorderChars(), item color arrays, altItemColor/altSelectedItemColor, borderChars, borderEnabled, scrollbarEnabled, numberedMode, allowANSI, wrapNavigation, and ampersandHotkeysInItems are not present in RIPLightbarMenu.

====== Example code ======
For a real-world example where RIPLightbarMenu is used, see:\\
  * Digital Distortion File Lister ([[https://gitlab.synchro.net/main/sbbs/-/blob/master/xtrn/DDFileLister/ddfilelister.js|ddfilelister.js]]): Uses RIPLightbarMenu when the terminal supports RIP, with NumItems/GetItem replacement, OnItemNav for updating the file description pane, additional quit keys for menu bar integration, and hotkey support.
  * uselect_rip.js ([[https://gitlab.synchro.net/main/sbbs/-/blob/master/exec/uselect_rip.js|uselect_rip.js]]): Synchronet's "Select Item" loadable module for RIP terminals, demonstrating basic RIPLightbarMenu usage with hotkeys.

===== Screenshots =====
^ Without scrollbar                          ^ With scrollbar^
| {{:custom:javascript:lib:riplightbarmenu_noscrollbar.png?400|}} | {{:custom:javascript:lib:riplightbarmenu_scrollbar.png?400|}} |

===== See Also =====
  * [[custom:javascript:lib:dd_lightbar_menu|dd_lightbar_menu.js]] — the ANSI counterpart
  * [[custom:javascript:lib:rip_scrollbar|rip_scrollbar.js]] — the scrollbar widget used by RIPLightbarMenu
  * [[:custom:javascript:lib:|custom:javascript:lib index]]

{{tag>}}