Controls.cs
using System;
using Sandbox;
using Sandbox.UI;
namespace Goo;
// Composed control factories (compose-list widgets). Stateless: each returns a
// Container subtree, following the Shapes/Skins idiom.
public static partial class Controls
{
// Maps a cursor X (in the root's rendered pixel frame) to a snapped, clamped value.
// Ratio-based (localX / width) so it is scale-invariant; engine UI scaling changes
// the absolute pixels but not the ratio (engine-fact-mousepanelevent-rendered-frame).
internal static float ValueAt(float localX, float width, float min, float max, float step)
{
if (width <= 0f || max <= min) return min;
float norm = Math.Clamp(localX / width, 0f, 1f);
float v = min + norm * (max - min);
if (step > 0f) v = MathF.Round(v / step) * step;
return Math.Clamp(v, min, max);
}
// Dev-diagnostic sink for the degenerate max<=min case (mirrors Skins.OnZeroBorder).
public static Action<string>? OnDegenerateRange;
static readonly Color TrackBg = new( 0.28f, 0.28f, 0.34f, 1f );
static readonly Color FillBg = new( 0.55f, 0.78f, 0.95f, 1f );
static readonly Color ThumbBg = new( 0.95f, 0.96f, 1.00f, 1f );
// Controlled, stateless horizontal slider: the caller owns value, updates it in onChanged, and re-renders. Press-to-jump + drag within the bar (no engine move-capture; engine-fact-sbox-ui-input-and-drag); key pins reconciler identity so the Active pointer survives per-move re-renders.
public static Container Slider(
float value, float min, float max, float step,
Action<float> onChanged, string? key = null )
{
if ( max <= min )
OnDegenerateRange?.Invoke( $"Goo.Controls.Slider: max ({max}) <= min ({min}); rendering an inert track." );
float pct = (max > min ? Math.Clamp( (value - min) / (max - min), 0f, 1f ) : 0f) * 100f;
void Set( MousePanelEvent e )
=> onChanged( ValueAt( e.LocalPosition.x, e.Target.Box.Rect.Size.x, min, max, step ) );
return new Container
{
Key = key,
PointerEvents = PointerEvents.All,
Width = Length.Percent( 100 ),
Height = 20f,
FlexDirection = FlexDirection.Column,
JustifyContent = Justify.Center,
OnMouseDown = Set, // press jumps to position
OnMouseMove = e => { if ( e.Target.HasActive ) Set( e ); }, // drag while pressed
Children =
{
new Container
{
Key = "track",
PointerEvents = PointerEvents.None,
Position = PositionMode.Relative, // positioned ancestor for fill/thumb
Width = Length.Percent( 100 ),
Height = 7f,
BorderRadius = 4f,
BackgroundColor = TrackBg,
Children =
{
new Container
{
Key = "fill",
PointerEvents = PointerEvents.None,
Position = PositionMode.Absolute,
Left = 0f,
Height = Length.Percent( 100 ),
Width = Length.Percent( pct ),
BorderRadius = 4f,
BackgroundColor = FillBg,
},
new Container
{
Key = "thumb",
PointerEvents = PointerEvents.None,
Position = PositionMode.Absolute,
Left = Length.Percent( pct ),
Top = Length.Percent( 50 ),
Width = 16f,
Height = 16f,
BorderRadius = Length.Percent( 50 ),
BackgroundColor = ThumbBg,
// Center the thumb on the (x = value, y = track-mid) point.
// Length.Percent returns Length?, never null here; ?? default unwraps
// it the same way Px.Of does (the codebase's nullable-Length idiom).
Transform = PanelTransform.Translate( Length.Percent( -50 ) ?? default, Length.Percent( -50 ) ?? default ),
},
},
},
},
};
}
}