Skip to content

Menus and UiElement

Bartholomew Ho edited this page Nov 9, 2026 · 1 revision

Menus and UiElement

Menus gives you a frame of your own on the game's UI thread. It tells you what has the focus and which window is active, and presses the controller's keys in the menus for you. UiElement is one element of a menu page: a page, a panel, a button or a list row, with where it's drawn on screen.

The game's menus never get the mouse. A mouse mod finds the element under the cursor (UiElement.TryGetBounds), gives it the focus (UiElement.Focus) and presses the controller's keys (Menus.Tap). The page then behaves exactly as it would with a controller.

Thread: the UI thread, inside Menus.Frame.

Example

A left click on a menu item selects it, as if you'd moved there with the d-pad and pressed A.

using System.Numerics;
using FH2;
using FH2ModPlatform;

public sealed class ClickMenus : Mod
{
    private bool _wasDown;

    public override ModInfo Info => new("ClickMenus", "1.0.0");

    protected override void OnInit()
    {
        Menus.Frame += dt =>
        {
            bool down = GameWindow.IsMouseDown(MouseButton.Left);
            if (down && !_wasDown && Menus.ActiveWindow is { } page)
            {
                var hit = Under(page, page, Menus.ToUi(GameWindow.Cursor));
                if (hit != null && hit.Focus()) Menus.Tap(MenuKey.A);
            }
            _wasDown = down;
        };
    }

    // The last focusable element under the point: later children are drawn over earlier ones.
    private static UiElement? Under(UiElement root, UiElement e, Vector2 at)
    {
        UiElement? hit = null;
        if (e.IsFocusable && e.Opacity > 0 && e.TryGetBounds(root, out var r) && r.Contains(at)) hit = e;
        foreach (var c in e.Children)
            if (Under(root, c, at) is { } h) hit = h;
        return hit;
    }
}
Imports System.Numerics
Imports FH2
Imports FH2ModPlatform

Public Class ClickMenus
    Inherits [Mod]

    Private _wasDown As Boolean

    Public Overrides ReadOnly Property Info As ModInfo
        Get
            Return New ModInfo("ClickMenus", "1.0.0")
        End Get
    End Property

    Protected Overrides Sub OnInit()
        AddHandler Menus.Frame,
            Sub(dt)
                Dim down = GameWindow.IsMouseDown(MouseButton.Left)
                Dim page = Menus.ActiveWindow
                If down AndAlso Not _wasDown AndAlso page IsNot Nothing Then
                    Dim hit = Under(page, page, Menus.ToUi(GameWindow.Cursor))
                    If hit IsNot Nothing AndAlso hit.Focus() Then Menus.Tap(MenuKey.A)
                End If
                _wasDown = down
            End Sub
    End Sub

    ' The last focusable element under the point: later children are drawn over earlier ones.
    Private Shared Function Under(root As UiElement, e As UiElement, at As Vector2) As UiElement
        Dim hit As UiElement = Nothing
        Dim r As UiRect
        If e.IsFocusable AndAlso e.Opacity > 0 AndAlso e.TryGetBounds(root, r) AndAlso r.Contains(at) Then hit = e
        For Each c In e.Children
            Dim h = Under(root, c, at)
            If h IsNot Nothing Then hit = h
        Next
        Return hit
    End Function
End Class

Menus

Properties

