Screens and elements
A mod's UI is built from elements - panels, labels, buttons, sliders, your own - placed on screens. The loader updates, draws and routes the mouse to them every frame they're shown, in the game's look.
Screens
context.UI |
Shown |
|---|---|
MainMenu |
On the main menu. |
InGame |
While a game is played. |
Always |
Everywhere: the main menu, the game, its loading and menus. |
Hud |
While a game is played, with the game's HUD: see Layers. |
When(() => condition, layer) |
While your condition says so (asked each frame), on a layer (default UILayer.Gui). |
var panel = context.UI.MainMenu.Add(new ExamplePanel());
var notes = context.UI.InGame.Add(new NotesPanel());
context.UI.MainMenu.Hidden += () => panel.Visible = false;
A screen out of its context isn't drawn, updated or clicked, and as it leaves nothing stays held: the mouse lets go, an open dropdown closes, a text box lets go of the keyboard. Its elements keep their own Visible for when it's back. A screen has IsActive, Shown and Hidden events, and MouseOverUI.
While the mouse is over mod UI, the game doesn't get the click (a click on a panel doesn't walk or attack), and while a text box has the keyboard, the game's hotkeys stay quiet.
Layers
Mod UI is drawn on one of two layers:
UILayer |
Drawn | Use it for |
|---|---|---|
Gui (default) |
Over everything: the world, the game's HUD and its windows. | Panels and windows of your own. |
Hud |
With the game's HUD: over the world, under its windows (inventory, map, dialogue, the Esc menu) and its bottom panel; hidden when its HUD is (UI turned off, a cutscene). | What belongs with the HUD: frames, markers, bars. |
var bar = context.UI.Hud.Add(new UIProgressBar(8, 40, 0, max: 100));
var screen = context.UI.When(() => Gm.InGame && Player.InCombat, UILayer.Hud);
context.DrawHud += () => Draw.Text(8, 60, "With the HUD", Draw.White);
A HUD element only has the mouse where none of the game's UI drawn over it is under the mouse: a click on a game window over it is the window's. context.DrawHud draws on the HUD layer as context.DrawGui draws over everything. Mod windows (UIWindow) still open over everything.
Elements
public class ExamplePanel : UIPanel
{
private readonly UILabel _clicks;
private int _count;
public ExamplePanel() : base(6, 0, 200, 120, UIAnchor.Left)
{
Add(new UILabel("Example", 10, 10));
_clicks = Add(new UILabel("", 10, 30, Draw.Muted));
var button = Add(new UIButton("Click me", 10, 0, 85) { Anchor = UIAnchor.BottomLeft, Y = 10 });
button.Clicked += _ => _count++;
Add(new UIButton("Close", 10, 0, 85, onClick: () => Visible = false) { Anchor = UIAnchor.BottomRight, Y = 10 });
}
protected override void OnUpdate(double deltaTime) => _clicks.Text = $"Clicks: {_count}";
}
Every element (UIElement) has:
X, Y, Anchor |
Where it is: an offset from its anchor point in its parent (UIAnchor.TopLeft, Top, TopRight, Left, Center, Right, BottomLeft, Bottom, BottomRight). |
Width, Height |
Its size, in the UI's units (the game's UI scale: see Drawing). |
Visible, Enabled, HitTest |
Hidden (with its children); drawn but not clicked; see-through to the mouse. |
Tooltip |
Shown in the game's hover frame when the mouse rests on it. |
Add(child), Remove, Clear, Children, Parent |
Its children, drawn over it and placed in it. Add returns the child. |
Clicked, IsHovered, IsPressed |
The mouse. |
ClipChildren |
Cut its children to its area (for scrolling). |
ScreenX, ScreenY, Contains, IsShown |
Where it is on screen; whether it's shown. |
Built-in elements
| Element | |
|---|---|
UIPanel(x, y, w, h, anchor) |
The game's menu background with a border; Framed for the hover-window frame. |
UIGroup |
A plain container that draws nothing. |
UILabel(text, x, y, colour) |
Text as the game draws it; Wrap wraps it at its Width, Align aligns it. |
UIImage(sprite, x, y, scale) |
A sprite, such as one from context.LoadSprite. |
UIButton(text, x, y, w, h, onClick) |
The game's button (100x26 is its own size). |
UICheckbox(text, x, y, checked) |
The game's checkbox; Changed gives the new state. |
UIHeader(text) |
A section header, as the Settings pages have. |
Controls
var health = Add(new UISlider(70, 31, value: 0.75, width: 120) { Tooltip = "Drag it, click the bar or use the wheel." });
var bar = Add(new UIProgressBar(12, 74, health.Value, style: UIBarStyle.Health, width: 196));
health.Changed += value => bar.Value = value;
var difficulty = Add(new UIDropdown(new[] { "Easy", "Normal", "Hard" }, 70, 102, 140, selected: 1));
difficulty.Changed += index => Say($"Difficulty: {difficulty.Selected}");
var name = Add(new UITextBox(70, 124, 140, placeholder: "Type a name...") { MaxLength = 24 });
name.Submitted += text => Say($"Hello, {text}!");
var list = Add(new UIScrollList(12, 148, 196, 104));
for (int row = 1; row <= 20; row++)
list.Add(new UIButton($"Row {row}", 0, 0, height: 14));
| Control | |
|---|---|
UISlider |
The game's volume slider: Min, Max, Step, Value, Format, Changed. |
UIProgressBar |
The game's health (UIBarStyle.Health) or energy (Energy) bar: Value, Max, Text, Smooth. |
UIDropdown |
The Settings menu's combobox: Options, SelectedIndex, Selected, MaxVisibleRows, Changed. Its list draws over everything. |
UITextBox |
The game's text input: Text, Placeholder, MaxLength, TextChanged, Submitted, Focus(), Blur(). Multiline wraps the text over several lines: Enter starts a new line, and the arrow keys move between them. |
UIScrollList |
Rows scrolled one at a time, with the game's scrollbar. |
UIScrollArea |
A smoothly scrolled column, as the Settings page: AddHeader, AddText, AddImage, AddCheckbox, ScrollTo. |
UIButtonRow |
A row of buttons, spread or packed, or at set Positions. |
UITabStrip |
A row or column of tabs (see Windows). |
Your own elements
Inherit UIElement (or any element) and override what you need:
using System;
using System.Collections.Generic;
using System.Linq;
using StoneForge;
public class FpsGraph : UIElement
{
private readonly Queue<double> _frames = new();
protected override void OnUpdate(double deltaTime)
{
_frames.Enqueue(deltaTime);
while (_frames.Count > 100)
_frames.Dequeue();
}
protected override void OnDraw(double x, double y)
{
Draw.Rectangle(x, y, x + Width - 1, y + Height - 1, Draw.Rgb(18, 16, 26));
double average = _frames.Count / Math.Max(_frames.Sum(), 0.001);
Draw.Text(x + 4, y + 3, $"{average:0} fps", IsHovered ? Draw.White : Draw.Muted);
}
}
| Override | |
|---|---|
OnUpdate(deltaTime) |
Every frame, before drawing. |
OnDraw(x, y) |
Draw it, its top-left corner at (x, y) on screen; its children are drawn after. |
OnDrawAfter(x, y) |
Draw over its children. |
OnClick, OnPress, OnMouseEnter, OnMouseLeave |
The mouse. |
OnWheel(delta) |
The wheel over it; return true if used, or it goes to the parent. |
ClipArea |
Where its children are cut when ClipChildren is set. |