Controller Support Expansion for Ren’Py includes several features to improve controller and keyboard support in Ren’Py. Pick up the tool from itch.io if you haven’t already:
The FocusDisplayable class is a special kind of displayable which will follow the currently focused button/bar/other focusable element. It can be used to draw attention to the focused item by using a cursor to point to it or a frame to highlight it.
Examples
Example 1
The first example is a simple arrow which will point to the left side of the currently focused item.
![A main menu screen. The Start button is in a column on the left, highlighted with orange text and a white arrow on its left side pointing to it.](https://feniksdev.com/wp-content/uploads/2025/02/screenshot0027-1024x576.png)
## Declare the FocusDisplayable
image focus_right_arrow = FocusDisplayable(
Transform("right_arrow", anchor=(1.0, 0.5), pos=(0.0, 0.5)))
## Note: the main menu screen has been slightly simplified for brevity
screen main_menu():
tag menu
add "main_menu_background"
vbox:
xpos 60 yalign 0.5 spacing 6
textbutton _("Start") id 'start' action Start() default_focus 10
textbutton _("Load") id 'load' action ShowMenu("load")
textbutton _("Preferences") id 'preferences' action ShowMenu("pref_display")
textbutton _("About") id 'about' action ShowMenu("about")
textbutton _("Help") id 'help' action ShowMenu("help")
textbutton _("Quit") id 'quit' action Quit(confirm=not main_menu)
use key_footer():
icon_button kind icn.select suffix "small"
## Add the FocusDisplayable from earlier
add 'focus_right_arrow'
Let’s break down the declaration:
image focus_right_arrow = FocusDisplayable(
Transform("right_arrow", anchor=(1.0, 0.5), pos=(0.0, 0.5)))
The first thing passed to the FocusDisplayable is a displayable. Namely, the image of the arrow. In particular, it is wrapped in a Transform so we can tell Ren’Py where it will be positioned relative to the button it’s pointing to. If you’re not familiar with the pos
and anchor
properties, Ren’Py Position Properties – Pos and Anchor will help you with those! Basically, this is saying that the right side of the arrow should be centered along the left side of the button it’s pointing to.
And that’s all! The only thing left to do is add it to the main menu screen where we wanted to use it, which was done with the line add "focus_right_arrow"
at the bottom. It’s important that the FocusDisplayable goes at the very bottom of the screen (or close to it) so it appears on top of any buttons it’s highlighting.
Example 2
Now let’s look at an example that has some animation. FocusDisplayable has several properties that can be used to add some movement.
![A main menu screen with the Start, Load, Preferences, About, Help, and Quit buttons in a column on the left. Each button is selected starting from the top. A white arrow follows the selected button on the left.](https://feniksdev.com/wp-content/uploads/2025/02/animate-cursor.gif)
image focus_right_arrow = FocusDisplayable(
Transform("right_arrow", anchor=(1.0, 0.5), pos=(0.0, 0.5)),
xtime=0.1, xwarper="ease", ytime=0.1, ywarper="ease",
hide_on_mouse=True)
We’ve added several new properties to this declaration. Let’s look at what they do.
xtime
and ytime
are how long the displayable should take to animate from one focus to another, in seconds. Usually this is pretty short, so the displayable isn’t spending several seconds floating across the screen to the next button.
Next, xwarper
and ywarper
are warpers to use to animate that movement with. By default, this is "linear"
, so it just moves at a consistent speed. Here we’ve set it to "ease"
, which starts slow, speeds up, and ends slow again. You can read more about the existing warpers in the Ren’Py documentation here. This can also be a function that takes in a number from 0-1 and returns a float.
Finally, the property hide_on_mouse=True
means that if the mouse is used, the FocusDisplayable will disappear. It reappears when the keyboard or a controller is used to focus something. This can be useful if you’re using the FocusDisplayable to draw additional attention to the focused item, but it would be too distracting for a mouse user who already has the cursor to draw their attention.
You can see from the animation above that this means the arrow spends a bit of time animating to the next focused item rather than simply appearing there.
Example 3
Now let’s look at an example with a Frame rather than a fixed image, which is restricted to a particular area.
![An audio preferences screen. The highlighted buttons and bars have a pink rectangle around them.](https://feniksdev.com/wp-content/uploads/2025/02/audio.gif)
screen pref_audio():
tag menu
default f1 = FocusDisplayable(Frame("pink_rect", 5, 5),
hide_on_mouse=True, padding=(10, 8),
active_area=(350, 200, config.screen_width-350, config.screen_height-200))
use preferences_common("pref_audio"):
side 'c r':
style_prefix 'pref'
controller_viewport:
id 'pref_display_vp'
vscroll_style "center"
## IMPORTANT!
focus_displayables f1
scroll_delay (0.1, 0.1)
mousewheel True draggable renpy.variant("touch")
has vbox
## Contents omitted for brevity
vbar value YScrollValue("pref_display_vp") keyboard_focus False
add f1 ## IMPORTANT!
First, let’s break down the FocusDisplayable declaration. Here it’s declared as a variable rather than an image. Both work well!
default f1 = FocusDisplayable(Frame("pink_rect", 5, 5),
hide_on_mouse=True, padding=(10, 8),
active_area=(350, 200, config.screen_width-350, config.screen_height-200))
First is the displayable – in this case, it’s a pink rectangle using Frame so it’s expandable. See How to make resizeable backgrounds in Ren’Py with Frame for more on Frame!
Next, hide_on_mouse=True
is as we saw before – it means the displayable doesn’t appear when the mouse is used.
padding=(10, 8)
is new. Each button and bar that can be focused has a “hitbox” – a rectangle around its coordinates that defines the area of the button or bar. The padding
property lets us define some extra padding around this hitbox so the rectangle isn’t right up against the button/bar area. You can provide it either as an (xpadding, ypadding)
pair, or as (left_padding, top_padding, right_padding, bottom_padding)
. It works the same way as the padding property of frame
– see Ren’Py Screen Language Basics – Frames for more on that. So, there is 10 extra pixels of space to the left/right of the buttons and bars, and 8 pixels to the top/bottom.
Lastly, there’s active_area
. This declares an (x, y, width, height)
tuple defining the area where the FocusDisplayable should appear. In this case, it’s the area where the viewport is. We don’t want the FocusDisplayable appearing over the menu options to the left, or the tabs at the top, just the viewport with the audio sliders and buttons.
The other thing to note is that this screen uses a controller_viewport
(see Controller Viewport). As a result, when scrolling the focus coordinates (hitbox) of the focused button can change over time. To make sure the FocusDisplayable is updated as the hitbox coordinates are changing, you should provide the FocusDisplayable to the controller_viewport
with the focus_displayables
property. In this case we declared the FocusDisplayable as default f1 = FocusDisplayable(...)
, so the property is set to focus_displayables f1
. If we’d instead declared this outside the screen as image f1 = FocusDisplayable(...)
then we’d pass it in as a string e.g. focus_displayables "f1"
.
Finally, note the add f1
at the bottom of the screen as before, so the FocusDisplayable appears on top of everything.
Properties
Now that you’ve seen several examples of the FocusDisplayable in use, let’s look at all the properties available.
d
A displayable which will follow the currently focused displayable.
e.g. d="my_pointer"
d=Transform("bouncy_arrow", anchor=(0.0, 1.0), pos=(1.0, 0.0))
d=Frame("my_rect", 10, 10)
padding
Padding which will be added around the “hitbox” of the currently focused displayable. (0, 0)
by default (no padding). Positive numbers increase the hitbox size away from the center, and negative numbers shrink it towards the center. You can provide padding either as an (xpadding, ypadding) tuple or as (left_padding, top_padding, right_padding, bottom_padding).
e.g. padding=(10, 8)
padding=(5, 5, 5, 5)
hide_on_mouse
If True, the FocusDisplayable will be hidden while the mouse is used. This is in real-time – it will reappear as soon as the keyboard or controller are used to focus something and disappear as soon as the mouse is moved. False by default.
e.g. hide_on_mouse=True
linger_on_focused
If True, the FocusDisplayable will stay on the last-focused item until a new one is focused, at which point it will animate over to the newly focused item. This is only relevant when hide_on_mouse
is False, because the keyboard and controllers can only go from one focused item to the next/there is no such thing as hovering over something that doesn’t have focus. False by default.
e.g. linger_on_focused True
active_area
An (x, y, width, height) tuple where the FocusDisplayable should be active in. If buttons or bars etc. are focused outside of this area, the FocusDisplayable will not highlight them. A tuple like (100, 200, 400, 500) means that the area starts at (100, 200) and is 400 pixels wide and 500 pixels tall.
e.g. active_area=(100, 200, 400, 500)
This will take floats, integers, absolutes, and position
as seen elsewhere in Ren’Py; see Position in the Ren’Py docs.
xwarper
The warper to use for scrolling animations in the x direction. Can be a string like "linear"
, in which case it must be the name of one of the built-in warpers (see https://www.renpy.org/doc/html/transforms.html#warpers). Otherwise, it can be a callable which will be passed a value between 0.0 and 1.0 and is expected to return a float. Default value is "linear"
.
e.g. xwarper="easein"
ywarper
The warper to use for scrolling animations in the y direction. Takes the same arguments as xwarper
above.
e.g. ywarper="linear"
xtime
How long it takes to animate to the new position in the x direction, in seconds. A float. Set to 0.0
(the default) for no animation.
e.g. xtime=0.2
ytime
How long it takes to animate to the new position in the y direction, in seconds. A float. Set to 0.0
(the default) for no animation.
e.g. ytime=0.1
recheck_period
How long to wait before checking for a focus position change, in seconds. Default is 0.25
. If you’re passing your FocusDisplayable to any controller_viewport
that use it (see the focus_displayables
property of Controller Viewport), you usually won’t need to adjust this.
e.g. recheck_period=0.1
displayables
Optionally, the FocusDisplayable can change based on the mouse
property of the focused item. See the mouse property docs on Ren’Py. If provided, this is a dictionary of event name : Displayable
pairs which correspond to cursors which should be used for the provided event. If the d
property is not provided, this dictionary must have at least the "default"
key in it.
The events are as for the built-in Hardware Mouse Cursor as seen in the Ren’Py documentation: https://www.renpy.org/doc/html/mouse.html#hardware-mouse-cursor
e.g.
image focus_rect = FocusDisplayable(
displayables=dict(
default=Frame("pink", 5, 5),
pressed_default=Frame("red", 5, 5),
investigate=Frame("yellow", 5, 5),
)
)