Member What it does
IsReady (property, bool) True once the menus' code has been checked and the frame hook is in. Reading it starts Menus. Any thread.
IsUiThread (property, bool) True on the UI thread. Any thread.
IsActive (property, bool) True if the UI has run a frame within the last second (the game is up and not minimised). Any thread.
FrameTime (property, float) The last UI frame's length, in seconds.
Size (property, Vector2) The UI's size in its own pixels (the game's render size: 1920 x 1080 at 1080p).
ActiveWindow (property, UiElement?) The active window (the page on screen, or the HUD's while driving), or null.
Focused (property, UiElement?) The element with the keyboard focus (the page itself, a button, a list), or null.
FocusedTypeName (property, string) The focused element's class name, or "" for none: "UIWindow" while driving, "MapScene" on the map...
QueuedKeys (property, int) How many queued key events are still to go.

Methods

Member What it does
ToUi(Vector2 window) (method, returns Vector2) Turns a point in the game window's pixels (GameWindow.Cursor) into the UI's pixels.
SendKey(MenuKey key, bool up) (method) Sends a key to the menus now, as the controller sends it: down (up false) or up (up true).
QueueKey(MenuKey key, bool up, float wait = 0) (method) Sends a key in a later frame, one key event per frame, after waiting wait seconds. Some menus look at what's held each frame and miss a press and release sent in the same frame.
Tap(MenuKey key, float wait = 0) (method) A press and release of key (two QueueKey calls), after wait seconds. The usual way to press a menu key.
PressPadButton(MenuKey key, bool hold = false) (method, returns bool) Presses the controller button that sends key, the way the controller does, so pages that check the button itself see it too (the pause menu's Y). Held at least 80 ms, and with hold until ReleasePadButton. False if the input hooks are off or a press is already under way; use Tap then.
ReleasePadButton() (method) Lets go of the button PressPadButton held.

Events

Member What it does
Frame (event, Action<float>) Once a UI frame, on the UI thread, before the game's UI updates. Gets the frame's length in seconds (0.001..0.1). Runs on the title screen, in every menu, and while driving. The place for everything on this page. Keep it short.

MenuKey (enum)

The keys the controller sends the menus: Pause, View, A, B, X, Y, LB, RB, LT, RT, Left, Up, Right, Down, LeftStick, RightStick.

UiElement

An element of the game's menus. Get one from Menus.ActiveWindow or Menus.Focused, then walk its Children and Parent.

Member What it does
Handle (property, nint) The element's address, for code of your own.
TypeName (property, string) Its class name as the game has it ("ListBoxItem", "MapScene", "ItemsSelector").
Is(UiType type) (method, returns bool) True if it's a type, or derives from one.
Parent (property, UiElement?) The element it's drawn in, or null.
ChildCount (property, int) How many elements are drawn in it.
Child(int index) (method, returns UiElement?) The indexth element drawn in it, or null. Later ones are drawn over earlier ones.
Children (property, IEnumerable<UiElement>) Every element drawn in it, in order.
Opacity (property, float) Its opacity, 0..1 (0 is hidden).
IsLaidOut (property, bool) True once it has a place and a size on the page.
RenderSize (property, Vector2) Its size, as laid out.
IsFocusable (property, bool) True if it can take the focus.
IsFocused (property, bool) True if it has the focus (a tab row marks its current tab this way).
DataContext (property, nint) What it shows. Only compare it, never follow it. 0 for none.
SelectedItem (property, nint) A UiType.Selector's selected item (compare it with an item's DataContext), or 0.
HelpButtonType (property, int) A UiType.HelpButton's button: 0 A, 1 B, 2 X, 3 Y, 4 Menu, 5 View, 6 LB, 7 RB, 8 LT, 9 RT, 10 L3, 11 R3, 12/13 d-pad both ways, 14-17 d-pad up/down/left/right... Or -1.
Focus() (method, returns bool) Gives it the focus, as the d-pad would. True if it has the focus now.
TryGetLayoutBounds(UiElement root, out UiRect bounds) (method, returns bool) Where it's laid out, in root's pixels (the screen's, when root is the active window). False if it isn't laid out.
TryGetBounds(UiElement root, out UiRect bounds) (method, returns bool) Where it's actually drawn, in root's pixels: as laid out, then as the menus' tilt shows it. Use this one to find what's under the cursor.
==, !=, Equals(UiElement? other) Two UiElements are equal when they're the same element.
ToString() (method) Its type name and address, for logs.

UiType (enum)

Kinds of element, for UiElement.Is.

Value Meaning
Visual, UIElement The base kinds, as in WPF: anything drawn is a Visual.
ButtonBase, RepeatButton, ListBoxItem, ComboBoxItem, TabItem, TreeViewItem, MenuItem, Slider The usual WPF kinds.
Selector A list or row of items that has a selection (ListBox, the game's ItemsSelector...).
Spinner The game's "< value >" spinner.
HelpButton One of the help bar's buttons ("(A) Select").

UiRect (record struct)

A rectangle in the UI's pixels.

Member What it does
UiRect(float Left, float Top, float Right, float Bottom) (constructor) The rectangle's edges.
Left, Top, Right, Bottom (properties, float) The edges.
Width, Height (properties, float) Its size.
Center (property, Vector2) Its middle.
Contains(Vector2 p) (method, returns bool) True if p is inside it.
DistanceTo(Vector2 p) (method, returns float) How far p is outside it (0 inside): the larger of the distance across and the distance down.

Notes

  • UI thread only. ActiveWindow, Focused, FocusedTypeName and SendKey throw InvalidOperationException if used off the UI thread. Do your menu work inside Menus.Frame. You can subscribe to Frame from OnInit, and IsReady, IsActive and IsUiThread work from any thread.
  • A UiElement is only valid during the UI frame you got it in. Don't keep one for later: find it again next frame.
  • Compare elements with == in C# and = in VB. Each call gives you a new UiElement object, so VB's Is (same object) is false even for the same element.
  • Is is a keyword in VB. Write el.[Is](UiType.Selector).
  • Menus starts the first time a mod reads IsReady or subscribes to Frame. GameInput and the photo and garage cameras start with it.
  • Most menus are drawn tilted back a little. TryGetBounds takes that into account; TryGetLayoutBounds doesn't.
  • The mouse position from GameWindow is in the window's pixels, but bounds are in the UI's pixels. Turn it with Menus.ToUi(GameWindow.Cursor) before you compare the two.
  • An exception in a Frame handler is written to platform.log and the handler stays on.
  • At most 64 key events wait in the queue; more are dropped.
  • If your mod is a hosted mod and it's reloaded, its Frame handlers and queued keys are removed.
  • Unsupported game build: if the menus' code isn't as expected, Menus switches itself off and platform.log says "Menus off: ...". IsReady stays false and Frame never runs.

See also

Clone this wiki locally