Table of Contents

Class UITKControl

Namespace
FishMMO.Client
Assembly
FishMMO.Client.dll

Abstract base class for UI Toolkit-backed controls. Drives a UnityEngine.UIElements.UIDocument and owns the show/hide, focus, drag and Escape-close behaviour every panel shares. Automatically registers and unregisters with UIManager on Awake/OnDestroy.

public abstract class UITKControl : MonoBehaviour
Inheritance
Object
Component
Behaviour
MonoBehaviour
UITKControl
Derived
Inherited Members
MonoBehaviour.IsInvoking()
MonoBehaviour.CancelInvoke()
MonoBehaviour.StopCoroutine(Coroutine)
MonoBehaviour.StopAllCoroutines()
MonoBehaviour.destroyCancellationToken
MonoBehaviour.useGUILayout
MonoBehaviour.didStart
MonoBehaviour.didAwake
MonoBehaviour.runInEditMode
Behaviour.enabled
Behaviour.isActiveAndEnabled
Component.GetComponent<T>()
Component.TryGetComponent<T>(out T)
Component.GetComponentInChildren<T>()
Component.GetComponentsInChildren<T>()
Component.GetComponentInParent<T>()
Component.GetComponentsInParent<T>()
Component.GetComponents<T>()
Component.GetComponentIndex()
Component.CompareTag(TagHandle)
Component.transform
Component.transformHandle
Component.gameObject
Component.tag
Object.GetEntityId()
Object.GetInstanceID()
Object.GetHashCode()
Object.InstantiateAsync<T>(T)
Object.InstantiateAsync<T>(T, Transform)
Object.InstantiateAsync<T>(T, Vector3, Quaternion)
Object.InstantiateAsync<T>(T, Transform, Vector3, Quaternion)
Object.Instantiate(Object, Vector3, Quaternion)
Object.Instantiate(Object, Vector3, Quaternion, Transform)
Object.Instantiate(Object)
Object.Instantiate(Object, Scene)
Object.Instantiate<T>(T, InstantiateParameters)
Object.Instantiate<T>(T, Vector3, Quaternion, InstantiateParameters)
Object.Instantiate(Object, Transform)
Object.Instantiate<T>(T)
Object.Instantiate<T>(T, Vector3, Quaternion)
Object.Instantiate<T>(T, Vector3, Quaternion, Transform)
Object.Instantiate<T>(T, Transform)
Object.Destroy(Object)
Object.DestroyImmediate(Object)
Object.DontDestroyOnLoad(Object)
Object.DestroyObject(Object)
Object.FindObjectsOfType<T>()
Object.FindObjectsByType<T>(FindObjectsSortMode)
Object.FindObjectsByType<T>(FindObjectsInactive, FindObjectsSortMode)
Object.FindObjectOfType<T>()
Object.FindFirstObjectByType<T>()
Object.FindAnyObjectByType<T>()
Object.FindFirstObjectByType<T>(FindObjectsInactive)
Object.FindAnyObjectByType<T>(FindObjectsInactive)
Object.FindObjectsByType<T>()
Object.FindObjectsByType<T>(FindObjectsInactive)
Object.ToString()
Object.name
Object.hideFlags

Fields

CloseOnEscape

Whether Escape closes this panel.

public bool CloseOnEscape

Field Value

bool

Remarks

Separate from ReleasesCursor. The two were briefly merged because PlayerInputController used "is anything Escape-closable" as its test for whether to keep the mouse cursor free — so a panel that released the cursor without registering for Escape had it taken straight back.

That proxy was the thing at fault, not the separation: the question the input controller actually wants is "is any panel that needs the cursor on screen", which AnyCursorReleasingVisible() now answers directly. Keeping the flags apart matters because they genuinely differ — a confirm dialog needs the cursor but must not be dismissable with Escape, since the point of it is that the player chooses.

CloseOnQuitToMenu

If true, closes this panel when quitting to the login menu.

public bool CloseOnQuitToMenu

Field Value

bool

Document

The UIDocument component that owns the visual tree for this control. Assign in the Inspector.

[Tooltip("UIDocument component backing this control.")]
public UIDocument Document

Field Value

UIDocument

IsAlwaysOpen

If true, Hide() calls are ignored — the panel cannot be closed.

public bool IsAlwaysOpen

Field Value

bool

OnLoseFocus

Raised when the pointer leaves this panel, having previously been over it.

public Action OnLoseFocus

Field Value

Action

Remarks

Two panels use this to dismiss themselves: the dropdown and the chat channel picker both do OnLoseFocus += Hide, so moving the mouse off them closes them without a click. UI Toolkit has no equivalent event on a panel root — every root fills the screen, so the pointer never leaves one — which is why this is derived from HasFocus rather than from a PointerLeaveEvent.

