REBOL
Docs Blog Get-it

REBOL/View Graphics - Event Handling

All about graphics system events and how they are processed.

Concept

User interface events are handled by a set of functions defined in the context of the feel object that is part of every face object. These functions get called in response to mouse input, keyboard input, refresh (redraw), window changes, and other types of GUI events.

Event Flow

In general, events flow down through the faces of the screen starting from the screen-face object toward the target face where the event actually occurred.

The events normally flow downward in an uninterrupted fashion (as long as the detect function is not used, see below). When an event reaches the target face (where the event occurred), it looks for the proper event handling functions (over or engage). If the face contains no event handlers, the event climbs back up the hierarchy looking for a parent face that does include event handlers.


Although it is not necessary for most user applications to understand or deal with the precise mechanism of event flow, we will outline it here for users who want to know more about it. Here is the event sequence:

  1. The event occurs (e.g. mouse down event)
  2. The system makes an event! object to hold information about the event.
  3. The new event object is queued to the event port (a special kind of REBOL port that holds the stream of events).
  4. REBOL resumes normal processing until a user function calls the wait function or queries the event port.
  5. The wait function calls the event-port's awake function. Normally, this is the system/view/wake-event function.
  6. The awake function reads the event from the event port. The event is removed from the port at the same time.
  7. The awake function deals with the event. In most cases, the function will call do with the event. This causes the event to be processed by the system, eventually being handled by specific face detect, over, and engage functions.

The user application actually has a lot of control over this event flow mechanism. In normal use, default functions are setup in the system/view object to alleviate the need for users to provide event processing control. However, applications can modify and add to the system event functions as necessary.

Most of the processing associated with events happens within the do function of the event. Here is a summary of the process:

  1. The system checks if windows have been moved or resized. This is done first to make the necessary changes to the face object fields prior to calling any feel functions.
  2. The screen-face detect function is called (if one has been provided). This is the highest level of event processing (the global level) essentially able to handle any event for any window or face. (See the insert-event-func section below). If this top level detect function returns none, then the event will be ignored. Otherwise, it returns the event and processing continues.
  3. The window's detect function is called for whatever window the event occurred within. Again, a none return will stop event processing.
  4. The window flags are checked to determine that the window is still open. (The above detect functions could have closed it.) If the window is closed, event processing stops here.
  5. The system processes various parts of the close, resize, move, and time events.
  6. The system checks if a focal-face event has occurred (keyboard or scroll-wheel), if so it checks that focal-face and caret are set in order to call the correct face engage handler. See the text section regarding focus and caret.
  7. The detect functions for faces along the pane path from the window to the target face all get called (if they are present).
  8. And, finally, the engage or over handler is called for the target face. In the case of engage, if that function does not exist for the face, then its parent face engage will be called. If the parent has no engage, then it's parent gets called, and the pattern continues upward to the top-level face.

Feel object

The feel facet controls a face's behavior in response to system events like redraw, mouse input, keyboard input, and timers. Each face can have its own feel object, but more often a single feel object can be shared between all faces that require the same behavior. For instance, a set of button faces may share a single button-feel object. Such reuse helps reduce memory overhead.

The fields of the feel object are all functions that are called at various times by the View system. Collectively they are sometimes referred to as the feel of the face (as in Look and feel). They are listed below.

The specification of the feel object is defined by the system and, because it is used by lower level implementation code, it should not be altered. The master face object (system/standard/face, or the global face) provides the proper definition; you can use it to define your own feel object.

Sharing a Feel Object

For efficiency reasons, the feel functions are located in an object rather than defined as functions directly in the face object. This allows faces to share feel objects without creating a lot of extra overhead.

However, this sharing requires a bit of care. Many face objects share their feel between face instances. For example, VID button faces share a single feel object that specifies the event handling required for buttons.

You need to keep this sharing in mind if you decide to modify the functions of a feel object. For example, if you modify the feel object of a button, you may accidentally be modifying the feel object of other buttons.

