-
Notifications
You must be signed in to change notification settings - Fork 0
Getting Started
This guide walks you through creating a custom GUI screen from scratch.
Add these references to your .csproj. Gui.dll lives wherever you downloaded the
library; the rest ship with the game and resolve via the VINTAGE_STORY environment variable.
<Reference Include="Gui">
<HintPath>C:\path\to\Gui\bin\Release\Mods\mod\Gui.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="OpenTK.Mathematics">
<HintPath>$(VINTAGE_STORY)\Lib\OpenTK.Mathematics.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="SkiaSharp">
<HintPath>$(VINTAGE_STORY)\Lib\SkiaSharp.dll</HintPath>
<Private>false</Private>
</Reference>Subclass GuiBase, override Build() for the UI and CreateWindowConfig() for window
behavior:
using Gui;
using Gui.Widgets;
using OpenTK.Mathematics;
using Vintagestory.API.Client;
public class MyInventoryGui : GuiBase
{
public MyInventoryGui(ICoreClientAPI capi) : base(capi) { }
protected override WindowConfig CreateWindowConfig() => new()
{
Size = new Vector2(400, 300),
Draggable = true,
Resizable = true,
};
protected override Widget Build()
{
return new WindowFrame(
title: "My Inventory",
onClose: () => TryClose(),
child: new Padding(
EdgeInsets.All(16),
child: new Text("Inventory content here")
)
);
}
}Build() is called once when the GUI opens. It returns the immutable widget tree that
describes the UI. The Library turns this into an element tree and a render tree automatically.
CreateWindowConfig() defines the window's size, position, and drag/resize behavior. See
Windowing for all options.
// In your ModSystem or event handler:
var gui = new MyInventoryGui(capi);
gui.TryOpen();
// Close it programmatically:
gui.TryClose();Alternatively, register a hotkey:
public override void StartClientSide(ICoreClientAPI api)
{
api.Input.RegisterHotKey("myguiopen", "Open My GUI", GlKeys.M);
api.Input.SetHotKeyHandler("myguiopen", _ =>
{
new MyInventoryGui(api).TryOpen();
return true;
});
}Wrap the content in a StatefulWidget to hold mutable data:
public class MyInventoryGui : GuiBase
{
public MyInventoryGui(ICoreClientAPI capi) : base(capi)
{
}
protected override Widget Build() => new _MyContent();
class _MyContent : StatefulWidget
{
public override State CreateState() => new _MyContentState();
}
class _MyContentState : State<_MyContent>
{
TextEditingController _textController = null!;
bool _filterEnabled = false;
public override void InitState()
{
base.InitState();
_textController = new TextEditingController("");
}
public override Widget Build(BuildContext ctx)
{
return new Center(
child: new Container(
style: new BoxStyle { Width = 400, Height = 300, Padding = EdgeInsets.All(16) },
child: new Column(
spacing: 12,
children:
[
new Text("Search Items", new TextStyle { FontSize = 16 }),
new Row(children:
[
new Expanded(child: new TextField(controller: _textController)),
]
),
new Checkbox(
_filterEnabled,
v => SetState(() => _filterEnabled = v),
"Enable filter"
),
new Text($"Filter active: {_filterEnabled}",
new TextStyle { Color = new Vector4(0.7f, 0.7f, 0.7f, 1) }
)
]
)
)
);
}
public override void Dispose()
{
_textController.Dispose();
base.Dispose();
}
}
}SetState(fn) executes fn to mutate state fields, then schedules a rebuild of only this
subtree. The parent and siblings are untouched.
If your screen needs animations, create the AnimationController in InitState() and dispose
it in Dispose():
class _MyContentState : State<_MyContent>
{
AnimationController? _fadeCtrl;
CurvedAnimation? _fadeAnim;
public override void InitState()
{
base.InitState();
_fadeCtrl = new AnimationController(
TimeSpan.FromMilliseconds(400),
Element.Owner.GetTickerProvider() // ticker is tied to the GUI's frame loop
);
_fadeAnim = new CurvedAnimation(_fadeCtrl, Curves.EaseOut);
_fadeCtrl.Forward(); // start playing immediately
}
public override Widget Build(BuildContext ctx)
{
return new AnimatedBuilder(
animation: _fadeCtrl!,
builder: c => new Opacity(
(float)_fadeAnim!.Value,
child: new Text("Fades in on open")
)
);
}
public override void Dispose()
{
_fadeCtrl?.Dispose(); // stops the ticker and frees resources
base.Dispose();
}
}GuiBase.OnGuiClosed() calls RootElement.Unmount() automatically, which recursively calls
State.Dispose() on every stateful widget. You only need to manually dispose resources that
live inside State, such as AnimationController.
using System;
using Gui;
using Gui.Widgets;
using Gui.Rendering.Text;
using OpenTK.Mathematics;
using Vintagestory.API.Client;
public class HelloWorldGui : GuiBase
{
public HelloWorldGui(ICoreClientAPI capi) : base(capi) { }
protected override WindowConfig CreateWindowConfig() => new()
{
Size = new Vector2(300, 180),
Draggable = true,
Resizable = true,
};
protected override Widget Build() => new WindowFrame(
title: "Hello World",
onClose: () => TryClose(),
child: new _Content()
);
class _Content : StatefulWidget
{
public override State CreateState() => new _State();
}
class _State : State<_Content>
{
int _clicks = 0;
public override Widget Build(BuildContext ctx) =>
new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.Center,
spacing: 12,
children:
[
new Text($"Clicked {_clicks} times",
new TextStyle { FontSize = 18, Color = Vector4.One }),
new GestureDetector(
onTap: _ => SetState(() => _clicks++),
child: new Container(
style: new BoxStyle
{
Color = new Vector4(0.2f, 0.5f, 0.9f, 1),
CornerRadius = new Vector4(6),
Padding = EdgeInsets.Symmetric(vertical: 8, horizontal: 20)
},
child: new Text("Click me", new TextStyle { FontSize = 14 })
)
)
]
)
);
}
}See Architecture for a deep dive.