! ==============================================================================
! dmenus.h -- an object-oriented menu system, similar to Graham Nelson's
! menus.h, but allowing concealed or locked menu items, sub-menus that are
! embedded in parent menus, and incremental hints.
!
! Version 8.4 (Mar06) by Roger Firth (roger@firthworks.com)
!                     after Dave Robinson (khelwood@hotmail.com)
!
! Compatibility: for Inform 6.3 (Z-code v5/8 and Glulx).
!
! Dependencies: requires infglk.h (for Glulx).
!
! License: The Creative Commons Attribution-ShareAlike License published at
! http://creativecommons.org/licenses/by-sa/2.5/ applies to this software.
! In summary: you must credit the original author(s); if you alter, transform,
! or build upon this software, you may distribute the SOURCE FORM of the
! resulting work only under a license identical to this one. Note that the
! ShareAlike clause does not affect the way in which you distribute the COMPILED
! FORM of works built upon this software.
! Copyright remains with the original author(s), from whom you must seek
! permission if you wish to vary any of the terms of this license.
! The author(s) would also welcome bug reports and enhancement suggestions.
!
! ------------------------------------------------------------------------------
! INSTALLATION
!
! Place this line anywhere after VerbLib.h:
!
!   Include "dmenus";
!
! ------------------------------------------------------------------------------
! USAGE
!
! Create a basic one-level menu like this:
!
!   Menu mytopmenu "All about Mordred";
!       Option -> "Looking around"
!           with description "I am your eyes and ears ...";
!       Option -> "Taking and Dropping"
!           with description "When you find items ...";
!       Option -> "About the author"
!           with description "The author was born in ...";
!
! Create a more complex multi-level menu in the same way:
!
!   Menu mytopmenu "All about Mordred";
!       Menu  -> "How to play adventure games";
!           Option -> -> "Looking around"
!               with description "I am your eyes and ears ...";
!           Option -> -> "Taking and Dropping"
!               with description "When you find items ...";
!       Menu -> "Background information";
!           Option -> -> "Credits"
!               with description "Many people assisted ...";
!           Option -> -> "About the author"
!               with description "The author was born in ...";
!       Menu -> "Help -- I'm stuck!";
!           Menu -> -> "In the forest";
!               HintOption -> -> -> "Where's the gold ring?"
!                   with description
!                      "..."
!                      "..."
!                      "...";
!              HintOption -> -> -> "I can't get past the giant"
!                  with description "..." "...";
!          Menu -> -> "At sea";
!              HintOption -> -> -> "How do I steer the boat?"
!                  with description "..." "..." "..." "...";
!              HintOption -> -> -> "What's the catapult useful for?"
!                  with description "..." "..." "...";
!
! Display the top-level menu with either of:
!
!   ShowMenu(mytopmenu);
!   mytopmenu.select();
!
! ------------------------------------------------------------------------------
! CUSTOMISATION
!
! You can change the default values of MENU_TOPLINE, HINT_NOMORE and
! OPTION_NOTEXT. Before Including this extension, define the appropriate
! constant as a string, or as a routine which prints a string (note that a
! MENU_TOPLINE routine is called with the objID of the Menu as an argument):
!
!   Constant MENU_TOPLINE " Information is provided on these subjects:";
!
!   [ MyMENU_TOPLINE; print " Information is provided on these subjects:"; ];
!   Constant MENU_TOPLINE MyMENU_TOPLINE;
!
! You can also redefine the XXXX__KY and XXXX__TX keys and on-screen prompts
! (but match the text lengths on the RHS of the screen).
!
! You can define INCLUDE_SWITCHOPTION if you wish to use the optional
! SwitchOption class:
!
!   Constant INCLUDE_SWITCHOPTION;
!
! Finally, you can define HINT_NOBLANKLINES if you wish to omit the blank lines
! between successive hints:
!
!   Constant HINT_NOBLANKLINES;
!
! ------------------------------------------------------------------------------
! NOTES
!
! 1.    At the top of a menu hierachy is an object of class Menu. The children
!       of a Menu can be objects of these classes:
!
!           Menu (sub-menus, to any depth),
!           LineGap (blank lines on Menus)
!           Option (plain text descriptions)
!           HintOption (incremental text descriptions)
!           SwitchOption (on/off toggles)
!           other (treated as Options)
!
! 2.    When selected for display, a Menu lists the titles of its children, and
!       then allows one of those titles to be selected. A top-level Menu object
!       looks like this:
!
!           Menu menuID "title";
!
!       ('menuID' is used when displaying the menu, and when dynamically adding
!       items to the menu), and a basic sub-menu looks like this:
!
!           Menu -> "title";
!
! 3.    A LineGap object cannot be selected; its function is to create an empty
!       line on a Menu. A basic LineGap looks like this:
!
!           LineGap ->;
!
!       To create a line of informative text, use 'locked' -- see Note 11.
!
! 4.    When selected, an Option displays its text. A basic Option object looks
!       like one of these:
!
!           Option -> "title"
!               with description "option_text";
!
!           Option -> "title"
!               with description [; "option_text"; ];
!
!       If an option's description is a string, or a routine returning 0 or 1,
!       the parent menu adds a "Press space" message and waits for a key before
!       redisplaying itself. If an option's description returns 2 or higher,
!       the menu doesn't wait for a key, so anything printed by the option tends
!       not to appear on screen. A return value of 2 redisplays the parent menu
!       immediately. A value of 3 redisplays the parent menu's parent, thus
!       returning one level up the menu hierarchy. A value of 4 returns two
!       levels up the hierarchy, and so on; a large return will therefore
!       close all open menus and immediately resume the game.
!
! 5.    When selected, a HintOption displays its texts one by one. A basic
!       HintOption looks like this -- an array of up to 32 strings:
!
!           HintOption -> "title"
!               with description
!                   "hint1_text"
!                   "hint2_text"
!                   ...
!                   "hintN_text";
!
!       To enable individual hints to adapt their output according to the game
!       state, any item in the array can also be a routine (which displays a
!       hint) or an object (whose 'description' property provides the hint):
!
!           HintOption -> "title"
!               with description
!                   [; "hint1_text" ]
!                   myHintObj2
!                   ...
!                   "hintN_text";
!
! 6.    When selected, a SwitchOption toggles its 'on' attribute, and the
!       suffix displayed after its title toggles between " (on)" and " (off)".
!       A basic SwitchOption looks like this:
!
!           SwitchOption -> "title";
!
!       SwitchOptions are available only if you define an INCLUDE_SWITCHOPTION
!       Constant before Including this extension.
!
! 7.    The items on a menu are listed in child order, eldest down to youngest.
!       You can add or remove menu items during play, either by adding code to
!       'before', 'description' or 'after' routines of menu items, or externally
!       to the menu hierarchy. To add an item at the top of the menu, use:
!
!           move objID to menuID;
!
!       To add an item at the bottom of a menu, use:
!
!           MenuMoveToBottom(objID, menuID);
!
!       To remove an item from a menu, use:
!
!           remove objID;
!
!       See also Note 10 on the 'concealed' attribute;
!
! 8.    By default, the same title text is used both for listing an item on a
!       Menu and (when that item is a Menu, Option or HintOption) as a heading
!       on the new screen displayed when that item is selected. You can define
!       an alternative heading by giving the item a 'short_name' property which
!       checks the 'open' attribute (set while displaying a heading):
!
!           short_name [;
!               if (self has open) { print "alternative_title"; rtrue; }
!           ],
!
! 9.    Menu, Option and HintOption items can have 'before' and 'after'
!       properties. A 'before' property is invoked when an item is selected but
!       before anything has been displayed. If the property returns 0, the
!       item is displayed as usual; otherwise, nothing is displayed, and the
!       return value is treated in the same way as for the 'description'
!       property -- see Note 4. An 'after' property is invoked after a Menu
!       is exited or after an Option has been displayed; it has no return value.
!
! 10.   If an item has a 'concealed' attribute, it is not listed on its parent
!       menu. The attribute setting can be changed at any time.
!
!       Use 'before' on a "Hints" menu to make the hints adapt to the player's
!       progress -- for example mentioning hints about the dungeon door only
!       once the dungeon has been visited and until the door has been unlocked:
!
!           before [;
!               if (dungeon has visited && dungeon_door has locked)
!                    give ddoor_hints ~concealed;
!               else give ddoor_hints concealed;
!               ...
!           ],
!
! 11.   If an item has a 'locked' attribute, it is listed on its parent menu,
!       but cannot be selected. The setting can be changed at any time.
!
!       To create a line of informative text on a menu, use:
!
!           Option -> "This menu tells you about..." has locked;
!
! 12.   If a sub-menu has a 'transparent' attribute, then selecting that
!       sub-menu opens it within its parent menu, rather than on a separate
!       screen.
!
!       If a HintOption has a 'transparent' attribute, the hints are numbered
!       simply as [1], [2], etc, rather than as [1/N], [2/N], etc.
!
! 13.   The menus support keys for PageDown, PageUp, End and Home, and the
!       emblazoned heading, which normally lists the keys, can be told to
!       include these. By default, the extra keys are described only on
!       multi-page menus. Set the global variable FullEmblaze to
!       FULLEMBLAZE_ALWAYS, FULLEMBLAZE_NEVER or FULLEMBLAZE_SOMETIMES (the
!       default). To use the FullEmblaze only on a specific menu, you might use
!       its 'before' and 'after' properties:
!
!           before [; FullEmblaze = FULLEMBLAZE_ALWAYS; ],
!           after  [; FullEmblaze = FULLEMBLAZE_NEVER; ];
!
! ------------------------------------------------------------------------------
! HISTORY
!
! Version 8.4: (Mar06) Simplified for compatibility with Library 6/11.
! Version 8.3: (Oct04) Reworked to use LIBRARY_STAGE.
! Version 8.2: (Jul04) Made MIF-compliant.
! Version 8.1: (Feb04) Made fuller use of Inform 6.3 facilities.
! Version 8.0: (Jan04) Reworked for Inform 6.3. HintOption texts now given
!              in 'description'.
! Version 7:   (Mar03) Added alternative pgup, pgdn, home, end keys, including
!              for Z-code. Added FullEmblaze feature. Added 'after' execution.
! Version 6:   Added 'before' interceptions and MENU_TOPLINE.
! Version 5:   Glulx compatibility.
!
! ==============================================================================

