[comp.sys.mac.hypercard] Scroll Bars in HyperTalk

davidl@inteloc.intel.com (David Levine) (02/17/88)

This article shows you one way to do a scroll bar entirely in HyperTalk.  

First, create your scrolling text field wherever you want it, and give it
a name.  (I called mine "test".)  Now use Command-Shift-3 to take a screen
snapshot.  Using Artisto or some such DA, copy the scroll bar from the 
screen dump and paste it over the scroll bar of the scrolling field.
Make it opaque.  

Create three Transparent buttons exactly corresponding to the up-arrow 
region, gray scrolling region, and down-arrow region.  Create a Rectangular
button exactly corresponding to the scroll bar's thumb (the white "elevator" 
box).  Call this last button "thumb".  The sizes of the buttons in pixels 
should be as follows:

  Arrows: 17 across by 16 high
  Scrolling region: 17 across by (height of field - 30) high
  Thumb: 15 across by 16 high

Using the appropriate gray, paint over the elevator box in the picture of 
the scroll bar.  Now you have a scrolling field, with a picture of the 
scroll bar (sans thumb) pasted over its scroll bar, and four buttons
corresponding to the active parts of the scroll bar over that.

Now you can start scripting.  

Assign the following script to the scrolling field:

    on closeField
      global contentHeight, fieldHeight, maxScroll, minThumb, maxThumb
	-- all updated whenever field contents change

      get the number of lines in card field "test" 
	-- too bad "in me" doesn't work...
      multiply it by the textHeight of me
      put it into contentHeight 
	-- height of field contents in pixels
	-- ONLY VALID IF FIELD CONTAINS NO SOFT RETURNS

      get the rect of me
      put (item 4 of it - item 2 of it) into fieldHeight
        -- height of field on screen, in pixels

      put (contentHeight - fieldHeight) + <Option-Return>
      (the textHeight of me - the textSize of me) - 2 into maxScroll
      if maxScroll < 0 then put 0 into maxScroll
	-- maximum pixels that can scroll off top of field
	-- ONLY VALID IF FIELD CONTAINS NO SOFT RETURNS

      put (item 2 of the rect of me + 24) into minThumb
	-- minimum valid Y coordinate (closest to top of screen)
	-- for loc (center) of "thumb" button: 24 is the height 
	-- of the scroll arrow region plus 1/2 height of thumb

      put (item 4 of the rect of me - 24) into maxThumb
	-- maximum valid Y coordinate (closest to bottom of screen)
	-- for loc (center) of "thumb" button, as for minThumb

      send setPos to card button "thumb"
	-- reset thumb position based on current field contents
    end closeField

Assign the following script to the up-arrow button:

    on mouseStillDown
      global maxThumb, minThumb, maxScroll -- set by closeField handler

      get the scroll of card field "test"
      subtract 40 from it
      if it < 0 then
        put 0 into it
	  -- don't scroll past top of field
      end if
      set the scroll of card field "test" to it
      send setPos to card button "thumb"
    end mouseStillDown

Assign the following script to the down-arrow button:

    on mouseStillDown
      global maxScroll -- set by closeField handler

      get the scroll of card field "test"
      add 40 to it
      if it > maxScroll then
        put maxScroll into it
	  -- don't scroll past bottom of field
      end if
      set the scroll of card field "test" to it
      send setPos to card button "thumb"
    end mouseStillDown

Assign the following script to the button corresponding to the gray
scrolling region:

    on mouseUp
      global fieldHeight, maxScroll -- set by closeField handler

      get the mouseLoc
      if item 2 of it < item 2 of the rect of card button "thumb" then
	  -- user clicked above top of thumb
        get the scroll of card field "test"
        subtract (fieldHeight - 10) from it
	  -- scroll by something less than a full field
        if it < 0 then
          put 0 into it
	    -- don't scroll past top of field
        end if
      else if item 2 of it > item 4 of the rect of card button "thumb" then
	  -- user clicked below bottom of thumb
        get the scroll of card field "test"
        add (fieldHeight - 10) to it
        if it > maxScroll then
          put maxScroll into it
	    -- don't scroll past bottom of field
        end if
      else
	  -- user clicked in thumb, don't do anything
        get the scroll of card field "test"
      end if

      set the scroll of card field "test" to it
      send setPos to card button "thumb"
    end mouseUp

