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:
- The event occurs (e.g. mouse down event)
- The system makes an event! object to hold information about the event.
- The new event object is queued to the event port (a special kind of REBOL port that holds the stream of events).
- REBOL resumes normal processing until a user function calls the wait function or queries the event port.
- The wait function calls the event-port's awake function. Normally, this is the system/view/wake-event function.
- The awake function reads the event from the event port. The event is removed from the port at the same time.
- 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:
- 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.
- 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.
- The window's detect function is called for whatever window the event occurred within. Again, a none return will stop event processing.
- 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.
- The system processes various parts of the close, resize, move, and time events.
- 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.
- The detect functions for faces along the pane path from the window to the target face all get called (if they are present).
- 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.
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:
face | The face object being redrawn. |
action | A 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. |
position | The 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.
The over function has the form:
over face into position
The arguments to the over function are:
face | The face object under the mouse pointer. |
into | A logic value: true indicates that the mouse is entering (over) the face, and false indicates that the mouse is exiting (away) the face. |
position | The 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:
face | The face that got the event. |
action | A word that indicates the action that has occurred. |
event | The 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.
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:
face | The face that has the event. |
event | The event that provides detailed information. |
The detect function must return either:
event | The same event that it was passed as an argument. |
none | When 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:
open | Open 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. |
close | Ignored. Does nothing. |
pick | Pick next event from the port. Index should always be set to one (because it always returns the first event, regardless). |
get-modes | If 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.