Skip to content

Event-driven programming

Previous: Chapter 8 — Views and Groups


The purpose of Turbo Vision is to provide you with a working framework for your applications so you can focus on creating the "meat" of your applications. The two major Turbo Vision tools are built-in windowing support and handling of events. Chapter 8 explained views, and this chapter discusses how to build your programs around events.

Bringing Turbo Vision to life

We have already described Turbo Vision applications as being event-driven, and briefly defined events as being occurrences to which your application must respond.

Reading the user's input

In a traditional program, you typically write a loop of code that reads the user's keyboard, mouse, and other input, then make decisions based on that input within the loop. You call functions, or branch to a code loop somewhere else that again reads the user's input:

loop {
    let input = read_key();
    match input {
        'i' => invert_array(),
        'e' => edit_array_params(),
        'g' => graphic_display(),
        'q' => break,
        _ => {}
    }
}

An event-driven program is not structured very differently from this. In fact, it is hard to imagine an interactive program that doesn't work this way. However, an event-driven program looks different to you, the programmer.

In a Turbo Vision application, you no longer have to read the user's input because Turbo Vision does it for you. It packages the input into event structures, and dispatches the events to the appropriate views in the program. That means your code only needs to know how to deal with relevant input, rather than sorting through the input stream looking for things to handle.

For instance, if the user clicks an inactive window, Turbo Vision reads the mouse action, packages it into an event, and sends the event to the inactive window.

If you come from a traditional programming background, you might be thinking at this point, "O.K., so I don't need to read the user's input anymore. What I'll be doing instead is learning how to read a mouse click event and how to tell an inactive window to become active." In fact, there's no need for you to write even that much code.

Views can handle much of a user's input all by themselves. A window knows how to open, close, move, be selected, resize, and more. A menu knows how to open, interact with the user, and close. Buttons know how to be pushed, how to interact with each other, and how to change color. Scroll bars know how to be operated. The inactive window can make itself active without any attention from you.

So what is your job as programmer? You define new views with new actions, which need to know about certain kinds of events that you define. You also teach your views to respond to standard commands, and even to generate their own commands ("messages") to other views. The mechanism is already in place. All you have to do is generate commands and teach views how to respond to them.

But what exactly do events look like to your program, and how does Turbo Vision handle them for you?

The nature of events

Events can best be thought of as little packets of information describing discrete occurrences to which your application needs to respond. Each keystroke, each mouse action, and any of certain conditions generated by other components of the program, constitute a separate event. Events cannot be broken down into smaller pieces. Therefore, the user typing in a word is not a single event, but a series of individual keystroke events.

In Turbo Vision, events are structures (the Event struct defined in src/core/event.rs) that convey information to your views. At the core of every event is a field named what, which describes the kind of event that occurred. The remainder of the event structure holds specific information about that event: the keyboard scan code for a keystroke event, information about the position of the mouse and the state of its buttons for a mouse event, and so on.

Because different kinds of events get routed to their destination views in different ways, we need to look first at the different kinds of events recognized by Turbo Vision.

Kinds of events

Let's look at the possible values of Event.what a little more closely.

There are basically four classes of events: mouse events, keyboard events, message events, and "nothing" events. Each class has a mask defined, so your views can determine quickly which general type of event occurred without worrying about what specific sort it was.

The event types are defined in src/core/event.rs:21:

pub const EV_NOTHING: u16 = 0x0000;
pub const EV_MOUSE_DOWN: u16 = 0x0001;
pub const EV_MOUSE_UP: u16 = 0x0002;
pub const EV_MOUSE_MOVE: u16 = 0x0004;
pub const EV_KEYBOARD: u16 = 0x0010;
pub const EV_COMMAND: u16 = 0x0100;
pub const EV_BROADCAST: u16 = 0x0200;
pub const EV_MOUSE_WHEEL: u16 = 0x0020;

The masks available for separating events are: - EV_NOTHING for "nothing" events - EV_MOUSE (a combination of all mouse event types) for mouse events - EV_KEYBOARD for keyboard events - EV_MESSAGE (a combination of command and broadcast types) for messages

Mouse events

There are basically four kinds of mouse events: an up or down click with either button, a change of position, or a mouse wheel event. Pressing down a mouse button results in an EV_MOUSE_DOWN event. Letting the button back up generates an EV_MOUSE_UP event. Moving the mouse produces an EV_MOUSE_MOVE event. And scrolling the mouse wheel generates an EV_MOUSE_WHEEL event.

All mouse event structures include the position of the mouse, so a view that processes the event knows where the mouse was when it happened. The mouse event information is stored in the mouse field of the Event struct.

Keyboard events

Keyboard events are even simpler. When you press a key, Turbo Vision generates an EV_KEYBOARD event, which keeps track of which key was pressed. The keyboard information is stored in the key_code and key_modifiers fields of the Event struct.

Message events

Message events come in two flavors: commands and broadcasts. The difference is in how they are handled, which is explained later. Basically, commands are flagged in the what field by EV_COMMAND, and broadcasts by EV_BROADCAST.

Commands are stored in the command field of the Event struct and are defined in src/core/command.rs.

"Nothing" events

A "nothing" event is really a dead event. It has ceased to be an event, because it has been completely handled. If the what field in an event structure contains the value EV_NOTHING, that event contains no useful information that needs to be dealt with.

When a Turbo Vision view finishes handling an event, it calls the clear_event() function, which sets the what field back to EV_NOTHING, indicating that the event has been handled. Views should simply ignore EV_NOTHING events.

Events and commands

Ultimately, most events end up being translated into commands. For example, clicking an item in the status line generates a mouse event. When it gets to the status line view, that view responds to the mouse event by generating a command event, with the command field value determined by the command bound to the status line item. Clicking Alt-X Exit generates the CM_QUIT command, which the application interprets as an instruction to shut down and terminate.

Routing of events

Turbo Vision's views operate on the principle "Speak only when spoken to." That is, rather than actively seeking out input, they wait passively for the event manager to tell them that an event has occurred to which they need to respond.

In order to make your Turbo Vision programs act the way you want them to, you not only have to tell your views what to do when certain events occur, you also need to understand how events get to your views. The key to getting events to the right place is correct routing of the events. Some events get broadcast all over the application, while others are directed rather narrowly to particular parts of the program.

Where do events come from?

The main processing loop of an Application, the run() method (see src/app/application.rs:133), basically consists of a loop that looks something like this:

pub fn run(&mut self) -> Option<i32> {
    // Main event loop
    while self.end_state.is_none() {
        // Get next event from terminal
        self.get_event();

        // Handle the event
        self.handle_event();
    }

    self.end_state
}

The event polling happens in the terminal subsystem (see src/terminal/mod.rs:268), which checks for keyboard input, mouse events, and terminal resize events. The handle_event() method then routes the event to the proper views. If the event is not handled (and cleared) by the time it returns, it is simply discarded.

Where do events go?

Events always begin their routing with the current modal view. For normal operations, this usually means your application's desktop. When you execute a modal dialog box, that dialog box view is the modal view. In either case, the modal view is the one that initiates event handling. Where the event goes from there depends on the nature of the event.

Events are routed in one of three ways, depending on the kind of event they are. The three possible routings are positional, focused, and broadcast. It is important to understand how each kind of event gets routed.

Positional events

Positional events are virtually always mouse events (EV_MOUSE_DOWN, EV_MOUSE_UP, EV_MOUSE_MOVE, EV_MOUSE_WHEEL).

The modal view gets the positional event first, and starts looking at its subviews in Z-order (reverse insertion order, so topmost first) until it finds one that contains the position where the event occurred. The modal view then passes the event to that view. Since views can overlap, it is possible that more than one view will contain that point. Going in Z-order guarantees that the topmost view at that position will be the one that receives the event. After all, that's the one the user clicked.

This process continues until a view cannot find a subview to pass the event to, either because it is a terminal view (one with no subviews) or because there is no subview in the position where the event occurred (such as clicking open space in a dialog box). At that point, the event has reached the view where the positional event took place, and that view handles the event.

The implementation of positional event routing can be seen in src/views/group.rs:445.

Focused events

Focused events are generally keystrokes (EV_KEYBOARD) or commands (EV_COMMAND), and they are passed down the focus chain.

The current modal view gets the focused event first, and passes it to its focused subview. If that subview has a focused subview, it passes the event to it. This process continues until a terminal view is reached. This is the focused view. The focused view receives and handles the focused event.

If the focused view does not know how to handle the particular event it receives, it passes the event back up the focus chain to its owner. This process is repeated until the event is handled or the event reaches the modal view again. If the modal view does not know how to handle the event when it comes back, the event is abandoned.

Keyboard events illustrate the principle of focused events

For example, in an application with multiple windows, you might have several files open in editor windows on the desktop. When you press a key, you know which file you want to receive the character. Let's see how Turbo Vision ensures it actually gets there.