To avoid the effect you need to make a copy of the feel object and only modify it for the face that you need. Examples are shown below.

On the other hand, if you want to change the behavior for all the faces of a certain style, you can do so in one place if they share a common feel object. Smalltalk programmers are used to being able to change the behavior of the system on a global level this way.

Feel Functions

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

redraw [face action position]

over [face action position]

engage [face action event]

detect [face event]

Each of these functions is described in detail in the sections that follow.

Redraw Function

The redraw function is called immediately before the face is drawn. This function allows code to dynamically modify the facets of the face prior to being displayed. However, such code should be minimized because this function will often get called many times a second as a user moves the mouse over the face.

If you do not need this function, it is strongly recommended that you set this field to none to speed up the display process.

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 type of action that occurred on the face: show, draw, and hide. The show action occurs when the show function is called on the face (or any of its parents). The draw occurs immediately before the face is rendered. The hide action is called when the face is hidden.
positionThe offset of the face if the face is iterated. Most of the time, you can ignore this argument. Iterated faces are covered below.

Example of Using Redraw

Here is an example that shows the behavior of the redraw function:

view make face [
    offset: 100x100
    pane: reduce [
        make face [
            text: "Left Click mouse here.^/^/Right click to hide."
            color: yellow
            edge: none
            feel: make feel [
                redraw: func [face action pos] [
                    print ["Redraw:" action pos]
                ]
                engage: func [face action event] [
                    switch action [
                        down [show face]
                        alt-down [hide face]
                    ]
                ]
            ]
        ]
    ]
]

Over Function

The over function is called whenever the mouse pointer passes over or off of a face. This function can be used to capture mouse hover events and provide user feedback by changing the appearance of the face. For example, hot text may change the color of the text as the mouse passes over it.

