UI Input Handling |
This article describes how the UI controls handle input.
This topic contains the following sections:
Focus management decides which control handles the user input first. For example, if the user clicks a text box with the mouse, the text box gets the input focus. When keyboard keys or game pad buttons are pressed, the focused control can handle the keys first. The focused control should also use a visual state to tell the user that it has the focus. Here are some examples:
The user can move the focus from one control to another control using the keyboard arrow keys, the left thumb stick or the D-pad on the game pad.
The focus is managed by the FocusManager class. Each UIScreen has a reference to a focus manager. The focus manager can be exchanged. You might want to inherit from FocusManager to change how the focus moves when keyboard arrow keys are pressed. The method FocusManagerOnMoveFocus can be overridden to customize the focus movement. This might be necessary for radial GUI layouts or other complex layouts. The default implementation works best for grid-like control layouts.
A UIControl has several properties and methods for focus management:
IsFocused indicates whether the control currently has the input focus. IsFocusWithin indicates whether the control or a child control has the focus. For example, if a text box in a window has the input focus, then the text box sets the IsFocused and the window sets the IsFocusWithin flag.
Focus moves the input focus to the control – but only if the control is Focusable . Controls that handle input, like buttons, text boxes, sliders, are focusable per default. Passive controls, like images or text blocks, are not focusable per default. There are a few special cases, for example, a window has a close button (the small X button in the upper right corner). This is a button but Focusable is set to false – the user cannot move the focus to this button.
FocusWhenMouseOver can be set to true if the control should receive the focus automatically when the mouse cursor hovers over the control. Menu items in a context menu and items in a drop-down box set this flag. This flag can also be useful for button in a start screen menu.
Focus scopes define the region in which the focus can move. Windows have set the flag IsFocusScope and this enables focus movement between child controls inside the window. When the focus is moved using keyboard or game pad input, it can only move to another control inside the same focus scope. IsFocusScope could also be set on other controls, e.g. pages in a tab control or the UIScreen itself – but that depends on the application. AutoUnfocus determines if the input focus should be removed from all controls if the user clicks the empty space of a focus scope.
When the mouse is moved over a control, the control should react and indicate that the user can interact with this control. Therefore, most interactive controls have a "MouseOver" visual state. For example, buttons can draw a glow effect when the mouse hovers over the button.
IsMouseOver indicates if the mouse cursor is over the control or over any child control. IsMouseDirectlyOver indicates if the mouse cursor is over the control and not over a child control. – Imagine the mouse is over the close button of a window: IsMouseOver and IsMouseDirectlyOver are true for the close button; IsMouseOver is true for the window but IsMouseDirectlyOver is false.
The UIScreen also keeps track of the control where IsMouseDirectlyOver is set – see property UIScreenControlUnderMouse.
GUI controls are managed in a tree-like structure, the visual tree. The root of the tree is a UIScreen control and each control can have child controls (visual children). Input handling is started by the UIManager when UIManagerUpdate is executed in the game loop. Each control has an OnHandleInput method. To handle the input, the whole visual tree is traversed calling OnHandleInput of each control. This traversal starts at the UIScreen. In OnHandleInput each control let’s the child control with the input focus (if there is any) handle the input first. Then the other children can handle input and finally the control itself.
Each control has the flag IsEnabled. If this is set to false, the control (including its children) will skip input handling. (The read-only flag ActualIsEnabled indicates if a control and all its visual ancestors are enabled.) The screen has an InputEnabled flag that can be set to false to skip input handling for the entire visual tree.
The screen builds an InputContext instance that gets passed to each control during input handling. This context provides a few very useful pieces of information, most notably the property MousePosition. This is the mouse position relative to the control considering any render transformation that may be applied to the control. That means, the control can read the MousePosition input and use it as if it was not scaled/rotated/translated. In contrast, ScreenMousePosition is the absolute mouse position on the screen.
The input context is also useful to hand down information to visual children. For example, each control can set the AllowedPlayer property to determine from which game pad it reads input. The allowed player is part of the input context because each control needs to know the allowed player of ancestors in the visual tree. Usually controls accept input from any game pad. But in some cases (e.g. split screen games), you might want to create a window that accepts only input from player 1 and not from player 2.
Each control has a virtual method HitTest. This method is called during the input handling to check if the mouse is over the control. Per default, the hit test method returns true if the mouse is over the rectangular area (UIControlActualBounds) of the control.
HitTest is also called to determine if the mouse is over a child control and this information is stored in InputContextIsMouseOver and passed to the child control. This is important for controls that clip the space of the child controls, like a scroll viewer. For example, a control could be outside the visible viewport of a scroll viewer. By checking IsMouseOver of the input context the control sees that mouse cannot be over this control because the mouse is either not over the parent control, or the control is clipped by the parent control.
You can override HitTest in custom controls to create round buttons or other non-rectangular clickable areas.
When a control handles keyboard input, it must let the other controls and other game logic know that the key presses have been handled. Here are a few examples:
For this, the IInputService has several flags: IsKeyboardHandled, IsMouseOrTouchHandled, IsGamePadHandled and IsAccelerometerHandled. These flags can be read to see if the input from this device has already been handled by someone else in this frame. Controls or other game logic objects that handle input from a device should set the flags accordingly, to let other objects know that device input has been handled.
Some controls can also simply ignore these flags and handle the input nevertheless; for example, a context menu will always close when ESCAPE is pressed – no matter if any other object handles ESCAPE too.
To customize the input handling, you can derive a class from an existing control class and override OnHandleInput. Don’t forget to call base.OnHandleInput(context); because this lets the visual children do the input handling. The basic structure of OnHandleInput is
protected override void OnHandleInput(InputContext context) { // Do some stuff before the child control handle input. ... // Call base class, which also calls child controls. base.OnHandleInput(context); // Do some stuff after the child controls have handled input. ... }
Any code before base.OnHandleInput(context) acts before the child controls have a chance to handle input. This is used, for example, by the Window class: If the user is currently dragging the window with the mouse, then the window sets IInputService.IsMouseOrTouchHandled to true before base.OnHandleInput(context). This lets the child controls know that they should ignore mouse input.
There are also two UIControl events that you can attach to instead of overriding OnHandleInput: