Section 7 Home page Section 9

8. Sliding across Window Contents

The sliders are what give the illusion of windows looking onto a wider space of data. Sliders are complicated to handle, as users have many ways of interacting with them. A slider can be dragged directly. Users can click on the arrows, or in the slider bar. As there are two sliders, this makes for ten functions just to handle the sliders. In addition, when a window is resized or made full, or if the window’s contents change, its dimensions change, and so the positions and size of the sliders must be changed as well.

I will cover the code for sliders and arrows in one go, as it is all related. Also, the vertical and horizontal sliders are essentially the same, code wise, so I shall mainly present code for the vertical slider here: code for the horizontal slider is similar, and contained in the example code.

Because of the large number of events that need handling (10 in total), this part of your program will be the most extensive, and the most complicated. Adding sliders also requires consideration when drawing the contents of the screen.

We shall begin by adding sliders to our program, getting the sliders to show the right size and position, then we modify draw_example to display things correctly, before finally seeing how to respond to all the events.

8.1. Showing the Sliders

Adding the sliders and arrows is done in the parts list when creating our window. We need to add the slider and its two arrows. A complete parts list, including all the previously mentioned windows widgets, is:

NAME|CLOSER|FULLER|MOVER|SIZER|UPARROW|DNARROW|VSLIDE|LFARROW|RTARROW|HSLIDE

The AES makes some space for the sliders, as with the other window widgets, and leaves the remaining room for us to draw in. This is why, when doing operations such as FULL, we use WF_CURRXYWH to get or set the current window dimensions, but, for drawing within the window, we use WF_WORKXYWH to get the current working area of the window.

8.2. Setting the Sliders

Each slider has two parameters: its size and its position. The size must reflect the portion of the window contents we can currently see. The position must reflect where our current window contents are, in relation to the whole.

Before we can start, we need to decide a few details about what our window will be displaying, and how using the sliders will affect what is displayed. For example, for a window displaying text, we would expect a click on the down arrow to move the display up by a single line of text. A click on page down would move the display up by a page of text, moving the currently bottom line to the top of the screen. The vertical slider should reflect the number of lines of text displayed in proportion to the total number of lines available to display. Similarly, the horizontal arrows would move the text by a character, in either direction.

Hence, the first two details we need to decide are the size of a vertical step, and the size of a horizontal step. For a text display, this will be the height and width of a character, respectively. For graphical displays, other kinds of step size might be appropriate: in the Sokoban program (see later), I chose the size of a displayed cell as the step size; when displaying an image, perhaps a single pixel would be the required step size.

This step size is an important feature of the window: I store it in the win_data structure, and set it when creating the window.

Four other items of information are also stored in win_data. These are:

  1. the total number of lines shown (the vertical height of the display);

  2. the maximum number of characters in a line (the horizontal width of the display);

  3. the current vertical position, the number of lines down in the display; and

  4. the current horizontal position, the number of characters across in the display.

These size items are added to win_data as follows:

struct win_data {
        int handle; /* window handle */

        int lines_shown; /* number of lines shown in current display */
        int colns_shown; /* number of columns shown in current display (longest line) */
        int vert_posn; /* number of lines down of vertical scroll bar */
        int horz_posn; /* number of characters from left of horizontal scroll bar */

        int cell_h; /* height of char/cell in window */
        int cell_w; /* width of char/cell in window */

        /* REMAINING SLOTS NOT SHOWN */
}

The following diagram illustrates each of these six slots (the blue rectangle illustrates the displayed window onto the background text):

images/sliders.png

The size of a slider must indicate the proportion of text actually shown against the total amount of text that could be shown. In our example, the poem has a number of lines to it. If all of the poem is showing in the window, then the vertical slider must be at its maximum - there is nothing to scroll. If however the window is shorter, so only some of the lines are showing, then the slider size must reflect the proportion of text showing against the total number of lines in the poem. Similar considerations apply to the horizontal slider.