The over function may get called at a very high rate, because a user interface often consist of hundreds of faces and the user may move the mouse over those faces. (That's the reason why this is a single function and 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.

Use Engage for Drag and Drop

Over is only called if no mouse button is currently pressed. It is not meant to be used for drag operations; you should use the engage function with the over and away actions for that.

For example, if the user clicks and drags off the face, the away action is sent to the engage function, not the over function; If the user has dragged off the face, then drags back over the face, the over action is sent to engage, not over.

See the Drag and Drop example section near the end of this document.

The over function has the form:

over face into position

The arguments to the over function are:

faceThe face object under the mouse pointer.
intoA logic value: true indicates that the mouse is entering (over) the face, and false indicates that the mouse is exiting (away) the face.
positionThe current X-Y position of the mouse.

Example Using Over

Here is an example that shows the behavior of the over function:

view make face [
    offset: 100x100
    pane: reduce [
        make face [
            text: "Move the mouse over this"
            color: yellow
            edge: none
            feel: make feel [
                over: func [face into pos] [
                    text: reform [
                        pick ["over" "away"] into
                        pos
                    ]
                    color: pick reduce [green red] into
                    show face
                ]
            ]
        ]
    ]
]

Example That Shows Over Behavior

The example below shows more clearly what happens with the over function when used with more than one face:

print "Move mouse over faces in the window..."

view make face [
    offset: 100x100
    size: 130x130
    color: navy
    edge: none
    pane: reduce [
        make face [
            offset: 10x10
            size: 75x75
            text: "Face1"
            color: red
            feel: make feel [
                over: func [face into pos] [print [text into]]
            ]
        ]
        make face [
            offset: 45x45
            size: 75x75
            text: "Face2"
            color: yellow
            feel: make feel [
                over: func [face into pos] [print [text into]]
            ]
        ]
    ]
]

Notice that the each face gets a false when it leaves each face even though it enters another face.

Continuous Over Events

If you run the code examples above, you will notice that it only reports the mouse event once as it moves around within the face. This is an optimization to prevent a flood of events.

If you need to receive all the mouse movement events, you can enable that as a special option for the window. This allows you to track the mouse constantly while it is over a face.

To enable mouse movement events, add the all-over option to the top level face (the window face) as shown here:

view make face [
    offset: 100x100
    options: [all-over] ;<--- added
    pane: reduce [
        make face [
            text: "Move the mouse over this"
            color: yellow
            edge: none
            feel: make feel [
                over: func [face action pos] [
                    text: reform [
                        pick ["over" "away"] action
                        pos
                    ]
                    color: pick reduce [green red] action
                    show face
                ]
            ]
        ]
    ]
]

The code now reports a continuous stream of mouse positions as the mouse moves over the face. Normally this is not necessary and it can slow down the user interface. Do it only when you need to.

Engage Function

The engage function is called whenever any event other than a redraw or an over occurs for a face. It is called for the majority of events that occur within a face. For example, it is called when the mouse pointer is over the face and either mouse button is pressed, or if a mouse button has been pressed and the mouse is moved over the face, or if the face is the focus of keyboard events and such an event happens, and finally when time events occur, such as for animation or repetitive selection events.

The engage function has the form:

engage face action event

Where its arguments are:

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

Example Using Engage

Here is an example that shows the behavior of the engage function:

view make face [
    offset: 100x100
    pane: reduce [
        make face [
            text: "Click, right click, drag mouse, and press keys."
            color: yellow
            edge: none
            feel: make feel [
                engage: func [face action event] [
                    text: reform [action event/key]
                    system/view/focal-face: face
                    system/view/caret: tail text
                    show face
                ]
            ]
        ]
    ]
]

The focal-face and caret must be set to allow the face to receive keyboard events. If they are not set, keyboard events do not know the target face.

Also, be sure to try holding down the mouse button and moving with it down. You will see that engage gets sent over and away events similar to the over function.

To see an example of how to implement a simple drag-and-drop operation using the engage function, see the examples section near the end of this document.

Summary of Engage Events

The main events received by engage are:

Event Description
down the main mouse button was pressed.
up the main mouse button was released.
alt-down the alternate mouse button was pressed (right button on right handed mice).
alt-up the alternate mouse button was released.
over the mouse was moved over the face while either button was pressed.
away the mouse has moved off the face while the button was pressed.
key a key has been pressed.

Detect Function

The detect function is 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 face objects. For example, it is used by VID to process keyboard shortcuts.

Important Performance Warning:

The detect function gets called for all events, and there can be multiple detect functions that get called for a single event (as the event moves from the screen to the window and to each of the faces of the face hierarchy).

It is best to avoid using detect if possible. It is more of a last resort when you must filter an event and have no other way to catch the event. Detect should never be used if engage can deal with the event properly. Engage is much more efficient.

If you find that your user interface is running slowly, and you are using the detect function, review your code and determine if you can eliminate the use of detect or try to minimize its computation.

The detect function works as an event filter. When an event occurs, detect can decide how to handle the event. When it is ready to exit, the return value allows the event to continue to lower level faces or stop it immediately.

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.

Example Using Detect

Here is a simple example of detect:

view make face [
    offset: 100x100
    pane: reduce [
        make face [
            text: "Test Detect"
            color: yellow
            edge: none
            feel: make feel [
                detect: func [face event] [
                    text: reform [event/type event/offset]
                    show face
                    event ; <-- do not forget this
                ]
            ]
        ]
    ]
]

Of course, this example is not that meaningful because the face has no subfaces. Here is a better example:

view make face [
    offset: 100x100
    size: 100x200
    pane: reduce [
        make face [
            text: "Face 1"
            color: yellow
        ]
        make face [
            offset: 0x100
            text: "Face 2"
            color: green
        ]
    ]
    feel: make feel [
        detect: func [face event] [
            print [event/type event/offset]
            event
        ]
    ]
]

Be sure to read the performance warning above.

Trapping Global Events

The detect function is often used at the window face or screen face level, but you should really use the insert-event-func function to trap such global events.

Bug Note: On current versions of REBOL, the event/face within the detect function is not set to the proper value. It should indicate the target face of the event, not the face in which the detect event is used.

Timers

Each face can have its own timer associated with it. When the timer expires, a time event will occur (which is normally handled by the face's engage function).

The rate field of the face object is used to set the desired time interval. The rate can specify either the number of events per second or the period between events.

view make face [
    offset: 100x100
    pane: reduce [
        make face [
            text: "Timer Example"
            color: yellow
            edge: none
            rate: 2
            feel: make feel [
                engage: func [face action event] [
                    if action = 'time [
                        text: now/time
                        color: get pick [green red] color = red
                        show face
                    ]
                ]
            ]
        ]
    ]
]

If you modify the above rate to:

rate: 0:00:10

Then, you would get the time event every ten seconds. If you set the rate to none, it will disable the timer (but see note below).

A show is Required - If you change the time rate for a face, you must call the show function with that face to enable the new timer. This is also true if you disable the timer by setting rate to none.

Event Datatype

In REBOL, events are a special datatype, event!.

There are four general categories of events: mouse, keyboard, window and time events. Mouse events occur during motion and button selection. Keyboard events occur when a key is pressed. Window events occur when a window becomes active, inactive, resized, or closed. And, time events occur during clock ticks of the computer's internal clock.

Some of the feel functions take an event value as an argument. Each event contains information about itself that is accessible by using the refinements supported by the event! datatype. The following list describes these refinements:

Refinement Description
/face Returns the top-level face object of the window that was active when the event occurred.
/type Returns event type such as down or up for the mouse button. The event types are listed in the next section.
/offset Returns the mouse cursor offset relative to the REBOL/View window. The offset returned has different meaning depending on the event type. See table below.
/key Returns the character for the event, if there is one. With key events, this returns the character pressed that triggered the event. The only exceptions are the arrow keys up, down, left, right, and the end and home keys, which return word! values.
/time Returns an integer representing a unique time index for the event.
/control Returns a logic value, true or false, depending on whether the control key is pressed when the event is sent. This works with the mouse up and down events as well as move, key, resize and time.
/shift Returns a logic value, true or false, depending on whether the shift key is pressed when the event is sent. This works with the mouse up and down events as well as move, key, resize and time.
/double-click Returns a logic true if a down event happened twice in succession, indicating that a double-click down event occurred. This is useful for some types of user interfaces.

These refinements allow events to be examined and actions taken based on the event.

For example, to see if the event is a left mouse click, check the event type using the /type:

if event/type = 'down [ ... ]

This next example checks whether the shift button is held down when the mouse-down event occurred:

if all [event/type = 'down  event/shift] [ ... ]

The following illustrates how different types of events can be handled:

switch event/type [
    down  [ ... ]
    up    [ ... ]
    key   [ ... ]
    close [ ... ]
]

Typically, an event will be checked in its face's feel/engage, or feel/detect function. These are the two functions of the feel object that take an event value as one of its arguments.

Note that you cannot currently set the fields of an event datatype. This may be added in a future version of REBOL.

Event Types

Event Type Description
down, alt-down mouse down event for the left or right mouse button (pressing down on the mouse button)
up, alt-up mouse up event for the left or right mouse button (releasing the mouse button)
move Mouse move event (position of the mouse cursor over a REBOL/View window)
offset Window move event (offset of window)
key Keyboard event (keyboard input)
time Timer event (unique time index - latest event has the highest number)
resize Resize window event (whenever the window is resized)
close Close window event (exiting the REBOL/View window session)
active Window becoming active; occurs when the window becomes active again after being inactive, while another application was active
inactive Window becoming inactive; occurs when the window becomes inactive, because another application became active
minimize The window has been minimized by the user.
maximize The window has been maximized by the user (opened to full screen size).
scroll-line Mouse wheel scrolled
scroll-page Mouse wheel scrolled with the Control key held down

Note: In MS Windows there is an optional desktop setting to "Show window contents while dragging". If that option is used, when a window is moved, you will receive a number of offset events after the mouse is released; if not, you will receive only one offset event.

Event Offset Meaning

The meaning of the event offset will vary with different kinds of events. Here are the possibilities:

Event Type Description
down, alt-down The offset when the mouse button is pressed.
up, alt-up The offset when the mouse button is released.
move The offset at the moment the mouse is moved. The frequency at which mouse move events are sent to the event port will vary per system. When the mouse cursor is outside the REBOL/View window, no move events are sent to the event port.
key The mouse cursor offset at the moment of keyboard input. When the mouse cursor is outside the REBOL/View window and a keyboard event occurs, the offset value will be positive or negative of the current REBOL/View window size.
time The offset of the mouse at the moment the event is sent.
offset The offset of window. Offset events are sent after the window is moved.
resize The offset of the mouse as the window is being resized. Resize events are sent as the window is being resized.
scroll-line 0xN, where N is the number of lines to scroll vertically. Negative values for N mean the wheel was scrolled up instead of down.
scroll-page 0xN, where N is the number of pages to scroll vertically. Negative values for N mean the wheel was scrolled up instead of down.

Note: The scroll-line and scroll-page events don't currently reach non-window detect functions. You can only receive them from a window face. One good way is to use the insert-event-func function.

The Event Port

As events occur, they are queued to a special type of port called the event port. As events are processed, they are read from the event port and acted upon.

Of course, most applications require no knowledge of the event port and its specific action. However, if your application needs to handle events directly at the event queuing level (very low level) it can do so via the event port.

The event port handles these actions:

openOpen the event port. The scheme is event, and normally a target of "events" is provided (for error output identification). The event port should only be opened once.
closeIgnored. Does nothing.
pickPick next event from the port. Index should always be set to one (because it always returns the first event, regardless).
get-modesIf the activity mode is queried, returns true if user activity has occurred since the last check. This feature is used by IOS to determine if the user is actively using IOS (for updating the user activity status indicator).

At this time you cannot insert your own events into the event port.

To see the code for the default functions event port functions, type these lines into the console:

source open-events

source do-events

probe get in system/view 'wake-event

Global Event Handler

To make it easier for applications to cooperate on the handling of global events, the insert-event-func function has been provided to set global event handlers.

The argument you provide can be either a function! or a block!. If you provide a block, it is used as the body of a function that takes face and event args. insert-event-func is a mezzanine, so you can see exactly how it works.

insert-event-func: func [
    {Add a function to monitor global events. Return the func.}
    funct [block! function!] "A function or a function body block"
][
    if block? :funct [funct: func [face event] funct]
    insert system/view/screen-face/feel/event-funcs :funct
    :funct
]

You can install as many handler functions as you want this way, and they will remain active while the REBOL session is running. If you need to remove one--for example, to insert an updated version of it while the program is running--you can use remove-event-func.

remove-event-func: func [
    "Remove an event function previously added."
    funct [function!]
][
    remove any [find system/view/screen-face/feel/event-funcs :funct ""]
]

In order to use remove-event-func, you need to keep a reference to your event function so you can provide it to remove-event-func.

A common idiom is to use the switch function to handle different event types in a handler function. You can handle as many event types as you want this way and easily ignore events you don't need.

insert-event-func [
    switch event/type [
        key         [handle-hot-keys event/key]
        time        [show-current-time]
        close       [cleanup]
        offset      [save-window-pos]
        resize      [save-window-pos]
        scroll-line [scroll-lines event/offset/y]
        scroll-page [scroll-pages event/offset/y]
    ]
    event
]

You can compare against event/face to handle events for different windows that may require different logic. (Seen "Known Problems" section below.)

Your Handler Must Return the Event - If you want the system to continue to process an event, then your event handling function must return the event as its result. If your code has handled the event, and no other processing is required, your function should return none.

In the code above, the last value in your function should be the event value, unless you return from the function somewhere in the body; in that case you should still return the event passed to the function as an argument.

About | Contact | PrivacyREBOL Technologies 2024