System_file;

#Iftrue LIBRARY_STAGE == AFTER_VERBLIB;

#Ifdef DMENUS_H;
Message warning "DMENUS already Included";
#Ifnot;
Constant DMENUS_H 84;
#Ifdef DEBUG; Message "[Including DMENUS]"; #Endif;

! ==============================================================================

Default  MENU_TOPLINE  "  Select from:";
Default  HINT_NOMORE   "No more hints.";
Default  OPTION_NOTEXT "[Sorry -- there is no text here.]";

! Basic menu texts and key definitions are in the library, but we can make
! them redefinable, and add a few more for FullEmblaze and Hints.
! These are Zcode values -- Glulx control keys are re-mapped to Zcode values.

Constant RESIZE__KY = $01;              ! dummy value -- cannot be generated
Constant RETURN__KY = $0d;
Constant ESC__KY    = $1b;
Constant UARROW__KY = $81;
Constant DARROW__KY = $82;
Constant LARROW__KY = $83;
Constant RARROW__KY = $84;

Default  QUIT1__KY  = QKEY1__KY;        ! 'Q'
Default  QUIT2__KY  = ESC__KY;          ! esc
Default  QUIT3__KY  = LARROW__KY;       ! left-arrow
Default  QUIT1__TX  = QKEY1__TX;        ! "  Q = resume game"
Default  QUIT2__TX  = QKEY2__TX;        ! "Q = previous menu"
Default  QUIT3__TX  =                     "         Q = back";

Default  NEXT1__KY  = NKEY1__KY;        ! 'N'
Default  NEXT2__KY  = DARROW__KY;       ! down-arrow
Default  NEXT__TX   = NKEY__TX;         ! "N = next subject"

Default  PREV1__KY  = PKEY1__KY;        ! 'P'
Default  PREV2__KY  = UARROW__KY;       ! up-arrow
Default  PREV__TX   = PKEY__TX;         ! "P = previous"

Default  SELN1__KY  = RETURN__KY;       ! return
Default  SELN2__KY  = RARROW__KY;       ! right-arrow
Default  SELN__TX   = RKEY__TX;         ! "RETURN = read subject"

Default  PGDN1__KY  = '+';
Default  PGDN2__KY  = PGDN1__KY;
Default  PGDN__TX   =                     "+ = next page";

Default  PGUP1__KY  = '-';
Default  PGUP2__KY  = PGUP1__KY;
Default  PGUP__TX   =                     "    - = previous page";

Default  MEND1__KY  = 'E';
Default  MEND2__KY  = MEND1__KY;
Default  MEND__TX   =                     "E = end of menu";

Default  MBEG1__KY  = 'B';
Default  MBEG2__KY  = MBEG1__KY;
Default  MBEG__TX   =                     "B = beginning of menu";

Default  HINT1__KY  = 'H';
Default  HINT2__KY  = RARROW__KY;
Default  HINT3__KY  = DARROW__KY;
Default  HINT__TX   =                     "H = next hint";

! ------------------------------------------------------------------------------

Constant FULLEMBLAZE_NEVER     = 0; ! No extra keys listed.
Constant FULLEMBLAZE_SOMETIMES = 1; ! Extra keys only on multi-page menus.
Constant FULLEMBLAZE_ALWAYS    = 2; ! Extra keys always listed.
Global   FullEmblaze = FULLEMBLAZE_SOMETIMES;

Global screen_width;    ! Width in characters.
Global screen_height;   ! Height in lines.
Global top_menu;        ! Object ID of top-level menu.

Constant TEMPBUFFERSIZE 128;
Array temp_buffer buffer TEMPBUFFERSIZE;

! Dummy object used when re-ordering children.
! getHints property (v7 compatibility) used here only to satisfy compiler.
#Ifndef temp_pile; Object temp_pile with getHints; #Endif;