Polling is gated on there being a subscriber, so a panel nobody listens to costs nothing: HasFocus runs a pick against the panel, and doing that every frame for all forty-odd panels to serve two of them would be wasteful.

ReleasesCursor

If true, showing this panel releases the mouse cursor so it can be clicked.

[Tooltip("Show() releases the mouse cursor. Enable for windows and dialogs, not for HUD elements.")]
public bool ReleasesCursor

Field Value

bool

Remarks

A window the player interacts with needs the cursor, and a HUD element that is permanently on screen must not take it away. Defaulting this to false keeps bars and hotkey strips from stealing the cursor the moment they appear.

Only ever set true here, never false — releasing the cursor is a panel's business but recapturing it is not, since another panel may still be open. PlayerInputController and Client own the reset.

StartOpen

If true, the panel is visible when the scene starts.

public bool StartOpen

Field Value

bool

Properties

CanDrag

Whether the player can drag this panel around the screen.

protected virtual bool CanDrag { get; }

Property Value

bool

Remarks

Derived from Layer and declared in code for the same reasons the layer is — see UITKPanelLayer. It used to be a serialized field, off by default, which meant a window was draggable only if somebody remembered to tick a box on its GameObject: of the thirty-one panels in ClientWorldGUI exactly three had the field serialized at all and all three had it false, so the whole drag subsystem below — and the panel-header handle authored into nearly every window's UXML — was dead code in the world scene.

Windows and dialogs are the things a player arranges; the HUD stays where its stylesheet puts it, and popups, tooltips and full-screen overlays are transient or cover the screen anyway. A newly written window therefore inherits "draggable" instead of silently not being.

The login-flow screens override this to false: they carry a header, but they are full-screen forms rather than windows, and there is nowhere for them to be dragged to.

Client

Injected Client instance for network/UI interaction.

public Client Client { get; }

Property Value

Client

ConsumesEscape

True while this panel is using Escape for something of its own, so a press must be consumed by it rather than closing anything.

public virtual bool ConsumesEscape { get; }

Property Value

bool

Remarks

Distinct from IsAlwaysOpen, which means "never closable" and lets CloseNext(bool) move on to the next panel down. This means "the press belongs to me right now", so nothing closes at all — moving on would close the window behind the one the player is actually interacting with.

The one case today is the settings panel while it is listening for a key to rebind: the row says "Escape cancels", and the Input System does not suppress that press, so without this the same keystroke cancels the rebind and closes the settings window.

FocusOnSelect

Whether clicking this panel raises it above its peers and makes it the next panel Escape closes.

protected virtual bool FocusOnSelect { get; }

Property Value

bool

Remarks

This used to be a per-scene flag, set on every window and on chat and left off for pure HUD readouts — a health bar has nothing to raise above and nothing to focus. The same split falls out of the layer, so it no longer has to be authored: windows and the menu are things the player opens, arranges and closes, and everything at Hud is a readout. Panels that disagree override this.

HasFocus

True when the pointer is over this panel or one of its elements has keyboard focus.

public bool HasFocus { get; }

Property Value

bool

Remarks

UI Toolkit already tracks both facts, so this reads them rather than shadowing them in a field that can fall out of step. A cached flag maintained by pointer enter/leave handlers is the trap here: a panel destroyed while the pointer is inside never gets its exit event, and the flag stays true forever.

IsInputFieldFocused

public bool IsInputFieldFocused { get; }

Property Value

bool

Layer

Draw and input order for this panel. Override to place a panel outside Window.

protected virtual UITKPanelLayer Layer { get; }

Property Value

UITKPanelLayer

Remarks

See UITKPanelLayer for why this is declared in code rather than set on each UIDocument in the scene.

Name

GameObject name; used as the key in UIManager.

public string Name { get; }

Property Value

string

Root

Shortcut to the UIDocument's root VisualElement. Returns null if Document is not assigned.

protected VisualElement Root { get; }

Property Value

VisualElement

Visible

True when the UIDocument is enabled (panel is rendered and interactive).

public bool Visible { get; }

Property Value

bool

Methods

BindListChrome(VisualElement, Label, Label, Label, string, string)

Keeps a list panel's header count, status line and empty placeholder in step with the number of rows in its container.

protected void BindListChrome(VisualElement list, Label count, Label subtitle, Label empty, string singular, string plural)

Parameters

list VisualElement

Container whose children are the rows.

count Label

Header badge showing the row count. May be null.

subtitle Label

Header line describing the list. May be null.

empty Label

Placeholder shown when the list has no rows. May be null.

singular string

Noun for exactly one row, e.g. "item".

plural string

Noun for zero or many rows, e.g. "items".

Remarks

