goo for AI¶
goo is a C#-only retained UI framework over Sandbox.UI.Panel for s&box. Build() returns a tree of blob value-structs; goo diffs it against the previous tree and applies minimal ops to the engine panels. drop the React/Razor priors: there is no markup, no stylesheet, no every-frame render loop. the tree is plain C#, rebuilt only when asked.
the surface map¶
reach for these before hand-rolling anything:
| need | use |
|---|---|
| layout, style, events | Container plus blobs: Text, Image, Sector, Arc, Polygon, ScenePanel, SvgPanel, WebPanel, TextEntry, Embed, Virtual |
| full-screen HUD scaffolding | Hud.Overlay(), Hud.Anchored(anchor, content), Hud.Fill(), Hud.Scrim(color), Hud.Wallpaper(tex, path), Hud.Spacer(), Hud.Divider() |
| corner pinning math | Layout.Anchor (4 corners, top/bottom center, center) via Hud.Anchored |
| rings, discs, wedges | Goo.Shapes (Ring, Disc) and the raw shape blobs |
| ready-made controls | Goo.Controls.Button(...), Goo.Controls.Slider(...) |
| encapsulated stateful widgets | Cell.Mount<TCell>(key, seed, configure) - see composition |
| dampers and tweens | Goo.Animation: Spring/Smooth/Decay x Float/Color/Vector2, Tween, Animator, Timeline, Easing |
| custom shaders on a panel | Effect = new ShaderEffect("shaders/x.shader", GrabMode.None/Sharp/Blurred) { ["Uniform"] = value } |
| tag-addressed effects | Tag = "frost" on a blob + override EffectMap Effects => new() { ["frost"] = effect } on the host; inline Effect wins |
| screen-space particles | a ParticleField field + Hud.Particles(field) overlay; spawn via field.Burst(...)/Emit(...) - self-driving, no Tick needed - see particles |
| virtualized long lists/grids | Virtual.List(items, itemHeight, row) / Virtual.Grid(items, itemSize, row) - only visible rows exist as panels; keyed stateful rows - see virtual lists |
| keyboard | Goo.Input.KeyTracker (Poll() per frame, JustPressed, modifiers) - see input |
| drag and drop | DragSource / DropZone / DragLayer - see drag-and-drop |
| drag math under UI scale | PointerDrag with Current(Panel) |
| theming values | Tokens.Scope / Tokens.Get - see tokens |
hard rules¶
-
C# only. no Razor, no SCSS, no markup, no DSL.
-
only the twelve blob types exist. do not invent or subclass blob types. helpers like
Shapes.RingreturnContainersubtrees.Embedis the escape hatch: it mounts an arbitrary enginePanel(Create runs once at mount, Update runs every owner rebuild) and goo never diffs inside it. -
init-only property syntax. no fluent chains;
.Padding(8)does not exist. properties go inside{ }on construction. to extend an already-built blob use awithexpression (this is your style-spread);withshallow-copies, so the copy shares the original'sChildrenlist - call helpers fresh per use. -
children go in
Children = { ... }. mixing init properties with bare child items in one brace block is a CS0747 compile error. after constructionChildrenis a live list:AddandAddRange(items, (i, item) => ...)(auto-keys by index; yourKeywins). two elements work inline in the initializer itself: a nullable child skips silently (Children = { header, cond ? badge : null }, also the idiom for a conditional child) andEach.Of(items, (i, item) => ...)runs a loop in place with the same auto-keying asAddRange(pass akeyOfargument for lists that reorder). -
mount by subclassing
GooPanel<TRoot>on a GameObject under aScreenPanelorWorldPanel. no manual instantiation, noRender(). -
Build()does not run every frame. it runs at mount, hotload, re-enable, and on rebuild. event handlers trigger a rebuild automatically after they run, soOnClick = e => _count++is complete - noRebuild()call needed in handlers. state that changes outside a handler (timers, network, polls) either callsRebuild()manually or, better, lives in aState<T>created withTrack(initial)on the panel or cell: writingstate.Value = xmarks the owner dirty automatically (equal writes are no-ops). for continuous motion overrideTick(float dt)and returntruewhile moving. -
state lives in fields on the
GooPanel(orCell) subclass. no hooks, no observables, no binding. for a reusable widget with private state, subclassCell<TRoot>and mount withCell.Mount<TCell>(seed: c => c.Initial = x, configure: c => c.Label = y)-seedruns once at first mount,configureruns every rebuild and wins on overlap. -
state variants are free.
HoverBackgroundColor,ActiveFontColor,FocusBackgroundColoretc resolve engine-side with no rebuild. never wireOnMouseEnter/OnMouseLeavejust for visual feedback. pair withTransitionMsfor fades. declare the restingBackgroundColorand its variant together on the same blob. -
compose controls. a button is a
ContainerwithOnClickand a hover variant;Goo.Controlscovers button/slider; everything else is function extraction:static Container Card(string title) => new Container { ... };. repeated property bundles are a missing helper, not a style class. promote repeated literals to a theme constants class. -
never hold a
Containeracross rebuilds. its children list comes from a per-build pool. extract the function that builds it; never cache the blob in a field orstatic readonly.TextandImageare pool-free and safe to cache. -
key every child of a list that reorders, filters, or grows, and never mix keyed with unkeyed siblings. a mixed list makes the reconciler abandon keys for the whole list (a warning plus real per-frame cost). keys must be unique and derived from a stable id, never a display value that can repeat.
Children.AddRangekeys loops for you. -
Position = Absoluteresolves against the nearest positioned ancestor. give the intended parentPosition = PositionMode.Relative. the inverse trap: sibling shapes meant to overlap must each bePosition = Absolute, Top = 0, Left = 0or flex lays them side by side (shapes). -
animate through the cheap channels. per-frame changes to transforms (
Transform = PanelTransform.Rotate(deg).Scale(s)), colors, opacity, and positions are style writes and effectively free. per-frame changes to these are NOT free and tank the frame rate:Polygonpoints driven every frame bake a new points-texture each frame - keep the points fixed and move the panel with aTransforminstead.SectorandArcgeometry (StartAngle/EndAngle, radii,StrokeWidth) is shader uniforms (GPU SDF, no bake) and cheap to animate freely;- any style on a panel carrying state variants re-emits its hover stylesheet each frame - keep variant-carrying panels statically styled;
- shader motion belongs in uniforms: a
ShaderEffectuniform accepts a literal or a per-frameFunc<T>(["LightPos"] = (Func<Vector2>)(() => Mouse.Position / Screen.Size)- the explicitFunccast is required, a bare lambda does not compile), bypassing the reconciler entirely.
-
animation state is a damper field, advanced in
Tick, sampled inBuild.SpringFloat _pulse = new(1f, 4f, 0.5f);then_pulse.Tick(dt)inTick, set.Target(or kick.Velocity) on input, read.CurrentinBuild. easing names are exactly:Linear, Ease, EaseIn, EaseOut, EaseInOut, ExpoIn, ExpoOut, ExpoInOut, BounceIn, BounceOut, BounceInOut, SineIn, SineOut, SineInOut, StepStart, StepEnd. -
do not declare
PointerEventsdefensively. goo auto-gates: handlers (orTextEntry/WebPanel) resolve toAll, state variants forceAll, everything else resolves toNone. the one case auto-gating does not cover: a scroll viewport, which needs an explicitPointerEvents.Allor it looks inert. a scroll container needs all three of: a bounded size on the scroll axis,OverflowY = OverflowMode.Scroll(notAuto, notHidden), andPointerEvents = PointerEvents.All. -
SwallowClick = truestops a click from bubbling. use it on content drawn over a dismiss-on-click scrim. an emptyOnClick = e => { }does NOT stop propagation. -
if a property seems missing, do not invent it. the generated files
Container.Style.g.cs,Text.Style.g.cs, etc under the goo library'sCode/Core/are the property tables (grep*.Style.g.csfrom the project root). if it is not there, it does not exist - say so instead of guessing. -
two C# name traps.
PanelTransformis ambiguous betweenGooandSandbox.UIwhen both are imported: addusing PanelTransform = Goo.PanelTransform;. do not alias anything toUI- it collides with theSandbox.UInamespace from insidenamespace Sandbox. (the ready-made controls sidestep a third trap by construction: the static class isControls, notComponents, so a bare reference never collides with an app-levelComponents.Button- justusing Goo;thenControls.Button(...).)
canonical shapes¶
every Goo source file opens with this header (the PanelTransform alias from rule 18 is non-optional whenever you touch Transform, and dampers need Goo.Animation):
using Goo;
using Sandbox;
using Sandbox.UI;
using PanelTransform = Goo.PanelTransform; // rule 18: PanelTransform is otherwise ambiguous with Sandbox.UI
using Goo.Animation; // only when using a damper (SpringFloat/SmoothFloat/DecayFloat/...)
counter (state, auto-rebuild, hover variant)¶
public class CounterUI : GooPanel<Container>
{
int _count;
protected override Container Build() => new Container
{
Padding = 16, Gap = 12, FlexDirection = FlexDirection.Row, AlignItems = Align.Center,
BackgroundColor = Color.White,
Children =
{
new Text( _count.ToString() ),
new Container
{
Padding = 8, BorderRadius = 6,
BackgroundColor = Color.Gray, HoverBackgroundColor = Color.White, TransitionMs = 150,
OnClick = e => _count++, // handler triggers the rebuild automatically
Children = { new Text( "+" ) },
},
},
};
}
continuous motion (Tick + damper + transform channel)¶
using Goo;
using Goo.Animation; // SpringFloat lives here, NOT in Goo
using Sandbox;
using PanelTransform = Goo.PanelTransform; // required: Sandbox.UI also defines PanelTransform
public class PulseUI : GooPanel<Container>
{
float _t;
SpringFloat _scale = new( 1f, 4f, 0.5f );
protected override bool Tick( float dt ) { _t += dt; _scale.Tick( dt ); return true; }
protected override Container Build() => new Container
{
Width = 64, Height = 64, BorderRadius = 8,
Transform = PanelTransform.Rotate( _t * 45f ).Scale( _scale.Current ),
BackgroundColor = Color.Red,
OnClick = e => _scale.Velocity += 5f, // springy kick on click
};
}
generated list (AddRange auto-keys)¶
var grid = new Container { FlexWrap = Wrap.Wrap, Gap = 4, Width = 232 };
grid.Children.AddRange( _items, ( i, item ) => new Container
{
Width = 52, Height = 52,
BackgroundColor = item.Color, HoverBackgroundColor = Color.White,
OnClick = e => Select( item ),
} );
HUD with anchored corners and an effect layer¶
protected override Container Build()
{
var root = Hud.Overlay(); // full-screen, pointer-through, Column
root.Children.Add( Hud.Fill() with
{
Effect = new ShaderEffect( "shaders/ui_particles.shader" ) { ["Speed"] = 0.4f },
} );
root.Children.Add( Hud.Anchored( Layout.Anchor.TopRight, Minimap(), padding: Px.Of( 16 ) ) );
root.Children.Add( Hud.Anchored( Layout.Anchor.BottomLeft, HealthCard(), padding: Px.Of( 32 ) ) );
return root;
}
note: a Hud factory result (and any pre-styled factory output) is overridable with with; goo style lists resolve last-declared-wins, so place an override after the factory or shorthand, never before.
scrollable container¶
var viewport = new Container
{
Height = 240, // bounded so children overflow
FlexDirection = FlexDirection.Column,
OverflowY = OverflowMode.Scroll, // Scroll, not Auto or Hidden
PointerEvents = PointerEvents.All, // required or the wheel never arrives
};
no visible scrollbar is rendered; scrolling is wheel and drag. programmatic scroll offset is not exposed on blobs - surface that as a gap rather than faking it.
see also¶
- animations - dampers, tweens, easings, timelines
- composition - Cell, stateless presenters, config structs
- events - the event payload,
PositionIn, pointer-events policy, click swallowing - overlay-layout - Hud helpers in depth
- shapes - Sector/Arc/Polygon, stacking, the Polygon points-texture trap
- web panel - embedding a live Chromium webview
- svg panel - rendering imported
.svgart at any resolution - text input - controlled and uncontrolled
TextEntryfields - gotchas - sizing, null-ternary colors, engine quirks
- published reference pages: container, text, image, and the other
*-reference.mdpages are the authoritative property tables
if anything in this primer contradicts the code, the code wins: trust the live library and open an issue on the goo repository.