The following function returns a number from 0 to 1000 based on the required size of the slider. This is calculated based on the number of lines or characters available (which the size of window would permit) and the number of lines or characters actually shown (which are displayed by the draw code).

int slider_size (int num_available, int num_shown) {
        int result;

        /* in case number shown is smaller than those available */
        if (num_available >= num_shown) { /* all visible                   <1> */
                result = 1000; /* so slider complete */
        } else {
                result = (1000 * (long)num_available) / num_shown;      // <2>
        }

        return result;
}
  1. Check if all the display will fit in the window: result is 1000 if so.

  2. Otherwise, find the fraction of 1000, using long multiplication for accuracy.

The position of the slider depends on three parameters: the number of lines or characters available, the number shown, and the current offset from the top or left. Again, if the number available exceeds the number to be shown, then the slider is simply at position 0 (the top or left).

Before working out the proportion, we need to compute the "scrollable region". This is the number of positions along which the slider can move. For example, if we have a 500 line text, and 50 lines can be displayed at a time, the top line can be moved from line 1 to line 450: so the scrollable region for the vertical slider is 450 lines.

The position of the slider is then computed as 1000 * offset / scrollable_region.

As the ST does not support floating point calculations, the code below computes that fraction using integer division and modulus, and can be used everywhere.

int slider_posn (int num_available, int num_shown, int offset) {
        int result;

        /* in case number shown is smaller than those available */
        if (num_available >= num_shown) { /* all visible */
                result = 0; /* so slider complete, and show bar at top position */
        } else {
                /* number of positions scrollbar can move across: must be positive due to above check */
                int scrollable_region = num_shown - num_available;
                int tmp1 = offset / scrollable_region;
                int tmp2 = offset % scrollable_region;

                result = (1000 * (long)tmp1) + ((1000 * (long)tmp2) / scrollable_region);
        }

        return result;
}

Given the above two functions, it is now straightforward to update the sliders. Our function simply finds the available numbers of lines and columns, based on the work area of the screen, and then each slider size and position can be updated. The offset to the sliders is recorded in wd→vert_posn and wd→horz_posn: these will be updated when the events, such as moving down a line, are processed.

void update_sliders (struct win_data * wd) {
        int lines_avail, cols_avail;
        int wrkx, wrky, wrkw, wrkh;

        wind_get (wd->handle, WF_WORKXYWH, &wrkx, &wrky, &wrkw, &wrkh);
        lines_avail = wrkh / wd->cell_h;        // <1>
        cols_avail = wrkw / wd->cell_w;         // <2>

        /* handle vertical slider                  <3> */
        wind_set (wd->handle, WF_VSLSIZE,
                  slider_size (lines_avail, wd->lines_shown), 0, 0, 0);
        wind_set (wd->handle, WF_VSLIDE,
                  slider_posn (lines_avail, wd->lines_shown, wd->vert_posn), 0, 0, 0);

        /* handle horizontal slider                <4> */
        wind_set (wd->handle, WF_HSLSIZE,
                  slider_size (cols_avail, wd->colns_shown), 0, 0, 0);
        wind_set (wd->handle, WF_HSLIDE,
                  slider_posn (cols_avail, wd->colns_shown, wd->horz_posn), 0, 0, 0);
}
  1. Find the number of lines available to show in the current window, by dividing the current height by the number of pixels in a given line.

  2. Find the number of columns available to show in the current window, by dividing the current width by the number of pixels in a given character.

  3. Update the vertical slider size and position.

  4. Update the horizontal slider size and position.

8.3. Drawing a Window with Sliders

There is no point having sliders if our window contents do not respond to the position of the sliders. In addition, our drawing code must record the amount of vertical and horizontal space taken up by the contents, to use when setting the size and position of the sliders.

To record the amount of space taken up by the contents, I update the number of lines shown every time a line of text is shown, and the number of columns shown by the maximum width of text.

