REBOL
Docs Blog Get-it

How to Handle User Interface Events

Part of: How-To
Revised: 1-Feb-2010
Original: 1-June-2001

Describes how events are handled within the REBOL View system.

Return to: REBOL How To

Contents

How to Run the Examples

To run any of the examples that are shown below, run a text editor and create a REBOL header line such as:

REBOL [Title: "Example"]

Then simply cut the example text and paste it after this line. Save the text file and give it a name, such as example.r. Then run the file just as you would run any REBOL file.

The Feel Object and Its Functions

Every graphical object displayed by REBOL is a FACE. The look and feel of a face is specified by the fields of the object. One field of the object defines the FEEL of the object, and specifies how the object behaves on user input and events. When you type a key on the keyboard or move and click the mouse, it is the FEEL object that determines how the action is handled.

The REBOL/View event system is quite elegant and has evolved over many years prior to release. This is how the Visual Interface Dialect (VID) is able to create a range of behaviors for dozens of user interface objects in only a couple hundred lines of code.

Feel Functions

The entire REBOL user interface system is handled by these four functions:

redraw [face action position]

over [face action position]

engage [face action event]

detect [face event]

These functions each have a specific purpose within the user interface event system:

redrawis called immediately before the face is drawn, allowing you to modify certain attributes of the face before it is shown. Each time the face refreshes its look, or each time it is shown or hidden, this function is called.
overis called whenever the mouse pointer passes over or off of a face. This may happen at a very high rate, because a user interface may consist of hundreds of faces and the user may move the mouse over those faces a lot. That's the reason why this is a single function and is not combined with the engage function. This function should be set to NONE if it is not needed allowing the system to ignore the face as the mouse passes over it.
engageis called whenever an event occurs for a face. This function handles events like mouse down, up, alt-down, double-click, timers, keyboard keys and more.
detectis called whenever any event occurs for a face, or for any of the faces that are contained within it. This allows a face to intercept events that are aimed at lower level graphical objects.

Any face can have a FEEL object with one or more of the above functions. This allows you to handle events from any type of graphical object, including images, text, boxes, or lines.

Note that most objects of the system share feel objects. For example, BUTTON faces share a single FEEL object that specifies the operations of a button. Modifying the FEEL object of a button will modify the FEEL object of all other buttons. Sometimes this effect may not be desired. To avoid the effect you can clone the FEEL object and only modify it for the face that you need.

The Redraw Feel

The REDRAW feel function is quite useful when you need to modify the look of a face immediately before it is drawn. Doing this can minimize the amount of code needed to produce face state effects such as highlighting or changing a graphic when the mouse clicks on a face.

The REDRAW function has the form:

redraw face action position

The arguments of the function are:

faceThe face object being redrawn.
actionA word that indicates the action that occurred on the face: DRAW, SHOW, HIDE.
positionThe X-Y position of the face if the face is iterated. Iterated faces will be covered in another document. For now, you can ignore this argument.

Example Redraw Function

Here is a very simple example that will help you understand the REDRAW function:

view layout [
    the-box: box "A Box" forest feel [
        redraw: func [face act pos] [print act]
    ]
    button "Show" [show the-box]
    button "Hide" [hide the-box]
]

This function will print the action word that is passed each time the REDRAW function is called. When you run the example, you will see the console immediately print:

show
draw

The show occurs when the VIEW function requests that the window be displayed. Then, the draw occurs immediately before the face is rendered.

If you click on the "Show" button, you will see the same two actions occur:

show
draw

This time the show is being done on the face directly.

When you click on the "Hide" button, the output will be:

hide

There is no show, so there is no draw either.

The Over Feel

The OVER feel function senses the mouse passing over a face. It can be used to provide visual feedback that the mouse is over an object, or it can display help information about an item when the user hovers the mouse over it.

The OVER function has the form:

over face action position

The arguments to the OVER function are:

faceThe face object under the mouse pointer.
actionA logic value that is either TRUE or FALSE to indicate if the mouse is entering or exiting the face.
positionThe current X-Y position of the mouse.

Example Over Function

Here is a simple example that will help you understand the OVER function:

print "Displaying..."

view layout [
    box "A Box" forest feel [
        over: func [face act pos] [print [act pos]]
    ]
]

Run this example and pass the mouse over the box. The console will open first, then the window with the box. Make sure that the window with the box is the active window and pass the mouse over the box.

As the mouse enters the box you will see the console display:

true 23x72

And, when the mouse exits the box, you will see a message such as:

false 120x73

The action argument is TRUE when the mouse is entering and it is FALSE when the mouse is exiting.

Note that first PRINT in this example is done to open the console window to display the OVER results. If this is not done, then the first time you pass the mouse over the box, the console will open. On some systems, such as Windows, the window containing the box will lose focus, and the OVER function will immedately return FALSE. You can see this happen if you remove the first PRINT line.

Doing Something on Over

Here are two examples that do something when the mouse passes over the face. The first example changes the text in the box to indicate the presence of the mouse:

