-
Notifications
You must be signed in to change notification settings - Fork 6
en Basic UI
UIHandle is the base class for InnoVault's general-purpose UI system. Inheriting from it wires your code into the UI pipeline automatically — no manual InterfaceLayers registration required.
Compared to tModLoader's UIState/UIElement system, UIHandle differs in the following ways:
- Auto-registration: Loader picks it up via reflection and handles layer insertion
- Multi-layer rendering: 10 render layers covering in-game HUD through main menu
-
Dual-thread updates: Draw-thread
Update()and logic-threadLogicUpdate()are separate -
Frame-rate independent animation:
AnimatedFloat+CurrentFrameDeltakeep animation speed consistent at any framerate -
Built-in open/close lifecycle:
Open()/Close()+OpenProgressdrive fade-in/out directly -
Built-in drag support: Enable
CanDragto get drag behavior with a global mutex -
Three-button state tracking: Left/right/middle buttons each support
Pressed/Held/Released, separated per draw/logic thread -
Persistent storage:
SaveUIData/LoadUIDatahooks into the SaveContent system
| Property/Field | Type | Description |
|---|---|---|
Active |
bool |
Whether the UI is active. Default: returns true when IsOpen is true or OpenProgress is fading out (IsClosing). Override freely if not using the lifecycle system |
Texture |
Texture2D |
Main UI texture, defaults to a placeholder, overridable |
LayersMode |
LayersModeEnum |
Render layer — determines which interface layer this UI inserts into (read once at load, runtime changes have no effect) |
RenderPriority |
float |
Sort order within the same layer, higher = drawn on top (sorted once at load) |
DrawPosition |
Vector2 |
Draw origin, typically the top-left corner of the UI rectangle |
Size |
Vector2 |
UI rectangle size; used by AutoUpdateHitBox to maintain the collision box |
UIHitBox |
Rectangle |
UI collision rectangle |
MousePosition |
Vector2 |
Mouse screen coordinate (screen-factor corrected; inside Draw() you can use Main.MouseScreen directly) |
MouseHitBox |
Rectangle |
1×1 mouse collision rectangle |
MousePoint |
Point |
Integer mouse coordinate, equivalent to MousePosition.ToPoint()
|
hoverInMainPage |
bool |
Hover state flag, read/write by subclasses; auto-maintained when AutoUpdateHitBox is on |
player |
Player |
Shortcut for Main.LocalPlayer (static) |
ID |
int |
Internal ID (read-only) |
Mod |
Mod |
Owning mod |
FullName |
string |
Internal full name |
| Property | Description |
|---|---|
keyLeftPressState |
Left button state; automatically returns the draw- or logic-thread value based on current context |
keyRightPressState |
Right button state, same as above |
keyMiddlePressState |
Middle button (scroll wheel click) state, same as above (static) |
MouseScrollDelta |
Mouse scroll wheel delta this frame, positive = up; equivalent to PlayerInput.ScrollWheelDeltaForUI (static) |
ShiftHeld |
Whether either Shift key is held (static) |
CtrlHeld |
Whether either Ctrl key is held (static) |
AltHeld |
Whether either Alt key is held (static) |
| Property | Type | Description |
|---|---|---|
OpenProgress |
AnimatedFloat |
Open progress [0, 1], smoothly follows IsOpen; use directly as opacity or scale |
HoverProgress |
AnimatedFloat |
Hover progress [0, 1], smoothly follows hoverInMainPage; useful for button highlight fade |
GlobalTimer |
float |
Accumulated time in seconds, frame-rate independent, only increments when Active; useful for looping animations |
IsLogicUpdate |
bool |
Static — whether the current execution is on the logic thread |
UIHandle provides a full open/close lifecycle. It's more robust than manually maintaining a boolean flag.
| Property | Description |
|---|---|
IsOpen |
Whether the UI is in the open state |
IsOpening |
Open() was called but OpenProgress hasn't reached 1 yet (transitioning in) |
IsClosing |
Close() was called but OpenProgress hasn't reached 0 yet (fading out); Draw() is still called during this time |
| Method | Description |
|---|---|
Open() |
Opens the UI; does nothing if already open. Fires: OnOpening → OnOpen() → OpenSound → OnOpened
|
Close() |
Closes the UI; does nothing if already closed. Fires: OnClosing → OnClose() → CloseSound → OnClosed. Also force-ends any active drag |
Toggle() |
Toggles between open and closed |
SnapOpenProgress() |
Immediately jumps OpenProgress to its target value, skipping the transition animation (useful after scene changes to avoid a sudden slide-in) |
| Hook | Description |
|---|---|
OnOpen() |
Called once when the UI opens (before OnOpened event), place initialization logic here |
OnClose() |
Called once when the UI closes (before OnClosed event), place cleanup logic here |
OpenSound |
Sound to play when opening, null by default |
CloseSound |
Sound to play when closing, null by default |
CloseOnEscape |
If true, pressing ESC automatically calls Close(). Defaults to false
|
myUI.OnOpening += ui => { /* before open sequence begins */ };
myUI.OnOpened += ui => { /* after open sequence completes */ };
myUI.OnClosing += ui => { /* before close sequence begins */ };
myUI.OnClosed += ui => { /* after close sequence completes */ };Calling
Open()orClose()recursively from within any lifecycle event or hook is silently ignored to prevent state corruption.
| Layer | Description |
|---|---|
Vanilla_Mouse_Text |
Mouse text layer (default), topmost |
Vanilla_Interface_Logic_1 |
Mouse item interaction logic layer |
Vanilla_MP_Player_Names |
Multiplayer player names/avatar layer |
Vanilla_Hide_UI_Toggle |
UI show/hide toggle layer |
Vanilla_Resource_Bars |
HP/MP/buffs layer |
Vanilla_Ingame_Options |
In-game options menu layer |
Vanilla_Diagnose_Net |
Network diagnostics layer |
Mod_MenuLoad |
Main menu layer; MenuLogicUpdate() only works here |
Mod_UIModItem |
Mod icon interface layer |
None |
No auto-update; requires manual calls to UIHanderElementUpdate
|
LayersModeis read only once during load. Changing it at runtime does not re-insert the UI into a different layer.
| State | Description |
|---|---|
None |
Not pressed |
Pressed |
Pressed this frame (single-frame trigger) |
Held |
Held continuously |
Released |
Released this frame (single-frame trigger) |
keyLeftPressState, keyRightPressState, and keyMiddlePressState each automatically return the value from the correct thread-local copy based on IsLogicUpdate, so draw-thread and logic-thread code will never cross-read stale state.
When LayersMode is None, global key states are not updated. Use the instance methods CheckLeftKeyState() / CheckRightKeyState() instead (call at most once per update cycle and cache the result). You can also use GetKeyState(MouseButtonType) to query any button by enum value.
AnimatedFloat is a lightweight easing wrapper for UI animations. Each frame it Lerps Current toward Target, with exponential frame-delta compensation so the animation speed stays consistent at 60 FPS, 144 FPS, or any other framerate.
public AnimatedFloat(float initial, float speed = 0.12f, float snapEpsilon = 0.001f)| Field/Property | Description |
|---|---|
Current |
Current value, advanced each frame by Update()
|
Target |
Target value; set this to start a transition |
Speed |
Lerp coefficient (approach ratio per frame at 60 FPS); higher = faster transition |
SnapEpsilon |
When the difference is below this threshold, Current snaps to Target directly, preventing endless micro-jitter |
IsSettled |
true when Current == Target
|
af.TweenTo(1f); // set target, begin transitioning
af.Snap(0f); // jump immediately, no animation
af.Update(frames); // advance one frame; frames is provided by UIHandleLoaderAnimatedFloat has an implicit conversion to float, so you can write:
spriteBatch.Draw(Texture, DrawPosition, null, Color.White * OpenProgress, ...);OpenProgress and HoverProgress are both AnimatedFloat instances managed by the base class — subclasses do not need to call Update() on them manually.
| Property | Default | Description |
|---|---|---|
AutoUpdateHitBox |
false |
When enabled, the base class updates UIHitBox and hoverInMainPage from DrawPosition + Size before each Update() call |
BlockMouseWhenHovered |
false |
When enabled, sets player.mouseInterface = true while hoverInMainPage is true, preventing in-game mouse interactions |
Both default to false to avoid breaking existing UIs that maintain these manually.
The base class includes drag support with a global mutex so only one UI can be dragged at a time (UIHandleLoader.CurrentDragOwner).
| Property/Method | Default | Description |
|---|---|---|
CanDrag |
false |
Enable drag support |
DragMouseButton |
Right |
Which mouse button triggers drag (MouseButtonType enum) |
DragHandleRect |
null |
Restricts the drag trigger area (e.g. a title bar); null means the entire UIHitBox
|
IsDragging |
— | Whether this UI is currently being dragged (read-only) |
OnDragStart() |
— | Called when drag begins |
OnDragEnd() |
— | Called when drag ends |
Minimal drag setup:
public override bool CanDrag => true;
public override bool AutoUpdateHitBox => true; // UIHitBox must be maintainedCalling Close() force-ends any active drag to prevent "ghost dragging" after the window closes.
To avoid writing UIHandleLoader.GetUIHandleOfType<T>() everywhere, inherit the generic version instead:
internal class MyUI : UIHandle<MyUI>
{
// Access the instance via MyUI.Instance — no extra declaration needed
}UIHandle<TSelf> exposes three accessors:
| Property/Method | Description |
|---|---|
TSelf.Instance |
Returns the instance directly; throws if the type isn't registered |
TSelf.InstanceOrNull |
Returns null instead of throwing when not registered |
TSelf.TryGetInstance(out TSelf) |
Standard try-get pattern |
| Method | When Called | Description |
|---|---|---|
Update() |
Every frame, draw thread | Called after BuiltinPreUpdate, before Draw()
|
LogicUpdate() |
Every frame, logic thread; not called in main menu | For data logic at a stable 60 ticks/sec |
MenuLogicUpdate() |
Main menu only, fixed 60 ticks/sec | Simulated via a tick accumulator on the draw thread; only active on the Mod_MenuLoad layer |
Draw(SpriteBatch) |
Every frame, draw thread | Render the UI |
Call order per frame (draw thread):
BuiltinPreUpdate() // base: auto hitbox, drag, ESC close, OpenProgress advance
↓
Update() // your logic
↓
BuiltinPostUpdate() // base: HoverProgress advance
↓
Draw() // your rendering
| Method | When Called | Description |
|---|---|---|
OnEnterWorld() |
On world entry | Only fires for non-None/Mod_MenuLoad/Mod_UIModItem layers |
UnLoad() |
On game unload | Resource cleanup |
SaveUIData(TagCompound) |
On save | Write data under FullName as key |
LoadUIData(TagCompound) |
On load | Not called if SaveUIData wrote nothing |
The example below uses the new lifecycle system. No more manual _active boolean or hand-rolled easing:
internal class ExampleUI : UIHandle<ExampleUI>
{
public override Texture2D Texture => Mod.Assets.Request<Texture2D>("Asset/ExampleUIPanel").Value;
public override bool AutoUpdateHitBox => true;
public override bool BlockMouseWhenHovered => true;
public override bool CloseOnEscape => true;
public override void Update() {
Size = Texture.Size() * 2f * OpenProgress;
DrawPosition = new Vector2(Main.screenWidth, Main.screenHeight) / 2f - Size / 2f;
}
public override void Draw(SpriteBatch spriteBatch) {
// OpenProgress converts implicitly to float
spriteBatch.Draw(Texture, DrawPosition, null, Color.White * OpenProgress,
0f, Vector2.Zero, 2f * OpenProgress, SpriteEffects.None, 0f);
}
}
// Trigger open/close from a button UI
internal class ExampleButtonUI : UIHandle<ExampleButtonUI>
{
public override void Update() {
if (keyLeftPressState == KeyPressState.Pressed && hoverInMainPage) {
ExampleUI.Instance.Toggle();
}
}
}UIHandleLoader exposes global events for all three mouse buttons:
UIHandleLoader.LeftPressedEvent += () => { };
UIHandleLoader.LeftHeldEvent += () => { };
UIHandleLoader.LeftReleasedEvent += () => { };
UIHandleLoader.RightPressedEvent += () => { };
UIHandleLoader.RightHeldEvent += () => { };
UIHandleLoader.RightReleasedEvent += () => { };
UIHandleLoader.MiddlePressedEvent += () => { };
UIHandleLoader.MiddleHeldEvent += () => { };
UIHandleLoader.MiddleReleasedEvent += () => { };Inherit GlobalUIHandle to intercept or modify all UI element behavior uniformly:
public class MyGlobalUI : GlobalUIHandle
{
// Called after key states have been updated for this frame
public override void UpdateKeyState() { }
// Called before a single UI element updates; return false to skip that element
public override bool PreUIHanderElementUpdate(UIHandle handle) => true;
// Called after a single UI element updates
public override void PostUIHanderElementUpdate(UIHandle handle) { }
// Called after all UI updates for the current frame
public override void PostUpdataInUIEverything() { }
}| Member | Description |
|---|---|
GetUIHandleInstance<T>() |
Get UI instance as base type |
GetUIHandleOfType<T>() |
Get type-safe UI instance |
GetUIHandleInstance(string name) |
Get instance by internal full name |
GetUIHandleID<T>() |
Get UI internal ID |
UIHandles |
List of all registered UI instances |
CurrentDragOwner |
The UI currently holding the drag lock (global mutex); external drag implementations should also set/clear this field |
CurrentFrameDelta |
Current frame's multiplier relative to 60 FPS (30 FPS ≈ 2, 144 FPS ≈ 0.42), clamped to [0.05, 5] to prevent extreme jumps |
UIHanderElementUpdate catches exceptions thrown by subclasses. After an error, the affected UI instance enters a 600-frame cooldown (ignoreBug) during which updates are skipped, preventing a single broken UI from affecting the rest of the system. Error details are written to Logger.Error.
| Previous | Next |
|---|---|
| Basic TP Entity | SaveContent Save System |