Symptom:
A widget that changes state, like a ColorSelector, is placed inside a container that dynamically sizes itself based on its content, like a UI.AutoPanel. When the user interacts with the widget (e.g., clicks a new color), the action is detected but the UI state does not update correctly. The click seems to have no effect. Debugging shows the click event fires, but the state change appears to be lost or reverted almost immediately.
Cause:
This complex issue stems from how the UI.AutoPanel container works. To determine its own size, it must run the UI code for its contents twice within a single frame:
- Calculation Pass: The UI logic is executed with a "null renderer." This pass processes input and calculates the layout of all child widgets to measure their total size, but does not draw anything.
- Drawing Pass: After the
AutoPanelknows its final size, it executes the same UI logic a second time, this time with the real renderer to actually draw the widgets to the screen.
The bug occurs when the Calculation Pass causes a side-effect that corrupts the state needed for the Drawing Pass.
- Diagnosis: The state change (database update, data refresh) was happening inside the
ifblock during the Calculation Pass. This was thought to be corrupting theAllTagslist that theforeachloop was iterating over, causing the Drawing Pass to fail. - Attempted Fix: Defer the state-changing logic. A flag or an
Actionwas set during the UI code, and the actual database update was invoked after theAutoPanelwidget had finished both its passes. - Result: Still failed. The UI interaction itself was being lost, not just the resulting action.
- Diagnosis: The
ColorSelectorused arefparameter. It was thought that the Calculation Pass was modifying thisrefparameter. Then, when the Drawing Pass started, it saw the already-modified value, concluded no change occurred, and returnedfalse. - Attempted Fix: Refactor
ColorSelectorto remove therefparameter and instead use anoutparameter to report the change. This would prevent the input state from being modified between passes. - Result: Still failed. While this was good practice for preventing side-effects, it did not address the core issue. The click event itself was still being consumed prematurely.
The true cause was that the Calculation Pass was processing the entire input-handling pipeline.
- During the Calculation Pass,
DrawButtonPrimitivecorrectly detects the mouse click. - As part of a successful click, it calls
state.ClearActivePress(). This resets the globalUIPersistentState, clearing which UI element is considered "pressed". - The Calculation Pass finishes.
- The Drawing Pass begins.
DrawButtonPrimitiveruns again for the same button. It processes the same mouse input, but becausestate.ActivelyPressedElementIdwas already cleared, it no longer detects a valid click release.
The first pass consumed and then erased the very input state the second pass needed to confirm the action.
Solution: The Calculation Pass must be made truly read-only with respect to any persistent, frame-to-frame UI state. It should be able to read input for layout purposes (like hover effects) but must be prohibited from modifying any state that persists beyond its own execution.
How to Fix:
- Introduce a "Layout Pass" Flag: A boolean flag,
IsLayoutPass, was added to theUIContext. - Activate Flag During Measurement: The
UI.CalculateLayoutmethod (used byAutoPanel) was modified to setContext.IsLayoutPass = truebefore executing the user's UI code in its Calculation Pass, and to reset it in afinallyblock. - Guard State Mutations: All methods in
UIPersistentStateandClickCaptureServerthat modify state (e.g.,SetFocus,ClearActivePress,RegisterClick,RequestCapture) were guarded with an initial check:if (UI.Context.IsLayoutPass) return;.
This ensures that during the Calculation Pass, the UI framework simply ignores any requests to change its core state, preserving it in a pristine condition for the final Drawing Pass where the user's input can be processed correctly and definitively.