Skip to content

en Basic UI

HoCha113 edited this page May 19, 2026 · 10 revisions

中文版本

What is UIHandle?

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-thread LogicUpdate() are separate
  • Frame-rate independent animation: AnimatedFloat + CurrentFrameDelta keep animation speed consistent at any framerate
  • Built-in open/close lifecycle: Open()/Close() + OpenProgress drive fade-in/out directly
  • Built-in drag support: Enable CanDrag to 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/LoadUIData hooks into the SaveContent system

Properties Reference

Base Fields and Properties

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

Button States

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)

Animation Progress

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

Open/Close Lifecycle

UIHandle provides a full open/close lifecycle. It's more robust than manually maintaining a boolean flag.

State Properties

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

Public Methods

Method Description
Open() Opens the UI; does nothing if already open. Fires: OnOpeningOnOpen()OpenSoundOnOpened
Close() Closes the UI; does nothing if already closed. Fires: OnClosingOnClose()CloseSoundOnClosed. 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)

Overridable Hooks

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

Events

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() or Close() recursively from within any lifecycle event or hook is silently ignored to prevent state corruption.


Render Layers (LayersModeEnum)

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

LayersMode is read only once during load. Changing it at runtime does not re-insert the UI into a different layer.


Key Press State (KeyPressState)

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

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 UIHandleLoader

AnimatedFloat 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.


Auto HitBox and Mouse Blocking

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.


Drag System

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 maintained

Calling Close() force-ends any active drag to prevent "ghost dragging" after the window closes.


Generic Variant UIHandle<TSelf>

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

Callback Reference

Update Hooks

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

Other Callbacks

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

Code Example

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();
        }
    }
}

Global Key Events

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 += () => { };

GlobalUIHandle (Global UI Hooks)

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() { }
}

UIHandleLoader Common Members

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

Error Protection

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.


Navigation

Previous Next
Basic TP Entity SaveContent Save System

Clone this wiki locally