! ------------------------------------------------------------------------------
! Menu objects must be of class Menu. Option objects can be of any class,
! though an Option class is provided for compatibility with other menu systems.
! LineGap objects simply force a blank line between two menu items.
! HintOption objects define a set of incremental hints.

Class   Menu        with select [; return ShowMenu(self); ];

Class   LineGap     has locked;

Class   Option;

Class   HintOption  with number 1;

! ------------------------------------------------------------------------------
! General Menu utility routines.

! Make object o the oldest child of object p (and thus at bottom of a menu).
[ MenuMoveToBottom o p;
    while (child(p)) move child(p) to temp_pile;
    move o to p;
    while (child(temp_pile)) move child(temp_pile) to p;
];

! Wait for keypress, re-map Glulx controls, return (upper-case) character.
[ MenuKey x;
    #Ifdef TARGET_GLULX;
    if (x) x = KeyCharPrimitive(gg_statuswin, true);
    else   x = KeyCharPrimitive(gg_mainwin, true);
    switch (x) {
      $80000000:        x = RESIZE__KY;
      keycode_End:      x = MEND2__KY;
      keycode_Home:     x = MBEG2__KY;
      keycode_PageDown: x = PGDN2__KY;
      keycode_PageUp:   x = PGUP2__KY;
      keycode_Escape:   x = ESC__KY;
      keycode_Return:   x = RETURN__KY;
      keycode_Down:     x = DARROW__KY;
      keycode_Up:       x = UARROW__KY;
      keycode_Right:    x = RARROW__KY;
      keycode_Left:     x = LARROW__KY;
    }
    #Ifnot; ! TARGET_ZCODE
    @read_char 1 -> x;
    #Endif; ! TARGET_
    return UpperCase(x);
];

! Output at specified position.
[ MenuPrintAtPos x lin col;
    MoveCursor(lin, col);
    PrintOrRunVar(x, true);
];

! Output the menu name and optional page count.
[ MenuPagePages mnu page pages
    tmp;
    tmp = (mnu has open);   ! Mark the menu open while we print its header,
    give mnu open;          ! so that short_name can optionally change the name.
    print (name) mnu;
    if (~~tmp) give mnu ~open;
    if (pages > 1)
        print (char) ' ', (char) '[', page, (char) '/', pages, (char) ']';
];

! Print the heading for an Option screen.
[ EmblazeOption opt bar_height page pages
    tmp;

    ! Get screen_width, clear the screen, and split it. Set font style.

    MainWindow();
    ClearScreen(WIN_ALL);
    StatusLineHeight(bar_height);
    MoveCursor(1, 1);
    font off; style reverse;
    #Ifdef TARGET_GLULX;
    glk_set_style(style_User1);
    #Endif; ! TARGET_

    screen_width = ScreenWidth(); spaces screen_width;

    ! Print the option title in the centre.

    #Ifdef TARGET_GLULX;
    tmp = PrintAnyToArray(temp_buffer, TEMPBUFFERSIZE, MenuPagePages, opt, page, pages);
    #Ifnot; ! TARGET_ZCODE
    if (standard_interpreter) {
        @output_stream 3 temp_buffer;
        MenuPagePages(opt, page, pages);
        @output_stream -3;
        tmp = temp_buffer-->0;
    }
    else
        tmp = 0;
    #Endif; ! TARGET_
    if (tmp >= screen_width) tmp = 1;
    else                     tmp = 1 + (screen_width - tmp) / 2;
    MoveCursor(1, tmp); MenuPagePages(opt, page, pages);
];  ! Font style is still set for Header.

! Print the heading for a Menu screen.
[ EmblazeMenu mnu bar_height page pages infull;
    if (infull) infull = 2;

    EmblazeOption(mnu, bar_height, page, pages);

    MoveCursor(2+infull, 1); spaces screen_width;
    MenuPrintAtPos(NEXT__TX, 2+infull, 2);
    MenuPrintAtPos(PREV__TX, 2+infull, screen_width-12);
    MoveCursor(3+infull, 1); spaces screen_width;
    MenuPrintAtPos(SELN__TX, 3+infull, 2);
    if (top_menu == mnu) MenuPrintAtPos(QUIT1__TX, 3+infull, screen_width-17);
    else                 MenuPrintAtPos(QUIT2__TX, 3+infull, screen_width-17);

    if (infull) {
        MoveCursor(2, 1); spaces screen_width;
        MenuPrintAtPos(MEND__TX, 2, 2);
        MenuPrintAtPos(MBEG__TX, 2, screen_width-21);
        MoveCursor(3, 1); spaces screen_width;
        MenuPrintAtPos(PGDN__TX, 3, 2);
        MenuPrintAtPos(PGUP__TX, 3, screen_width-21);
    }

    if (MENU_TOPLINE) {
        style roman;
        #Ifdef TARGET_GLULX;
        glk_set_style(style_Normal);
        #Endif; ! TARGET_
        MoveCursor(5+infull, 1);
        switch (metaclass(MENU_TOPLINE)) {
          Routine: MENU_TOPLINE.call(mnu);
          String:  print (string) MENU_TOPLINE;
          default: "[Invalid MENU_TOPLINE.]";
        }
        style reverse;
        #Ifdef TARGET_GLULX;
        glk_set_style(style_User1);
        #Endif; ! TARGET_
    }
];

