gotchas¶
common surprises and their fixes.
sizing the root in embedded hosts¶
under a full-screen ScreenPanel, sizing your build root is enough: Width = Length.Percent(100), Height = Length.Percent(100) on the root container fills the screen, and Hud.Overlay() is the ready-made root for layered huds. no OnEnabled override needed.
the gotcha lives in embedded and partial-screen hosts (GooView inside an existing razor panel, a WorldPanel, any content-sized host): there, percent sizing resolves against whatever size the host actually has, which may be undefined. the fallback is to style the engine host panel directly:
protected override void OnEnabled()
{
base.OnEnabled(); // keeps goo's per-enable bookkeeping running
Panel.Style.Width = Length.Percent(100);
Panel.Style.Height = Length.Percent(100);
}
goo doesn't size the host panel for you because filling the host is one valid choice and a content-sized tooltip is another. picking either as a forced default makes the other harder.
new Text { ... } compiles but throws at runtime¶
Text.Content is a normal init property, so new Text { Content = "Hello", FontSize = 16f } compiles. it also throws a NullReferenceException at runtime: object-initializer syntax invokes the implicit struct default constructor, which leaves the internal style list null, and the first style init-setter trips over it.
always construct Text through its explicit constructor; it seeds the style list:
new Text("Hello") { FontSize = 16f } // correct
new Text { Content = "Hello", FontSize = 16f } // compiles, NPEs at runtime
GooPanel or GooView?¶
GooPanel<T> is a PanelComponent: subclass it and drop the component on a GameObject under a ScreenPanel or WorldPanel. it owns its whole panel and is the default mount for a standalone goo ui.
GooView is a razor panel element: use it to embed a goo subtree inside an existing .razor file, when the surrounding ui is already razor and only one region should be goo.
owning the screen -> GooPanel<T>. renting a corner of someone else's markup -> GooView.
container can't be a static field or held across rebuilds¶
Container rents pooled storage when you construct it, and the pool is only alive during a Build() call. two things to avoid:
// breaks at static init.
private static readonly Container Card = new Container { ... };
// breaks on the second build.
private Container _root;
protected override Container Build()
{
_root ??= new Container { ... };
return _root;
}
both throw InvalidOperationException, and the message says the same thing this section does: build the container inside Build(), or in a helper called from it.
the reusable unit in goo is the function that builds a container, not the container itself. same model as react: you don't keep <div> literals in fields, you keep the function that returns one.
public class MyHud : GooPanel<Container>
{
protected override Container Build() => new Container
{
Card("hello"),
Card("world"),
};
static Container Card(string text) => new Container
{
Padding = 12,
BackgroundColor = Color.White,
new Text(text),
};
}
helpers can live in other files, other classes, anywhere. as long as they're called transitively from Build(), the pool is active and everything works.
GooView Build= needs new BlobBuilder(...) in markup¶
embedding a goo subtree in a .razor panel uses the <GooView> tag, but its Build attribute does not bind from a bare method group:
<GooView Build=@MyBuild /> <!-- does NOT compile (CS0029) -->
<GooView Build=@(new BlobBuilder(MyBuild)) /> <!-- the canonical idiom -->
Build is a BlobBuilder (delegate IBlob BlobBuilder()), and razor binds an attribute expression by its natural type. the method group IBlob MyBuild() has natural type Func<IBlob> - but Func<IBlob> is itself illegal: IBlob carries a static abstract member (Kind), and C# bars interfaces with static-abstract members as generic type arguments (CS8920). razor can therefore never form the delegate from @MyBuild, so you wrap the method in new BlobBuilder(...) explicitly.
the sibling OnTick attribute binds cleanly from @Animate because its type is Func<float, bool>, an ordinary generic razor can construct. only Build needs the wrapper, and only because of IBlob's static-abstract Kind.
ZIndex wins within its parent, and only there¶
the engine paints siblings in ascending SiblingIndex + ZIndex order, per parent. goo scales your declared ZIndex so it dominates that arithmetic: any ZIndex = 1 paints above every undeclared sibling no matter how many there are, distinct declared values keep their declared order, equal declared values resolve by document order, and negative values paint behind. paint order and hit-testing agree, because both use the same per-parent key.
what ZIndex cannot do is cross a parent boundary. there are no CSS stacking contexts in the engine: a deep child's ZIndex never lifts it above anything outside its own parent. to beat a panel elsewhere in the tree, put the ZIndex on the ancestor that is the direct sibling of what it must out-paint:
new Container
{
Children =
{
new Container { ZIndex = 1, Children = { Marker() } }, // lifts above the overlay
Overlay(),
},
},
one cosmetic residual: the editor inspector shows the scaled engine value (4096 for a declared 1). the declared value in your code is the source of truth.
see also¶
- build method - why the reusable unit is the function, not the container
- styles - the extract pattern in depth
- overview - the model behind the tree and its rebuilds
- overlay layout -
Hud.Overlay()and sizing the host panel for a full-screen root - your first panel -
GooPanel<T>, the standard root for a standalone goo UI - events - paint order and hit-testing share the same per-parent key