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 |
| 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 } |
| 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 ten blob types exist. do not invent or subclass blob types. helpers like
Shapes.RingreturnContainersubtrees. -
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:Add,AddRange(items, (i, item) => ...)(auto-keys by index; yourKeywins),AddIf(cond, child). -
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. callRebuild()only when state changes outside a handler (timers, network, polls). 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. never mix keyed and unkeyed siblings in one list - the reconciler abandons keys for the whole list and it costs real performance on top of the warning. keys must also be unique within the list: derive them from a stable id, never from a display value that can repeat (two dropped "ruby" items must not both be
row-ruby); a duplicate key likewise abandons keys for the list and warns.Children.AddRangeauto-keys 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:- shape geometry (
StartAngle, radii, polygon points) re-bakes a mask texture each frame - sweep a fixed wedge with aTransforminstead; - 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)), bypassing the reconciler entirely.
- shape geometry (
-
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 place you must declare it:PointerEvents.Allon a scroll viewport, which otherwise 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;. inside any component class, a bareComponentsresolves to the engine'sComponentListproperty, so alias static component classes (using Kit = Goo.Components.Components;). do not alias anything toUI- it collides with theSandbox.UInamespace from insidenamespace Sandbox.
canonical shapes¶
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)¶
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;
}
Hud factory style fields are first-declared and cannot be overridden via with; goo style lists resolve first-declared-wins, so declare per-edge before shorthand and do not redeclare what a factory set.
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, geometry-animation trap
- 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. file a bug.