Driven off the container's own geometry rather than from each panel's rebuild path. The panels that own these lists fill them from half a dozen different broadcast handlers, and hooking every one of them is how a count ends up disagreeing with the list beneath it. UI Toolkit has no "children changed" event, but adding or removing a row relayouts the container, so its geometry is a reliable proxy — and a redundant recount costs one integer read.

BringToFront()

Raises this panel above the others in its layer.

public void BringToFront()

Remarks

A Canvas would re-parent the transform to make the panel the last sibling. Documents have no sibling order to exploit, so the equivalent is to renumber the tier: this goes to the end of its layer's list and every panel in that layer is re-stamped from the layer's base value.

Hide()

Hides the panel unless IsAlwaysOpen is true.

public void Hide()

Remarks

Deliberately NOT virtual. This overload only forwards to Hide(bool), so an override here would be bypassed entirely by every caller that uses the bool form — and quit-to-login is one of them. That trap cost a CRITICAL bug: UITKLoadingScreen overrode this method, its teardown never ran on quit-to-login, and the overlay stayed over the login screen forever with no way out but Alt+F4. The colour picker, the dropdown and the drag object each silently lost their cleanup the same way.

With only Hide(bool) overridable there is exactly one method to override and no wrong choice to make. Panels needing teardown override that one and check overrideIsAlwaysOpen to tell a real hide from a refused one.

Hide(bool)

Hides the panel unless overrideIsAlwaysOpen is true.

public virtual void Hide(bool overrideIsAlwaysOpen)

Parameters

overrideIsAlwaysOpen bool

When true, the call is a no-op.

OnAfterShow()

Called at the end of Show(), against the tree the player will actually see. Override to write content that changes from one opening to the next.

protected virtual void OnAfterShow()

Remarks

This exists because "set the text, then Show" does not work and fails silently. UnityEngine.UIElements.UIDocument clones the UXML on enable, so a panel that fills in its message, list rows or icon before calling Show() writes into a tree that is thrown away microseconds later, and the player sees whatever the UXML declares — an empty dialog, a blank tooltip, a selector with no rows.

Distinct from OnAfterStarting(), which re-applies state after the tree is rebuilt. This one runs on every show, rebuilt tree or not.

OnAfterStarting()

Called immediately after OnStarting(). Override to re-apply any state that arrived before the visual tree existed.

protected virtual void OnAfterStarting()

Remarks

Initialisation is no longer guaranteed to happen before the control is given data. A panel that starts hidden has no visual tree until it is shown, and world entry can hand it a character in the meantime — so whatever it was told before it had elements to write into has to be applied again here, or the panel stays blank.

OnClientSet()

Called when a Client is injected via SetClient(Client). Override to subscribe to client events.

public virtual void OnClientSet()

OnClientUnset()

Called when the Client is cleared via SetClient(Client). Override to unsubscribe from client events.

public virtual void OnClientUnset()

OnDestroying()

Called at the start of the MonoBehaviour OnDestroy function. Override to clean up event subscriptions and managed resources.

public virtual void OnDestroying()

OnQuitToLogin()

Called when the client quits to the login screen. Override to perform cleanup such as stopping coroutines.

public virtual void OnQuitToLogin()

OnStarting()

Called once the control's visual tree is available. Override to perform one-time initialisation against Root.

public virtual void OnStarting()

Remarks

Guaranteed to see a populated Root. For a control that starts hidden this runs when it is first shown rather than at Awake, because a hidden panel's UIDocument is disabled and has no tree to query.

OnTick()

Per-frame hook for controls that need one.

protected virtual void OnTick()

ReloadStoredPosition()

Re-reads this panel's stored position and moves it there, or returns it to the stylesheet when nothing is stored.

public void ReloadStoredPosition()

Remarks

For the case where the stored position changed underneath the panel rather than because of it — loading a shared UI profile is the only one today. FishMMO.Client.UITKControl.EnforcePosition() reads the stored value exactly once, on first layout, and remembers that it has; without this, a layout written into configuration would not appear until the client restarted.

The absence of a stored position is applied too, and deliberately: a profile that says nothing about a panel means that panel belongs where the stylesheet puts it. Leaving it wherever the player last dragged it would produce a layout that is neither theirs nor the one they loaded.

ResetPosition()

Returns the panel to the position its stylesheet gives it, ends any drag, and forgets the stored position.

public void ResetPosition()

Remarks

Clearing the inline styles rather than writing a remembered position hands the panel back to the USS that placed it, so it lands where an untouched install would put it on this screen size.

SetClient(Client)

Injects (or replaces) the Client instance. Handles cleanup of any previously injected client automatically.

public void SetClient(Client client)

Parameters

client Client

The new client instance, or null to clear.

Show()

Shows the panel by enabling the UnityEngine.UIElements.UIDocument.

public virtual void Show()

ToggleVisibility()

Toggles the panel's visibility.

public virtual void ToggleVisibility()