Interface: a first menu and UI structure
A complete responsive C# menu: Control hierarchy, shared styling, buttons and size checks.
Choose the building blocks
Control is an interface entity with a rectangle, dimensions, anchors and focus. Label displays text, Button invokes an action and LineEdit accepts one line. Containers distribute space between child Controls. These are part of the regular Entity hierarchy; UI does not introduce a separate ECS model.
| Task | Guide |
|---|---|
| First menu screen | Complete example below |
| Edge-aligned HUD and window resizing | Sizes and anchors |
| Lists, free space and scrolling | Containers |
| Tab, arrows, confirmation and return | Focus and navigation |
| Choose an input, list or toggle | Interface controls |
| Custom drawing and input behavior | A custom Control |
Run the menu
In a C# project, create a level with a Control root, save UiSampleStyle.cs and ResponsiveMenu.cs, attach ResponsiveMenu to the root and build. Run with F6. See the first project if C# setup is needed.
Start increments a counter, Enter activates the selected button and Tab moves focus. The settings button identifies where to connect your screen; it does not implement game settings. Resizing keeps the card centered and at most 520 logical pixels wide, with 24-pixel outer margins. The example targets an area of at least 390 × 480; an arbitrarily small window cannot fit arbitrary text.
UiSampleStyle is also required by the next examples: it supplies shared colors, font size and spacing. A game can keep its shared theme in a resource assigned to the UI root. See the theme guide for inheritance and per-type styling.
UiSampleStyle.cs
using System;
using Reckfell;
public static class UiSampleStyle
{
public static string Text(string ru, string en) =>
TranslationServer.GetLocale().StartsWith("ru", StringComparison.OrdinalIgnoreCase) ? ru : en;
public static Theme Create()
{
var theme = new Theme { DefaultFontSize = 16 };
theme.SetColor("font_color", "Label", Colors.White);
theme.SetConstant("separation", "VBoxContainer", 12);
theme.SetConstant("separation", "HBoxContainer", 12);
foreach (string state in new[] { "normal", "hover", "pressed", "disabled" })
{
var style = Box(state == "pressed" ? new Color(.12f, .27f, .75f) : new Color(.14f, .14f, .16f), 12);
theme.SetStylebox(state, "Button", style);
}
var focus = Box(Colors.Transparent, 0);
focus.DrawCenter = false; focus.BorderColor = new Color(.25f, .45f, 1);
focus.SetBorderWidthAll(2); theme.SetStylebox("focus", "Button", focus);
theme.SetStylebox("panel", "PanelContainer", Box(new Color(.065f, .065f, .075f), 20));
return theme;
}
private static StyleBoxFlat Box(Color color, int padding)
{
var style = new StyleBoxFlat { BgColor = color };
style.SetCornerRadiusAll(6);
foreach (Side side in Enum.GetValues<Side>()) style.SetContentMargin(side, padding);
return style;
}
public static void Prepare(Control root)
{
root.SetAnchorsAndOffsetsPreset(Control.LayoutPreset.FullRect);
root.Theme = Create();
var background = new ColorRect { Color = new Color(.02f, .02f, .025f), MouseFilter = Control.MouseFilterEnum.Ignore };
root.AddChild(background); background.SetAnchorsAndOffsetsPreset(Control.LayoutPreset.FullRect);
}
}ResponsiveMenu.cs
using Reckfell;
public partial class ResponsiveMenu : Control
{
public PanelContainer Card { get; private set; } = null!;
public Button Start { get; private set; } = null!;
public Label Status { get; private set; } = null!;
public int StartCount { get; private set; }
public override void _Ready()
{
UiSampleStyle.Prepare(this);
Card = new PanelContainer { Name = "Card" }; AddChild(Card);
var column = new VBoxContainer(); Card.AddChild(column);
column.AddChild(new Label { Text = "Reckfell" });
column.AddChild(new Label
{
Text = UiSampleStyle.Text("Начните игру или настройте управление.", "Start a game or adjust the controls."),
AutowrapMode = TextServer.AutowrapMode.WordSmart
});
Start = new Button { Name = "Start", Text = UiSampleStyle.Text("Начать", "Start"), FocusMode = FocusModeEnum.All };
column.AddChild(Start);
var settings = new Button { Text = UiSampleStyle.Text("Настройки управления", "Control settings") }; column.AddChild(settings);
Status = new Label { Text = UiSampleStyle.Text("Готово", "Ready") }; column.AddChild(Status);
Start.Pressed += () => { StartCount++; Status.Text = UiSampleStyle.Text("Нажатий: ", "Presses: ") + StartCount; };
settings.Pressed += () => Status.Text = UiSampleStyle.Text("Здесь откройте свои настройки.", "Open your settings here.");
Resized += LayoutCard;
Card.MinimumSizeChanged += () => Callable.From(LayoutCard).CallDeferred();
LayoutCard(); Callable.From(() => Start.GrabFocus()).CallDeferred();
}
private void LayoutCard()
{
if (Card is null || !IsInsideTree()) return;
float width = Mathf.Min(520, Mathf.Max(0, Size.X - 48));
Card.Size = new Vector2(width, Card.GetCombinedMinimumSize().Y);
Card.Position = (Size - Card.Size) / 2;
}
public override void _UnhandledInput(InputEvent @event)
{
if (IsVisibleInTree() && @event.IsAction("ui_accept")) GetViewport().SetInputAsHandled();
}
}Assign layout ownership
Card has a plain Control parent, so LayoutCard sets the card position and size. Inside it, PanelContainer and VBoxContainer lay out content. Do not manually position buttons in that column: the next container pass will replace those positions.
MinimumSizeChanged can arrive during child-size recalculation. The example defers card placement to avoid recursively performing the whole layout chain inside the signal. Text wrapping is explicitly enabled through AutowrapMode; resizing alone does not enable wrapping for every text control.
For a HUD above a moving world, usually put the Control root under CanvasLayer so the world camera does not move the screen interface. A Control's size follows its logical parent/viewport area, not necessarily physical monitor pixels. Establish correct layout first, then configure project scaling for resolutions and DPI separately.
If the screen is empty or the card is clipped, first inspect the root Control Size, GetParentAreaSize and the card’s GetCombinedMinimumSize. Compare dimensions in the same logical coordinate system. A long label’s large minimum does not fit automatically just because its parent uses FullRect.
Check before connecting gameplay
Resize between wide and narrow formats, lengthen labels, navigate without a mouse and return after hiding the menu. Keep gameplay actions separate from ui_*. _Input() runs before GUI; polling Input.IsActionJustPressed() is not canceled by AcceptEvent either. Gate gameplay reactions through game/menu state and receive unhandled events in _UnhandledInput().