view layout [
    box "A Box" forest feel [
        over: func [face act pos] [
            face/text: either act ["Over"]["Away"]
            show face
        ]
    ]
]

It shows the box with the word "Over" when the mouse is over the box, or "Away" when the mouse is off the box.

Notice that the SHOW function was called to display the new text for the face.

Here's an example that changes the color of the face as the mouse passes over:

view layout [
    box "A Box" forest feel [
        over: func [face act pos] [
            face/color: either act [brick][forest]
            show face
        ]
    ]
]

Here's an example that shows a "help line" based on where the mouse is:

view layout [
    box "Top Box" forest feel [
        over: func [face act pos] [
            helper/text: either act ["Over top box."][""]
            show helper
        ]
    ]
    box "Bottom Box" navy feel [
        over: func [face act pos] [
            helper/text: either act ["Over bottom box."][""]
            show helper
        ]
    ]
    helper: text 100
]

Overlapping Faces

When faces overlap the system will inform your code as you pass from one face to another. Here is an example that shows how this occurs:

print "Displaying..."

view layout [
    box "A Box" forest feel [
        over: func [face act pos] [print ["A Box:" act]]
    ]
    pad 30x-40
    box "B Box" brick feel [
        over: func [face act pos] [print ["B Box:" act]]
    ]
]

As the mouse passes onto the A box the console prints:

A Box: true

When the mouse moves from the A box to the B box, then the console prints:

A Box: false
B Box: true

It first indicates that the mouse is no longer over the A box, then tells you that it is over the B box.

Continuous Over Events

It is possible to track the mouse constantly while it is over a face. To do so, you must indicate to the top level window face that you want to know about all over events that occur:

print "Displaying..."

out: layout [
    box "A Box" forest feel [
        over: func [face act pos] [print [act pos]]
    ]
]

view/options out [all-over]

This code will report a continuous stream of mouse positions as the mouse moves over the face. Normally this is not necessary. It can slow down the user interface. Do it only when you need to.

Also, this is not the only way to track mouse movements. Other ways will be shown in the next sections.

The Engage Feel

The engage function is called whenever any event other than a REDRAW or an OVER occurs for a face. It handles mouse click events, keyboard input, timers, and other types of events.

The ENGAGE function has the form:

engage face action event

Where its arguments are:

faceThe face that has the event.
actionA word that indicates the action that has occurred.
eventThe event that provides detailed information about the action.

Mouse Event Example

Here's a short example that will print mouse events that occur on a box:

view layout [
    box "A Box" forest feel [
        engage: func [face action event] [
            print action
        ]
    ]
]

Run this example and click on the box. Right click on the box. Press the mouse button down, then move the mouse as if you were dragging the box.

As you use the mouse, you will see a stream of events such as:

down
up
alt-down
alt-up
down
over
over
over
up
down
over
over
over
away
away
away
away
away
up

These events reflect the actions you peformed with the mouse. Here is a summary of the events:

downthe main mouse button was pressed.
upthe main mouse button was released.
alt-downthe alternate mouse button was pressed (right button).
alt-upthe alternate mouse button was released.
overthe mouse was moved over the face while either button was pressed.
awaythe mouse passed off the face while the button was pressed.

Drag and Drop Example

Using some of the above actions, here is an example that shows how to drag a face around within a window:

view layout [
    size 200x200
    box 40x40 coal "Drag Box" font-size 11 feel [
        engage: func [face action event] [
            if action = 'down [start: event/offset]
            if find [over away] action [
                face/offset: face/offset + event/offset - start
                show face
            ]
        ]
    ]
]

When the mouse is clicked in the box, the offset of the mouse relative to the box is stored in the START variable. As the mouse is moved with the button down, the over and away events are sent and the new offset is added to the position of the face. The START offset is subtracted to locate the face correctly relative to the mouse pointer.

If the start position is stored within the box face, then a style can be created and multiple drag and drop boxes can be made from a single style:

view layout [
    size 240x240
    style dragbox box 40x40 font-size 11 feel [
        engage: func [face action event] [
            if action = 'down [face/data: event/offset]
            if find [over away] action [
                face/offset: face/offset + event/offset - face/data
                show face
            ]
        ]
    ]
    dragbox "Box 1" navy
    dragbox "Box 2" teal
    dragbox "Box 3" maroon
    dragbox "Box 4" gold
]

You can now drag any of the four boxes around. Notice that you can drag boxes under other boxes. The DRAGBOX style is defined with the new ENGAGE function that stores the starting position in the face's data field.

To pop the current face to the top, move it to the end of the window's face pane list when the down event occurs:

view layout [
    size 240x240
    style dragbox box 40x40 font-size 11 feel [
        engage: func [face action event] [
            if action = 'down [
                face/data: event/offset
                remove find face/parent-face/pane face
                append face/parent-face/pane face
            ]
            if find [over away] action [
                face/offset: face/offset + event/offset - face/data
            ]
            show face
        ]
    ]
    dragbox "Box 1" navy
    dragbox "Box 2" teal
    dragbox "Box 3" maroon
    dragbox "Box 4" gold
]

The example uses the parent-face field to access the window's pane list. You could also have assigned the window layout to a variable and used that to refer to the pane list.

Keyboard Events

To receive keyboard events, the face must be made the focus before they will be sent. Here is an example:

view/new layout [
    the-box: box "A Box" forest feel [
        engage: func [face action event] [
            print [action event/key]
        ]
    ]
]
focus the-box
do-events

When you type on the keyboard you will see a stream such as:

key a
key b
key c
key d

The event/key contains the keycode character for the key that was pressed.

If you press the function keys you will see:

key home
key end
key up
key down
key f1
key f5

However, these are not keycodes; they are words. In REBOL you do not need to decode the keyboard sequences. They are decoded for you. This makes it easy to write a key handler. Here is an example:

view/new layout [
    the-box: box "A Box" forest feel [
        engage: func [face action event] [
            if action = 'key [
                either word? event/key [
                    print ["Special key:" event/key]
                ][
                    print ["Normal key:" mold event/key]
                ]
            ]
        ]
    ]
]
focus the-box
do-events

To detect if the control or shift keys are being held down you can write conditional tests such as:

if event/control [...]

if event/shift [...]

if event/control/shift [...]

And finally, if your system has a scroll wheel, its events will also occur as:

scroll-line

and if the control key is held down:

scroll-page

The amount of the scroll can be determined from the Y offset field of the event. Add this to one of the above examples.

if action = 'scroll-line [print event/offset/y]

The size of the Y offset is determined by the scroll-wheel sensitivity that has been set for your operating system.

Timer Events

Each face can have its own timer associated with it. When the timer expires, a TIME event will occur. Here is an example of a repeating time event that occurs every second:

view layout [
    box "A Box" forest rate 1 feel [
        engage: func [face action event] [
            print action
        ]
    ]
]

The rate can specify either the number of events per second or the period between events. If you used:

rate 10

then you would get ten events per second. If you wrote:

rate 0:00:10

then you would get a time event every ten seconds. Or,

rate 0:10

would send you a time event every 10 minutes.

Here is a digital clock that is based on time events:

view layout [
    origin 0
    banner "00:00:00" rate 1 feel [
        engage: func [face act evt] [
            face/text: now/time
            show face
        ]
    ]
]

To create a "single shot" time event that occurs only once, you can disable the timer within the event:

ticks: 0

view layout [
    box "A Box" forest rate 0:00:10 feel [
        engage: func [face action event] [
            if action = 'time [
                if ticks > 0 [
                    face/rate: none
                    show face
                ]
                ticks: ticks + 1
                print now
            ]
        ]
    ]
]

The first time event occurs immediately. (A bug in time events.) The TICKS variable keeps the count of how many times the event has occurred. To shut off the timer, the rate is set to NONE and SHOW is called on the face to update the timer internal values.

The Detect Feel

The DETECT function is similar to ENGAGE, but has the ability to intercept events for all of its subfaces. DETECT can be used to process special events such as keyboard input, timers, or mouse events.

The DETECT function works as an event filter. When an event occurs, DETECT can decide how to handle the event. When it is done, the function can allow the event to continue to lower level faces, or stop it immediately.

DETECT should not be used if ENGAGE can deal with the event directly. DETECT is only needed when it is necessary to filter out events that are directed toward subfaces.

The DETECT function has the form:

detect face event

Where its arguments are:

faceThe face that has the event.
eventThe event that provides detailed information.

The DETECT function must return either:

eventThe same event that it was passed as an argument.
noneWhen the event is not to be processed by subfaces.

Detect Example

Here is an example that will print every event that is received by the box face:

print "Running.."

view layout [
    box 200x200 "A Box" navy feel [
        detect: func [face event] [
            print event/type
            event
        ]
    ]
]

If the box is expanded to include a few faces within it, you can see how DETECT is used to filter events. In the example below, if you click on the "Lock Out" button, all events are locked out until you right click.

lock-out: off

out: layout [
    the-box: box 240x140 teal feel [
        detect: func [face event] [
            if event/type = 'alt-down [
                lock-out: off
                vt/text: "Back to normal."
                show vt
            ]
            if not lock-out [return event]
            return none
        ]
    ]
]

out2: layout [
    space 0x8
    field "Type here"
    across
    button "Lock Out" [
        lock-out: on
        vt/text: trim/lines {Events are locked out.
            Right click to resume.}
        show vt
    ]
    button "Quit" [quit]
    return
    vt: vtext bold 200x30
]

the-box/pane: out2/pane

view out

Here the detect function only returns the event if the LOCK-OUT variable is set false. Otherwise it returns NONE and no events are passed down to the subfaces.

Window Level Detect

To use the DETECT function at the window level, you must set the DETECT feel function after the window VIEW has occurred, otherwise the VIEW function will override your FEEL. Here is an example:

out: layout [banner "Testing"]

view/new out

out/feel: make out/feel [
    detect: func [face event] [...]
]

It is better to use the INSERT-EVENT-FUNC function to set window event handlers. This function allows multiple handlers for each window. It will be discussed separately.

About | Contact | PrivacyREBOL Technologies 2024