! ------------------------------------------------------------------------------
! Utility routines specific to transparent menus,

! Count the lines of a transparent menu and its sub-menus.
[ MenuCountLines mnu
    obj i;
    i = 0;
    objectloop (obj in mnu)
        if (obj hasnt concealed) {
            i++;
            if (obj ofclass Menu && obj has transparent && obj has open)
                i = i + MenuCountLines(obj);
        }
    return i;
];

! Find the next item after the specified one on a transparent menu.
[ MenuNext mnu item
    obj;
    if (item ofclass Menu && item has open && item has transparent && item hasnt concealed)
        for (obj=child(item) : obj : obj=sibling(obj))
            if (obj hasnt concealed) return obj;
    while (item && item ~= mnu) {
        for (obj=sibling(item) : obj : obj=sibling(obj))
            if (obj hasnt concealed) return obj;
        item = parent(item);
    }
    rfalse;
];

! Ensure that branches of transparent menus start off closed.
! There's no reason they should be open, but it's simple to check anyway.
[ MenuCloseBranches mnu
    obj;
    objectloop (obj in mnu)
        if (obj ofclass Menu && obj has transparent && obj hasnt concealed) {
            MenuCloseBranches(obj);
            give obj ~open;
        }
];

! Return the number of generations between item and mnu.
! If parent(item)==mnu, returns 1, if parent(parent(item))==mnu, returns 2. And so on.
! If item is not ultimately inside mnu, returns 0.
[ MenuDescent mnu item
    i;
    for (i=0 : item : i++) {
        if (item == mnu) return i;
        item = parent(item);
    }
    rfalse;
];

! Get the position (index, from 0) of an item in a transparent Menu.
! Return -1 if item not found.
[ MenuGetPosOfItem mnu item
    pos obj;
    obj = child(mnu);
    while (obj && obj has concealed) obj = sibling(obj);
    if (obj == 0) return -1;
    pos = 0;
    while (obj) {
        if (obj == item) return pos;
        pos++;
        obj = MenuNext(mnu, obj);
    }
    return -1;
];

! Get the item at a given position in a transparent Menu.
! Return 0 if it runs out of items.
[ MenuGetItemAtPos mnu pos
    obj;
    if (pos < 0) rfalse;
    obj = child(mnu);
    while (obj && obj has concealed) obj = sibling(obj);
    while (obj && pos > 0) {
        pos--;
        obj = MenuNext(mnu, obj);
    }
    return obj;
];

! ------------------------------------------------------------------------------

! Display a Menu object.
! Call with ShowMenu(mnu,true) to skip the 'before' checking.

