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

  1. C# only. no Razor, no SCSS, no markup, no DSL.

  2. only the ten blob types exist. do not invent or subclass blob types. helpers like Shapes.Ring return Container subtrees.

  3. init-only property syntax. no fluent chains; .Padding(8) does not exist. properties go inside { } on construction. to extend an already-built blob use a with expression (this is your style-spread); with shallow-copies, so the copy shares the original's Children list - call helpers fresh per use.

  4. children go in Children = { ... }. mixing init properties with bare child items in one brace block is a CS0747 compile error. after construction Children is a live list: Add, AddRange(items, (i, item) => ...) (auto-keys by index; your Key wins), AddIf(cond, child).

  5. mount by subclassing GooPanel<TRoot> on a GameObject under a ScreenPanel or WorldPanel. no manual instantiation, no Render().

  6. 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, so OnClick = e => _count++ is complete - no Rebuild() call needed in handlers. call Rebuild() only when state changes outside a handler (timers, network, polls). for continuous motion override Tick(float dt) and return true while moving.

  7. state lives in fields on the GooPanel (or Cell) subclass. no hooks, no observables, no binding. for a reusable widget with private state, subclass Cell<TRoot> and mount with Cell.Mount<TCell>(seed: c => c.Initial = x, configure: c => c.Label = y) - seed runs once at first mount, configure runs every rebuild and wins on overlap.

  8. state variants are free. HoverBackgroundColor, ActiveFontColor, FocusBackgroundColor etc resolve engine-side with no rebuild. never wire OnMouseEnter/OnMouseLeave just for visual feedback. pair with TransitionMs for fades. declare the resting BackgroundColor and its variant together on the same blob.

  9. compose controls. a button is a Container with OnClick and a hover variant; Goo.Controls covers 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.

  10. never hold a Container across rebuilds. its children list comes from a per-build pool. extract the function that builds it; never cache the blob in a field or static readonly. Text and Image are pool-free and safe to cache.

  11. 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.AddRange auto-keys loops for you.

  12. Position = Absolute resolves against the nearest positioned ancestor. give the intended parent Position = PositionMode.Relative. the inverse trap: sibling shapes meant to overlap must each be Position = Absolute, Top = 0, Left = 0 or flex lays them side by side (shapes).

  13. 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 a Transform instead;
    • 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 ShaderEffect uniform accepts a literal or a per-frame Func<T> (["LightPos"] = (Func<Vector2>)(() => Mouse.Position / Screen.Size)), bypassing the reconciler entirely.
  14. animation state is a damper field, advanced in Tick, sampled in Build. SpringFloat _pulse = new(1f, 4f, 0.5f); then _pulse.Tick(dt) in Tick, set .Target (or kick .Velocity) on input, read .Current in Build. easing names are exactly: Linear, Ease, EaseIn, EaseOut, EaseInOut, ExpoIn, ExpoOut, ExpoInOut, BounceIn, BounceOut, BounceInOut, SineIn, SineOut, SineInOut, StepStart, StepEnd.

  15. do not declare PointerEvents defensively. goo auto-gates: handlers (or TextEntry/WebPanel) resolve to All, state variants force All, everything else resolves to None. the one place you must declare it: PointerEvents.All on a scroll viewport, which otherwise looks inert. a scroll container needs all three of: a bounded size on the scroll axis, OverflowY = OverflowMode.Scroll (not Auto, not Hidden), and PointerEvents = PointerEvents.All.

  16. SwallowClick = true stops a click from bubbling. use it on content drawn over a dismiss-on-click scrim. an empty OnClick = e => { } does NOT stop propagation.

  17. 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's Code/Core/ are the property tables (grep *.Style.g.cs from the project root). if it is not there, it does not exist - say so instead of guessing.

  18. two C# name traps. PanelTransform is ambiguous between Goo and Sandbox.UI when both are imported: add using PanelTransform = Goo.PanelTransform;. inside any component class, a bare Components resolves to the engine's ComponentList property, so alias static component classes (using Kit = Goo.Components.Components;). do not alias anything to UI - it collides with the Sandbox.UI namespace from inside namespace 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

if anything in this primer contradicts the code, the code wins. file a bug.