The vertical slider position is accounted for by ignoring the number of lines in the display equal to the slider position.

The horizontal slider position is accounted for by printing the text offset to the left by the slider position.

Note, I don’t worry about text being displayed off screen, as this is taken care of by the clipping, set in draw_interior.

void draw_example (int app_handle, struct win_data * wd, int x, int y, int w, int h) {
        int i = 0;
        int lines_to_ignore = wd->vert_posn;  // <1>
        int cur_y = y + wd->cell_h;           // <2>

        wd->lines_shown = 0;                  // <3>
        wd->colns_shown = 0;                  // <4>

        while (wd->poem[i] != 0) {
                if (lines_to_ignore == 0) {   // <5>
                        v_gtext (app_handle, x+wd->cell_w*(1-wd->horz_posn), cur_y, wd->poem[i]);

                        if (strlen(wd->poem[i])+2 > wd->colns_shown) {  // <6>
                                wd->colns_shown = strlen (wd->poem[i]) + 2;
                        }

                        cur_y += wd->cell_h;  // <7>
                } else {
                        lines_to_ignore -= 1; // <8>
                }

                wd->lines_shown += 1;         // <9>

                i = i + 1;
        }

}
  1. We have to leave out the lines above the current vertical slider position.

  2. The next y position in the window at which to display text.

  3. Clear the current lines shown in the display (height).

  4. Clear the current columns shown in the display (width).

  5. When we have ignored the lines above the current vertical slider position, we can display the next line of text.

  6. Update the current columns shown if the current line is wider.

  7. Move the y position down a line.

  8. Otherwise, reduce the number of lines to ignore by 1.

  9. Increase the number of lines shown in the display.

The display of the window contents will depend on your window. The example above is suitable for lines of text. For a graphical display, you may have a fixed size for the window, which means some of the calculations can be simplified.

8.4. Updating the Sliders

The sliders must be checked and changed whenever the window size or contents are altered. In practice, we need only alter the sliders when we update the contents of the window. The most suitable place for this is at the end of draw_interior.

void draw_interior (struct win_data * wd, GRECT clip) {

        /* REST OF FUNCTION */

        set_clip (false, clip);
        update_sliders (wd);            // <1>
        graf_mouse (M_ON, 0L);
}
  1. Call the function to update the sliders size and position, based on the revised height and width of the displayed contents.

The sliders need adapting also whenever the window size changes. We do this within do_sized by changing the horz_posn and vert_posn values to reflect the change in size of the window. If, for example, a window is made larger, then it will display more information, and we can reduce the number of lines off the top of the window.

The sliders themselves will be updated automatically as setting a new window size will trigger a redraw event, which also updates the slider sizes and positions. Unfortunately, that redraw event will only apply to the newly exposed parts of the window: if we have moved the horizontal or vertical position, then all the contents will need redrawing, and we will need to trigger that redraw ourselves.