[ ShowMenu mnu tmp
    lines page pages pos old_pos obj pkey page_lines cur_item top_obj cur_menu infull;

    if (tmp == false) {
        tmp = RunRoutines(mnu, before);
        if (tmp) return tmp;
    }
    if (top_menu == 0) top_menu = mnu;
    cur_item = 0;
    cur_menu = mnu;

  .TotalRedisplay;

    ! cur_item==0 means either 1) the routine has just started or 2) the menu's in a mess.
    ! In either case, get everything ready from scratch.

    if (cur_item && cur_item notin mnu) {
        cur_menu = parent(cur_item);
        if (~~IndirectlyContains(mnu, cur_menu)) {
            cur_item = 0;
            cur_menu = mnu;
            MenuCloseBranches(mnu);
        }
    }
    else
        if (cur_item == 0 && cur_menu ~= 0 or mnu)
            if (~~IndirectlyContains(mnu, cur_menu)) cur_menu = mnu;
    if (cur_menu == 0) cur_menu = mnu;
    if (cur_item && cur_item notin cur_menu) cur_item = 0;

    top_obj = 0; ! force top_obj to be recalculated
    tmp = 2;

    ! Count the Options.

    lines = MenuCountLines(mnu);
    if (lines == 0) jump ExitMenu;

    ! Set cur_item (if it's unset)

    for (obj=child(cur_menu) : obj && ~~cur_item : obj=sibling(obj))
        if (obj hasnt concealed or locked) cur_item = obj;
    if (cur_item == 0) jump ExitMenu;

    ! Find the available height.

    #Ifdef TARGET_GLULX;
    StatusLineHeight(1); ! set the status line to 1 so we can measure the screen.
    glk_window_get_size(gg_mainwin, gg_arguments, gg_arguments+WORDSIZE);
    screen_height = gg_arguments-->1;
    ! screen_height has been set to the sum of the heights of the two display windows.
    ! This'll actually be one less than the actual screen height.
    ! So, the mainwin will be visible at the bottom.
    #Ifnot; ! TARGET_ZCODE
    screen_height = 0->32;
    #Endif; ! TARGET_

    ! Check height is vaguely sensible.

    #Ifdef TARGET_GLULX;
    if (screen_height <= 0 || screen_height >= 250) {
        ! There's at least one Glulx 'terp that will tell you the height of the status window
        ! but not the height of the main window. So try and expand the status window and
        ! hopefully we can see how big it actually gets.
        StatusLineHeight(250);
        glk_window_get_size(gg_statuswin, gg_arguments, gg_arguments+WORDSIZE);
        screen_height = gg_arguments-->1;
    }
    #Endif; ! TARGET_
    ! If that didn't work, or if we're in Z-code, we can do little but make a conservative estimate.
    ! 18 lines is enough space to list 11 items, or 8 if fullemblaze is active.
    if (screen_height <= 0 || screen_height >= 250) screen_height = 18;
    switch (FullEmblaze) {
      FULLEMBLAZE_NEVER:
        infull = 0;
      FULLEMBLAZE_ALWAYS:
        infull = 2;
      FULLEMBLAZE_SOMETIMES:
        if (screen_height < 7+lines) infull = 2;
        else                         infull = 0;
    }
    ! A little check here: in the highly unlikely circumstance that your screen is 8 or 9
    ! lines tall, that FullEmblaze information will completely kill your menu.
    ! 8 is the minimum height with the menu in this layout, and that's one option.
    ! One option per page is usable.
    if (screen_height < 10) infull = 0;
    if (screen_height < 8) screen_height = 8;
    if (screen_height < 7+infull+lines) {
        page_lines = screen_height - (7+infull);
        pages = 1 + (lines-1) / page_lines;
    }
    else {
        pages = 1; page = 1;
        screen_height = 7 + infull + lines;
        page_lines = lines;
    }

  .ReDisplay;

    ! Find pos and page.

    pos = MenuGetPosOfItem(mnu, cur_item);
    if (pages > 1) {
        page = 1 + pos / page_lines;
        pos = pos % page_lines;
    }
    EmblazeMenu(mnu, 7+infull+page_lines, page, pages, infull);
    if (cur_menu ~= mnu) MenuPrintAtPos(QUIT3__TX, 3+infull, screen_width-17);
    style roman; font off;
    #Ifdef TARGET_GLULX;
    glk_set_style(style_Normal);
    #Endif; ! TARGET_

    ! Find top_obj for the page

    if (pages > 1 || top_obj == 0) top_obj = MenuGetItemAtPos(mnu, (page-1)*page_lines);
    if (top_obj == 0) jump TotalRedisplay;

    ! Print the options.

    for (tmp=0,obj=top_obj : obj && tmp<page_lines : obj=MenuNext(mnu,obj)) {
        if (~~obj ofclass LineGap) {
            old_pos = 3 + MenuDescent(mnu, obj) * 2;
            pkey = (obj has open);
            give obj ~open;
            MenuPrintAtPos(obj, 7+infull+tmp, old_pos+1);
            if (pkey) {
                give obj open;
                ! Mark open submenus:
                if (obj ofclass Menu && obj has transparent)
                    MenuPrintAtPos(">", 7+infull+tmp, old_pos-1);
            }
        }
        tmp++;
    }
    old_pos = -1;
    tmp = MenuDescent(mnu, cur_item) * 2;

    ! The moving-the-pointer-up-and-down loop

    for (::) {

        ! Draw the pointer.

        if (old_pos ~= pos) {
            if (old_pos>=0) MenuPrintAtPos("  ", 7+infull+old_pos, tmp+1);
            old_pos = pos;
            MoveCursor(7+infull+pos, tmp+1);
            if (cur_menu ~= mnu) print "->";
            else                 print " >";
        }

        ! Wait for a keypress.

        pkey = MenuKey(1);
        ! Pageup on page 1 means "go to top".
        ! Pagedown on the last page means "go to bottom".
        if (pkey == PGUP1__KY or PGUP2__KY && page == 1)   pkey = MBEG1__KY;
        if (pkey == PGDN1__KY or PGDN2__KY &&
           (page == pages || pos+page*page_lines > lines)) pkey = MEND1__KY;

        switch (pkey) {

          RESIZE__KY:                           ! Resize (Glulx only)
            jump TotalRedisplay;

          QUIT1__KY,QUIT2__KY,QUIT3__KY:        ! Quitting, and exiting sub-menus
            tmp = 2;
            if (cur_menu == mnu) jump ExitMenu;
            RunRoutines(cur_menu, after);
            cur_item = cur_menu;
            cur_menu = parent(cur_menu);
            if (cur_menu == 0) cur_menu = mnu;  ! Certainly shouldn't happen
            give cur_item ~open;
            jump TotalRedisplay;

          PGDN1__KY,PGDN2__KY:                  ! Page down
            tmp = 0;
            while (tmp < page_lines && cur_item) {
                if (cur_item hasnt concealed) {
                    tmp++;
                    if (cur_item hasnt locked) obj = cur_item;
                }
                cur_item = sibling(cur_item);
            }
            ! If cur_item is unselectable, there may be more selectable options
            ! beneath it.
            while (cur_item && (cur_item has concealed or locked))
                cur_item = sibling(cur_item);
            ! If there aren't, we'll have to use the last selectable object
            ! we passed, which is obj.
            if (cur_item == 0) cur_item = obj;
            jump Redisplay;

          PGUP1__KY,PGUP2__KY:                  ! Page up
            tmp = 0;
            while (tmp < page_lines && cur_item) {
                if (cur_item hasnt concealed) {
                    tmp++;
                    if (cur_item hasnt locked) obj = cur_item;
                }
                cur_item = elder(cur_item);
            }
            ! Skip up past unselectable options...
            while (cur_item && (cur_item has concealed or locked))
                cur_item = elder(cur_item);
            ! No good? go to the last good option we passed, which is obj.
            if (cur_item == 0) cur_item = obj;
            jump Redisplay;

          MEND1__KY,MEND2__KY:      ! Last selectable option in current (sub?)menu
            cur_item = youngest(cur_menu);
            while (cur_item && (cur_item has concealed or locked))
                cur_item = elder(cur_item);
            pos = MenuGetPosOfItem(mnu, cur_item);
            if (page ~= 1 + pos / page_lines) jump Redisplay;
            pos = pos % page_lines;

          MBEG1__KY,MBEG2__KY:      ! First selectable option in current (sub?)menu
            cur_item = child(cur_menu);
            while (cur_item && (cur_item has concealed or locked))
                cur_item = sibling(cur_item);
            pos = MenuGetPosOfItem(mnu, cur_item);
            if (page ~= 1 + pos / page_lines) jump Redisplay;
            pos = pos % page_lines;

          NEXT1__KY,NEXT2__KY:                  ! Cursor down
            pos++;
            cur_item = sibling(cur_item);
            while (cur_item && (cur_item has concealed or locked)) {
                if (cur_item hasnt concealed) pos++;
                cur_item = sibling(cur_item);
            }
            if (cur_item == 0) {
                cur_item = child(cur_menu);
                while (cur_item && (cur_item has concealed or locked))
                    cur_item = sibling(cur_item);
                pos = MenuGetPosOfItem(mnu, cur_item);
                if (page ~= 1 + pos / page_lines) jump Redisplay;
                pos = pos % page_lines;
            }
            if (pos < 0 || pos >= page_lines) jump ReDisplay;

          PREV1__KY,PREV2__KY:                  ! Cursor up
            pos--;
            cur_item = elder(cur_item);
            while (cur_item && (cur_item has concealed or locked)) {
                if (cur_item hasnt concealed) pos--;
                cur_item = elder(cur_item);
            }
            if (cur_item == 0) {
                cur_item = youngest(cur_menu);
                while (cur_item && (cur_item has concealed or locked))
                    cur_item = elder(cur_item);
                pos = MenuGetPosOfItem(mnu, cur_item);
                if (page ~= 1 + pos / page_lines) jump ReDisplay;
                pos = pos % page_lines;
            }
            if (pos < 0 || pos >= page_lines) jump Redisplay;

          SELN1__KY,SELN2__KY:                  ! Select an item
            tmp = 0;
            if (cur_item ofclass Menu && cur_item has transparent) {
                cur_menu = cur_item;
                give cur_menu open;
                tmp = RunRoutines(cur_menu, before);
                if (tmp == 1) {
                    give cur_menu ~open;
                    cur_item = cur_menu;
                    cur_menu = parent(cur_menu);
                    ! Hold tmp's value...
                }
                if (tmp == 0) {
                    cur_item = MenuNext(mnu, cur_menu);
                    if (cur_item && cur_item in cur_menu)
                         while (cur_item && (cur_item has concealed or locked))
                             cur_item = sibling(cur_item);
                    if (cur_item == 0 || cur_item notin cur_menu) {
                        cur_item = cur_menu;
                        cur_menu = parent(cur_item);
                        give cur_item ~open;
                        continue;
                    }
                    jump TotalRedisplay;
                }
            }
            if (tmp == 0) {
                ! If tmp was set above by a 'before' execution, then we keep it until here,
                ! so it can be considered with these other possibilities.
                if (cur_item ofclass Menu) tmp = cur_item.select();
                else                       tmp = ShowOption(cur_item);
            }
            while (cur_menu ~= mnu or 0 && tmp > 2) {
                tmp--;
                cur_item = cur_menu;
                give cur_menu ~open;
                cur_menu = parent(cur_menu);
            }
            if (tmp > 2) { tmp--; jump ExitMenu; }
            if (tmp < 2) {
                L__M(##Miscellany, 53);
                MenuKey(0);
            }
            if (cur_item notin cur_menu) cur_item = 0;
            while (cur_item && (cur_item has concealed or locked))
                cur_item = sibling(cur_item);
            ! If cur_item==0, it will be set in TotalRedisplay
            jump TotalRedisplay;
        } ! end of switch (pkey)
    } ! end of pointer-loop

  .ExitMenu;

    RunRoutines(mnu, after);
    if (top_menu == mnu) {
        top_menu = 0;
        MainWindow();
        ClearScreen(WIN_ALL);
        StatusLineHeight(1);
        #Ifdef TARGET_GLULX; new_line; #Endif;
        new_line;
        font on;
        ! Look, as long as we're alive and somewhere
        if (parent(player) && location && ~~deadflag) {
            LookSub(1);
            tmp = true;
        }
    }
    return tmp;

]; ! end of ShowMenu

! ------------------------------------------------------------------------------

! Display an Option object.
! Call with ShowOption(opt,true) to skip the 'before' checking.

[ ShowOption opt tmp;
    if (tmp == false) {
        tmp = RunRoutines(opt, before);
        if (tmp) return tmp;
    }
    tmp = true;

    ! Display the heading.

    if (opt ofclass HintOption) {
        EmblazeOption(opt, 2, 1, 1);
        MoveCursor(2, 1); spaces screen_width;
        MenuPrintAtPos(HINT__TX,  2, 2);
        MenuPrintAtPos(QUIT3__TX, 2, screen_width-17);
    }
    else
        EmblazeOption(opt, 1, 1, 1);

    ! Display the Option's description.

    MainWindow();
    #Ifdef TARGET_GLULX; new_line; #Endif;
    new_line;
    style roman; font on;
    #Ifdef TARGET_GLULX;
    glk_set_style(style_Normal);
    #Endif; ! TARGET_
    if (opt.#description || opt provides getHints)  ! v7 compatibility
        if (opt ofclass HintOption) tmp = ShowHintOption(opt);
        else                        tmp = PrintOrRun(opt, description);
    else
        return PrintOrRunVar(OPTION_NOTEXT);

    ! All done.

    RunRoutines(opt, after);
    return tmp;
]; ! end of ShowOption

[ ShowHintOption opt
    numhints i;
    if (opt provides getHints)                      ! v7 compatibility
        numhints = opt.getHints(0);                 ! v7 compatibility
    else                                            ! v7 compatibility
        numhints = opt.#description / WORDSIZE;
    for (i=1 : i<=opt.number : i++) ShowNextHint(opt, i, numhints);
    while (opt.number <= numhints) {
        if (MenuKey(0) ~= HINT1__KY or HINT2__KY or HINT3__KY) return 2;
        if (opt.number == numhints) return PrintOrRunVar(HINT_NOMORE);
        (opt.number)++;
        ShowNextHint(opt, opt.number, numhints);
    }
    rtrue;
];

[ ShowNextHint opt hnt numhints;
    if (hnt < 10 && numhints >= 10) print (char) ' ';
    print (char) '[', hnt;
    if (opt hasnt transparent) print (char) '/', numhints;
    print (char) ']', (char) ' ';
    if (opt provides getHints)                      ! v7 compatibility
        opt.getHints(hnt);                          ! v7 compatibility
    else {
        hnt = opt.&description-->(hnt-1);
        switch (metaclass(hnt)) {
          Object:  PrintOrRun(hnt, description);
          Routine: hnt();
          String:  print (string) hnt, "^";
          default: "[Invalid HintOption.]";
        }
    }
    #Ifndef HINT_NOBLANKLINES;
    new_line;
    #Endif;
];

! ------------------------------------------------------------------------------

#Ifdef INCLUDE_SWITCHOPTION;

! When a SwitchOption is selected from a menu, the only effect is to toggle
! its 'on' attribute.
Class   SwitchOption
  with  short_name [;
            print (object) self;
            if (self has on) print " (on)";
            else             print " (off)";
            rtrue;
        ],
        before [;
            if (self has on) give self ~on;
            else             give self on;
            return 2;
        ],
  has   ~on;

#Endif; ! SWITCHOPTION

! ------------------------------------------------------------------------------

#Endif; ! DMENUS_H

#Ifnot; ! LIBRARY_STAGE ~= VERBLIB
Message "Warning: LIBRARY_STAGE is not VERBLIB";
#Endif; ! Iftrue LIBRARY_STAGE == VERBLIB
! ==============================================================================