Assign the following script to the button "thumb":

    on mouseStillDown
      global maxThumb, minThumb, maxScroll -- set by closeField handler
	-- this handler lets the user drag the button up and down
	-- and sets the scroll of the field accordingly

      if maxScroll = 0 then
	  -- field has nothing to scroll, don't move
        set the loc of me to item 1 of the loc of me, minThumb
        exit mouseStillDown
      end if

      get the mouseLoc
      put item 1 of the loc of me into item 1 of it
	-- only allow vertical motion
      if item 2 of it > maxThumb then
	  -- don't go below bottom of scroll region
        put maxThumb into item 2 of it
      end if
      if item 2 of it < minThumb then
	  -- don't go above top of scroll region
        put minThumb into item 2 of it
      end if
      set the loc of me to it

      get item 2 of the loc of me
      subtract minThumb from it
      divide it by (maxThumb - minThumb)
      multiply it by maxScroll
      set the scroll of card field "test" to trunc(it)
        --
	-- as the Y coordinate of "the loc of me" varies from minThumb
	-- to maxThumb, "the scroll of card field test" varies from
	-- maxScroll to 0, according to the formula
	-- 
        --           thumb - minThumb
        -- scroll = ------------------- * maxScroll
        --          maxThumb - minThumb
        --
    end mouseStillDown

    on setPos
      global maxThumb, minThumb, maxScroll -- set by closeField handler
	-- this handler sets the position of the button based on
	-- the current scroll of the field

      if maxScroll = 0 then
	  -- field has nothing to scroll, set to top of scroll bar
        set the loc of me to item 1 of the loc of me, minThumb
        exit setPos
      end if

      put the scroll of card field "test" into it
      multiply it by (maxThumb - minThumb)
      divide it by maxScroll
      add minThumb to it
      put trunc(it) into temp
      get the loc of me
      put temp into item 2 of it
      put item 1 of the loc of me into item 1 of it
        --
	-- as "the scroll of card field test" varies from maxScroll to 0,
	-- the Y coordinate of "the loc of me" varies from minThumb
	-- to maxThumb, according to the formula
	-- 
        --         scroll * (maxThumb - minThumb)
        -- thumb = ------------------------------ + minThumb
        --                   maxScroll
        -- 
      if item 2 of it > maxThumb then
        put maxThumb into item 2 of it
	  -- don't go below bottom of scroll region
      end if
      if item 2 of it < minThumb then
        put minThumb into item 2 of it
	  -- don't go above top of scroll region
      end if
      set the loc of me to it
    end setPos

Finally, assign the following script to the card:

    on openCard
      send closeField to card field "test" -- update global variables
    end openCard

That's all.  You now have a simulation of scroll bars in HyperTalk.  You
can extend these scripts to scroll several fields in parallel.  You could
also adapt this code to provide horizontal scroll bars and other
nonstandard controls.

A few words of explanation:

    Most of the buttons need to know certain characteristics of the field, so 
    I decided to store those characteristics in global variables.  These 
    globals are all maintained by the field itself: they are automatically 
    updated whenever the field contents are changed (closeField message).  I 
    put a 'send closeField to card field "test"' in the openCard handler to 
    be sure these variables are all updated properly before they are needed.  
    The "thumb" button similarly maintains its own position, using the 
    user-defined setPos message.  

    The constant 40 in the up-arrow and down-arrow scripts was chosen to 
    get a reasonable approximation of the speed of a real scroll bar without
    excessive jerkiness.  Feel free to mess with this constant for your own
    purposes.

    The "<Option-Return>" in the closeField handler means an Option-Return 
    (appears on the Mac screen as a "hook" character).

I think everything else should be fairly self-explanatory.

These scripts are not completely bug-free.  For one thing, they don't work
if the field contains any "soft" returns.  They can also get seriously
confused if you change the field contents from all fitting in the visible
part of the field at once at once to not fitting, or vice versa.  (If this
happens, the "thumb" button can vanish: use "set the loc of card button
thumb to 50,50" to get it back on the screen, then drag it back to where
it's supposed to be.)  If you come up with any fixes, let me know.

Now that I've done this, I don't think I'd use it in a for-real stack.  For
one thing, it's much slower and jerkier than real scroll bars.  The behavior
is also somewhat different from real scroll bars (the arrows don't highlight 
when you mouseDown on them; when you drag the thumb, the field scrolls
right away (as opposed to scrolling when you release the mouse)).  If I 
really had to do something like this, I'd do an XCMD.  However, I thought
it might be useful for some purposes (if nothing else, as an example of 
HyperTalk programming).

-- David D. Levine, Technical Writer, Intel Corporation
   ...{decvax,ihnp4,hplabs}!tektronix!ogcvax!inteloa!inteloe!davidl

This article Copyright 1988 by David D. Levine.  All rights reserved.  
Permission to redistribute this information in electronic or printed 
form is granted, provided that such redistribution is not for profit 
and this copyright notice is included.