void do_sized (struct win_data * wd, int * msg_buf) {
        int new_height, new_width;
        bool changed;                           // <1>

        if (msg_buf[6] < MIN_WIDTH) msg_buf[6] = MIN_WIDTH;
        if (msg_buf[7] < MIN_HEIGHT) msg_buf[7] = MIN_HEIGHT;

        // <2>
        new_height = (msg_buf[7] / wd->cell_h) + 1; /* find new height in characters */
        new_width = (msg_buf[6] / wd->cell_w) + 1;  /* find new width in characters */

        /* if new height is bigger than lines_shown - vert_posn,
           we can decrease vert_posn to show more lines */
        if (new_height > wd->lines_shown - wd->vert_posn) {     // <3>
                wd->vert_posn -= new_height - (wd->lines_shown - wd->vert_posn);
                if (wd->vert_posn < 0) wd->vert_posn = 0;
                changed = true;                                 // <4>
        }
        /* if new height is less than lines_shown - vert_posn,
           we leave vertical position in same place,
           so nothing has to be done                               <5> */

        /* similarly, if new width is bigger than colns_shown - horz_posn,
           we can decrease horz_posn to show more columns */
        if (new_width > wd->colns_shown - wd->horz_posn) {      // <6>
                wd->horz_posn -= new_width - (wd->colns_shown - wd->horz_posn);
                if (wd->horz_posn < 0) wd->horz_posn = 0;
        }

        wind_set (wd->handle, WF_CURRXYWH,                      // <7>
                  msg_buf[4], msg_buf[5], msg_buf[6], msg_buf[7]);

        if (changed) {                                          // <8>
                GRECT rec;

                wind_get (wd->handle, WF_WORKXYWH,
                          &rec.g_x, &rec.g_y, &rec.g_w, &rec.g_h);
                do_redraw (wd, &rec);
        }
}
  1. Introduce a flag to indicate if the top or side of display has changed.

  2. new_height and new_width are the number of characters that will fit vertically and horizontally in the new window size.

  3. If the height has increased, we can show more lines, and so reduce the number of lines off the top of the window.

  4. Set the flag to trigger an update, as the top of the display has moved.

  5. If the height has decreased, we leave the top line where it is.

  6. Similarly, alter the left column only if the window has got wider.

  7. Setting the new window size will trigger a redraw event, updating the new part of the display and sliders, if required.

  8. Request a redraw of the entire window.

8.5. Slider Events

The sliders can be moved directly, by dragging them with the mouse. These events have their own message type, one for the vertical and one for the horizontal slider. msg_buf[4] contains the new position of the slider, as a number between 0 (top/left) and 1000 (bottom/right).

case WM_VSLID:
        wind_set (msg_buf[3], WF_TOP, 0, 0);    // <1>
        do_vslide (wd, msg_buf[4]);             // <2>
        break;

case WM_HSLID:
        wind_set (msg_buf[3], WF_TOP, 0, 0);
        do_hslide (wd, msg_buf[4]);
        break;
  1. I force the window to the top when moving the slider: in Mint, it is possible to grab the slider and move it, leaving the window in the background. I think this is a flaw, hence the forced top.

  2. Call the code to handle the slider, along with its new position.

The code to handle the slider update requires us to compute any change to the top line. The new vertical position reflects the location of the new slider position along the scrollable region.

void do_vslide (struct win_data * wd, int posn) {
        GRECT r;
        int lines_avail;

        wind_get (wd->handle, WF_WORKXYWH, &r.g_x, &r.g_y, &r.g_w, &r.g_h);
        lines_avail = r.g_h / wd->cell_h;                       // <1>
        wd->vert_posn = (posn * (long)(wd->lines_shown - lines_avail)) / 1000; // <2>
        if (wd->vert_posn < 0) wd->vert_posn = 0;               // <3>
        wind_set (wd->handle, WF_VSLIDE, posn, 0, 0, 0);        // <4>
        do_redraw (wd, &r);                                     // <5>
}
  1. Find the new number of lines available.

  2. Compute the new vertical position in the displayed text.

  3. Ensure this new position is valid.

  4. Update the position of the vertical slider.

  5. Redraw the contents of the window (this is not automatic).

The code for handling the horizontal slider is very similar.

8.6. Arrow Events

The remaining events are all known as "arrow" events: the AES sends the application a message that an arrow event has occurred, and passes the arrow type in msg_buf[4].

case WM_ARROWED:
        wind_set (msg_buf[3], WF_TOP, 0, 0); /* bring to the top   <1> */
        do_arrow (wd, msg_buf[4]);                              // <2>
        break;
  1. As with the sliders, we force the window to the top if the arrows are used.

  2. Call out to the arrow handling code, with the arrow type as a parameter.

do_arrow simply calls the appropriate function based on the arrow type.

There are 8 arrow types. In each slider we can move in either direction by one step, or by a "page". The horizontal slider arrow events are handled identically to the vertical ones, except with the obvious change in orientation. For the vertical slider, the move up or move down is again the same, except for minor changes. I shall describe here just the UPLINE event, which moves the window down by a single step, and the UPPAGE event, which moves the window down by a whole page of text. We start with UPPAGE, which is simpler:

/* This function is called in response to WA_UPPAGE arrow type */
void do_uppage (struct win_data * wd) {
        GRECT r;
        int lines_avail;

        wind_get (wd->handle, WF_WORKXYWH, &r.g_x, &r.g_y, &r.g_w, &r.g_h);
        lines_avail = r.g_h / wd->cell_h;               // <1>
        wd->vert_posn -= lines_avail;                   // <2>
        if (wd->vert_posn < 0) wd->vert_posn = 0;       // <3>
        do_redraw (wd, &r);                             // <4>
}
  1. Compute the amount of text available on the screen in the vertical direction.

  2. Alter the vertical position by a screenful of text.

  3. Check we have not gone too far: 0 is the top.

  4. Redraw the window contents.

We could, in theory, treat UPLINE in an identical manner. Instead of changing wd→vert_posn by a page of text, we would change it by 1. The only aesthetic problem is that it makes the display flicker: there is a better technique, which gives smooth scrolling. The better technique copies the unchanged, existing image down by one line, and then simply redraws the new exposed line.

The following function uses this better technique. The first four lines compute the new vert_posn in the same way as for do_uppage. We then set up a call to vro_cpyfm. This VDI function copies screen data from one place to another. In pxy[0-3] we place the location of the start display, and in pxy[4-7] we place the location of the end display, which is the same location but one line down. Finally, we adjust the rectangle so we redraw just the top, exposed parts of the display.

/* This function is called in response to WA_UPLINE arrow type */
void do_upline (struct win_data * wd) {
        FDB s, d;
        GRECT r;
        int pxy[8];

        if (wd->vert_posn == 0) return; /* already at top of screen  <1> */
        wind_get (wd->handle, WF_WORKXYWH, &r.g_x, &r.g_y, &r.g_w, &r.g_h);
        wd->vert_posn -= 1;
        if (wd->vert_posn < 0) wd->vert_posn = 0;

        set_clip (true, r);                             // <2>
        graf_mouse (M_OFF, 0L);
        s.fd_addr = 0L;
        d.fd_addr = 0L;
        pxy[0] = r.g_x;                                 // <3>
        pxy[1] = r.g_y + 1;
        pxy[2] = r.g_x + r.g_w;                         // <4>
        pxy[3] = r.g_y + r.g_h - wd->cell_h - 1;
        pxy[4] = r.g_x;                                 // <5>
        pxy[5] = r.g_y + wd->cell_h + 1;
        pxy[6] = r.g_x + r.g_w;                         // <6>
        pxy[7] = r.g_y + r.g_h - 1;
        vro_cpyfm (wd->handle, S_ONLY, pxy, &s, &d);    // <7>

        graf_mouse (M_ON, 0L);                          // <8>
        set_clip (false, r);

        r.g_h = 2*wd->cell_h; /* draw the height of two rows at top <9> */
        do_redraw (wd, &r);                             // <10>
}
  1. If we are already at the top of the screen, there is nothing to do, so return.

  2. The copy affects the screen, so set a clip region to our window, and disable the mouse.

  3. Top left of work area.

  4. Bottom right of work area, less one row.

  5. New top left area, which is original top left down one row.

  6. Bottom right or work area.

  7. Perform the move on the screen.

  8. Enable the mouse, and remove the clip region.

  9. Constrain the redraw to the top two rows.

  10. Redraw the exposed display area.

The above two functions are repeated four times, for the four different directions of movement. See the source code for all the variations, and also do_arrow.

8.7. Sample Program: Version 5

This version includes all the functions required for handling the sliders. As can be seen by comparing versions 4 and 5, the code for the sliders dominates the final program: version 4 has approximately 240 lines of code, whereas version 5 has around 550 lines, more than double the size.