A UI component Razor file for a Vector3 field axis cell. It renders a label and either a readout or an editable TextEntry, and implements mouse scrubbing (drag to change value), double-click to edit, fine-scrub when a modifier action is held, and commits edits via a setter callback.
@namespace Sunless.Libraries.UI
@using Sandbox
@using Sandbox.UI
@attribute [StyleSheet]
@inherits Panel
<root class="v3-cell axis-@AxisLower @(_scrubbing ? "is-scrubbing" : "") @(_editing ? "is-editing" : "")">
<div class="v3-tag">
@if ( _scrubbing )
{
<text><></text>
}
else
{
<text>@Axis</text>
}
</div>
@if ( _editing )
{
<TextEntry @ref=_entry Numeric=@true NumberFormat="@Format" Text="@_editBuffer" OnTextEdited="@OnEditBufferChanged" />
}
else
{
<div class="v3-readout">@Value.ToString(Format)</div>
}
</root>
@code
{
public string Axis { get; set; } = "X";
public float Value { get; set; }
public string Format { get; set; } = "0.###";
public float Min { get; set; } = float.MinValue;
public float Max { get; set; } = float.MaxValue;
public Action<float> Setter { get; set; }
// Pixels per 1.0 of value change. Calibrated for integer position ranges;
// pass higher for 0..1 normalized ranges.
public float PixelsPerUnit { get; set; } = 4f;
// Action name the project's input system has bound to "fine scrubbing"
// (Shift / Walk in most s&box setups). Empty disables fine-scrub.
public string FineScrubAction { get; set; } = "Run";
TextEntry _entry;
bool _armed;
bool _scrubbing;
bool _editing;
string _editBuffer = "";
bool _entryEverFocused;
Vector2 _downPos;
float _valueAtDown;
static Vector3FieldCell _activeScrubber;
public static bool IsAnyScrubbing => _activeScrubber is not null;
string AxisLower => Axis?.ToLowerInvariant() ?? "x";
const float DragThresholdPx = 4f;
const float FineMultiplier = 8f;
protected override void OnMouseDown( MousePanelEvent e )
{
base.OnMouseDown( e );
if ( e.MouseButton != MouseButtons.Left ) return;
if ( _editing ) return;
_armed = true;
_scrubbing = false;
_downPos = Mouse.Position;
_valueAtDown = Value;
e.StopPropagation();
}
protected override void OnDoubleClick( MousePanelEvent e )
{
base.OnDoubleClick( e );
if ( e.MouseButton != MouseButtons.Left ) return;
if ( _editing ) return;
EnterEditMode();
e.StopPropagation();
}
protected override void OnMouseMove( MousePanelEvent e )
{
base.OnMouseMove( e );
if ( !_armed ) return;
if ( !HasActive ) return;
var delta = Mouse.Position - _downPos;
if ( !_scrubbing )
{
if ( delta.Length <= DragThresholdPx ) return;
_scrubbing = true;
_activeScrubber = this;
StateHasChanged();
}
var fine = !string.IsNullOrEmpty( FineScrubAction ) && Input.Down( FineScrubAction );
var perUnit = PixelsPerUnit * (fine ? FineMultiplier : 1f);
var v = (_valueAtDown + delta.x / perUnit).Clamp( Min, Max );
if ( v != Value )
{
Value = v;
Setter?.Invoke( v );
}
e.StopPropagation();
}
protected override void OnMouseUp( MousePanelEvent e )
{
base.OnMouseUp( e );
if ( !_armed ) return;
var wasScrubbing = _scrubbing;
EndScrub();
if ( wasScrubbing ) e.StopPropagation();
}
public override void Tick()
{
base.Tick();
if ( _armed && !HasActive )
{
EndScrub();
}
if ( _editing && _entry is not null )
{
if ( Sandbox.UI.InputFocus.Current == _entry )
{
_entryEverFocused = true;
}
else if ( _entryEverFocused )
{
CommitAndExit();
}
}
}
void EndScrub()
{
if ( !_armed && !_scrubbing ) return;
_armed = false;
if ( _scrubbing )
{
_scrubbing = false;
if ( _activeScrubber == this ) _activeScrubber = null;
StateHasChanged();
}
}
void EnterEditMode()
{
_editBuffer = Value.ToString( Format );
_editing = true;
_entryEverFocused = false;
StateHasChanged();
}
void ExitEditMode()
{
_editing = false;
StateHasChanged();
}
void OnEditBufferChanged( string s )
{
_editBuffer = s;
}
void CommitAndExit()
{
if ( float.TryParse( _editBuffer, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var v ) )
{
v = v.Clamp( Min, Max );
Value = v;
Setter?.Invoke( v );
}
ExitEditMode();
}
public override void OnDeleted()
{
if ( _activeScrubber == this ) _activeScrubber = null;
base.OnDeleted();
}
protected override int BuildHash() => HashCode.Combine( Axis, Value, Format, _scrubbing, _editing );
}