Your keystroke produces an EV_KEYBOARD event, which goes to the current modal view, the Application object. The application sends the event to its focused view, the desktop (the desktop is always the application's focused view). The desktop sends the event to its focused view, which is the active window (the one with the highlighted frame). That window also has subviews—a frame, a scrolling interior view, and scrollbars. Of those, only the interior is selectable (and therefore focused, by default), so the keyboard event goes to it.

Broadcast events

Broadcast events are generally either broadcasts (EV_BROADCAST) or user-defined messages.

Broadcast events are not as directed as positional or focused events. By definition, a broadcast does not know its destination, so it is sent to all the subviews of the current modal view.

The current modal view gets the event, and begins passing it to its subviews in Z-order. If any of those subviews is a group, it too passes the event to its subviews, also in Z-order. The process continues until all views owned (directly or indirectly) by the modal view have received the event, or until a view clears the event.

Broadcast events are commonly used for communication between views. For example, when the command set changes (commands are enabled or disabled), a CM_COMMAND_SET_CHANGED broadcast is sent to all views, allowing them to update their display accordingly.

Masking events

Every view has a bitmapped field called event_mask which is used to determine which events the view will handle. The bits in the event_mask correspond to the bits in the Event.what field. If the bit for a given kind of event is set, the view will accept that kind of event for handling. If the bit for a kind of event is cleared, the view will ignore that kind of event.

For example, by default a view's event_mask excludes EV_BROADCAST, but a group's event_mask includes it. Therefore, groups receive broadcast events by default, but simple views don't.

Phase

There are certain times when you want a view other than the focused view to handle focused events (especially keystrokes). For example, when looking at a scrolling text window, you might want to use keystrokes to scroll the text, but since the text window is the focused view, keystroke events go to it, not to the scroll bars that can scroll the view.

Turbo Vision provides a mechanism, however, to allow views other than the focused view to see and handle focused events.

When a group gets a focused event to handle, there are actually three "phases" to the routing:

  1. The event is sent to any subviews (in Z-order) that have their OF_PRE_PROCESS option flag set.
  2. If the event isn't cleared by any of them, the event is sent to the focused view.
  3. If the event still hasn't been cleared, the event is sent (again in Z-order) to any subviews with their OF_POST_PROCESS option flag set.

The three-phase keyboard routing is implemented in src/views/group.rs:266.

So if a scroll bar needs to see keystrokes that are headed for the focused text view, the scroll bar should be initialized with its OF_PRE_PROCESS or OF_POST_PROCESS option flag set.

In general, however, use OF_POST_PROCESS in a case like this; it provides greater flexibility. Later on you might want to add functionality to the interior that checks keystrokes. However, if the keystrokes have been taken by the scroll bar before they get to the focused view (OF_PRE_PROCESS), your interior never gets to act on them.

Although there are times when you need to grab focused events before the focused view can get at them, it's a good idea to leave as many options open as possible. In that way, you (or someone else) can derive something new from this view in the future.

Commands

Most positional and focused events are translated into commands by the views that handle them. That is, a view often responds to a mouse click or a keystroke by generating a command event.

For example, by clicking on the status line in a Turbo Vision application, you generate a positional (mouse) event. The application determines that the click was positioned in the area controlled by the status line, so it passes the event to the status line view.

The status line determines which of its status items controls the area where you clicked, and reads the status item record for that item. That item usually has a command bound to it, so the status line creates a new event with the what field set to EV_COMMAND and the command field set to the command bound to that status item.

Defining commands

Turbo Vision has many predefined commands (see src/core/command.rs), and you will define many more yourself. When you create a new view, you also create a command to invoke the view. Commands can be called anything, but Turbo Vision's convention is that a command identifier should start with CM_. Creating a command is simple—you just create a constant:

pub const CM_CONFUSE_THE_CAT: u16 = 100;

Turbo Vision reserves commands 0 through 99 for its own use. Your applications can use the numbers 100 through 65,535 for custom commands.

The standard Turbo Vision commands include:

pub const CM_QUIT: u16 = 1;
pub const CM_MENU: u16 = 2;
pub const CM_CLOSE: u16 = 3;
pub const CM_ZOOM: u16 = 4;
pub const CM_RESIZE: u16 = 5;
pub const CM_NEXT: u16 = 6;
pub const CM_PREV: u16 = 7;
pub const CM_OK: u16 = 10;
pub const CM_CANCEL: u16 = 11;
pub const CM_YES: u16 = 12;
pub const CM_NO: u16 = 13;

Binding commands

When you create a menu item or a status line item, you bind a command to it. When the user chooses that item, an event is generated, with the what field set to EV_COMMAND, and the command field set to the value of the bound command. The command may be either a Turbo Vision standard command or one you have defined.

Remember that defining the command does not specify the action to be taken when that command appears in an event. You have to tell the appropriate views how to respond to that command.

Enabling and disabling commands

There are times when you want certain commands to be unavailable to the user for a period of time. For example, if you have no windows open, it makes no sense for the user to be able to generate CM_CLOSE, the standard window closing command. Turbo Vision provides a way to disable and enable sets of commands.

To enable or disable commands, use the global command set defined in src/core/command_set.rs. The following code disables a group of five window-related commands:

use crate::core::command_set::{disable_commands, enable_commands};

// Disable window commands
disable_commands(&[CM_NEXT, CM_PREV, CM_ZOOM, CM_RESIZE, CM_CLOSE]);

// Later, enable them again
enable_commands(&[CM_NEXT, CM_PREV, CM_ZOOM, CM_RESIZE, CM_CLOSE]);

When commands are enabled or disabled, Turbo Vision automatically broadcasts a CM_COMMAND_SET_CHANGED event to all views, allowing them to update their appearance (for example, graying out disabled menu items).

Handling events

Once you have defined a command and set up some kind of control to generate it—for example, a menu item or a dialog box button—you need to teach your view how to respond when that command occurs.

Every view implements a handle_event() method that already knows how to respond to much of the user's input. If you want a view to do something specific for your application, you need to override its handle_event() and teach the new handle_event() how to respond to new commands you've defined, and how to respond to mouse and keyboard events the way you want.

A view's handle_event() method determines how it behaves. Two views with identical handle_event() methods will respond to events in the same way. When you create a new view type, you generally want it to behave more or less like its base functionality, with some changes. The easiest way to accomplish this is to handle your specific events, then let the default implementation handle the rest.

The general pattern of a handle_event() implementation would look like this:

fn handle_event(&mut self, event: &mut Event, ctx: &mut dyn ViewContext) {
    // Handle specific events for this view
    match event.what {
        EV_COMMAND => {
            match event.command {
                CM_MY_CUSTOM_COMMAND => {
                    // Handle your custom command
                    self.do_something();
                    clear_event(event);
                }
                _ => {}
            }
        }
        _ => {}
    }

    // Let the default implementation handle everything else
    // (This would be a call to the base trait implementation if applicable)
}

In other words, if you want your new view to handle certain events differently (or not at all!), you would trap those particular events and clear them. Any events you don't clear will be handled by other parts of the system or passed up the ownership chain.

Clearing events

When a view's handle_event() method has handled an event, it should call clear_event() to mark the event as handled. clear_event() sets the Event.what field equal to EV_NOTHING, which is the universal signal that the event has been handled. If the event then gets passed to another view, that view should ignore this "nothing" event.

Remember that you haven't finished handling an event until you call clear_event().

Inter-view communication

A Turbo Vision program is encapsulated into views, and you write code within view implementations. But suppose a view needs to exchange information with another view within your program? In a traditional program, that would probably just mean copying information from one data structure to another. In a view-based program, that may not be so easy, since the views may not have direct references to one another.

If you need to do inter-view communication, the first question to ask is if you have divided the tasks up between the two views properly. It may be that the problem is one of poor program design. Perhaps the two views really need to be combined into one view, or part of one view moved to the other view.

Messages among views

If you've analyzed your situation carefully and are certain that your program design is sound, you can implement communication between views using command events.

The typical pattern is: 1. A child view generates a command event in response to user input 2. The view's handle_event() method transforms the mouse/keyboard event into a command 3. The command event is routed up through the view hierarchy 4. A parent or ancestor view handles the command

For example, when a button is clicked: 1. The button receives an EV_MOUSE_DOWN event 2. The button's handle_event() generates an EV_COMMAND event with the button's command ID 3. The dialog containing the button receives the command event 4. The dialog handles the command (e.g., CM_OK or CM_CANCEL) and calls end_modal()

Broadcasts for notifications

Broadcast events are used when you need to notify multiple views of a change. For example: - When commands are enabled/disabled, CM_COMMAND_SET_CHANGED is broadcast to all views - Views that display command state (like menu items) can update themselves when they receive this broadcast

To send a broadcast, generate an event with what set to EV_BROADCAST:

let mut event = Event::new_broadcast(MY_CUSTOM_BROADCAST);
self.handle_event(&mut event, ctx);

All views in the view hierarchy will receive this broadcast and can respond to it if needed.

Summary

Event-driven programming in Turbo Vision is built around a few key concepts:

  1. Events are discrete packets of information (keystrokes, mouse actions, commands)
  2. Routing determines which views receive which events (positional, focused, or broadcast)
  3. Commands are the primary way views communicate and trigger actions
  4. Phases allow multiple views to see focused events in a controlled order
  5. Event masking lets views filter what types of events they want to receive
  6. Clearing events marks them as handled and prevents duplicate processing

By understanding these mechanisms, you can build sophisticated applications where views coordinate seamlessly without tight coupling between components.


Next: Chapter 10 — Application Objects