A custom Control: drawing, mouse and keyboard
A complete interactive meter: minimum size, dragging, focus, value bounds and a signal.
When a custom element is useful
Use HSlider for an ordinary slider: it already provides behavior and settings. A custom Control is useful for a different presentation or interaction. This example is a minimal working base with a 0…1 value, bar drawing, mouse/keyboard input and a signal. It does not promise every built-in Slider or accessibility feature.
Add UiSampleStyle.cs, MeterControl.cs and MeterDemo.cs. Attach MeterDemo to a Control root, build and run with F6. Click/drag sets the value and arrow keys adjust it by 0.05. A white border marks focus; a label shows the percentage. Resizing makes the bar use its new width.
MeterControl.cs
using Reckfell;
public partial class MeterControl : Control
{
[Signal] public delegate void ValueChangedEventHandler(float value);
private float _value = .25f;
private bool _dragging;
[Export(PropertyHint.Range, "0,1,0.01")]
public float Value
{
get => _value;
set
{
if (!float.IsFinite(value)) return;
float next = Mathf.Clamp(value, 0, 1);
if (Mathf.IsEqualApprox(next, _value)) return;
_value = next; QueueRedraw(); EmitSignal(SignalName.ValueChanged, next);
}
}
public override Vector2 _GetMinimumSize() => new(160, 40);
public override void _Ready()
{
FocusMode = FocusModeEnum.All; MouseFilter = MouseFilterEnum.Stop;
FocusEntered += QueueRedraw;
FocusExited += () => { _dragging = false; QueueRedraw(); };
Resized += QueueRedraw;
VisibilityChanged += () => { if (!IsVisibleInTree()) _dragging = false; };
}
public override void _Draw()
{
DrawRect(new Rect2(0, 10, Size.X, 20), new Color(.16f, .16f, .18f));
DrawRect(new Rect2(0, 10, Size.X * _value, 20), new Color(.15f, .35f, 1));
if (HasFocus()) DrawRect(new Rect2(Vector2.Zero, Size), Colors.White, false, 2);
}
public override void _GuiInput(InputEvent @event)
{
if (@event is InputEventMouseButton click && click.ButtonIndex == MouseButton.Left)
{
_dragging = click.Pressed;
if (click.Pressed) { GrabFocus(); Value = click.Position.X / Mathf.Max(1, Size.X); }
AcceptEvent();
}
else if (@event is InputEventMouseMotion motion && _dragging)
{
Value = motion.Position.X / Mathf.Max(1, Size.X); AcceptEvent();
}
else if (@event.IsActionPressed("ui_left", true)) { Value -= .05f; AcceptEvent(); }
else if (@event.IsActionPressed("ui_right", true)) { Value += .05f; AcceptEvent(); }
}
}MeterDemo.cs
using Reckfell;
public partial class MeterDemo : Control
{
public MeterControl Meter { get; private set; } = null!;
public Label Status { get; private set; } = null!;
public override void _Ready()
{
UiSampleStyle.Prepare(this);
var margin = new MarginContainer(); AddChild(margin); margin.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect, margin: 24);
var column = new VBoxContainer(); margin.AddChild(column);
column.AddChild(new Label { Text = UiSampleStyle.Text("Мышь или стрелки ← →", "Mouse or arrow keys") });
Meter = new MeterControl { Name = "Meter", SizeFlagsHorizontal = SizeFlags.ExpandFill }; column.AddChild(Meter);
Status = new Label(); column.AddChild(Status); UpdateLabel(Meter.Value);
Meter.ValueChanged += UpdateLabel;
Callable.From(() => Meter.GrabFocus()).CallDeferred();
}
private void UpdateLabel(float value) => Status.Text = $"{value:P0}";
}Separate dimensions, state and drawing
_GetMinimumSize() tells the container it needs 160 × 40; it does not assign position. _Draw() uses current Size. State changes call QueueRedraw; there is no need to recreate the entity or redraw from _Process each frame. If the minimum requirement itself changes, call UpdateMinimumSize separately.
Call DrawRect and other Draw* commands from _Draw or a Draw handler rather than directly from the value setter. QueueRedraw defers drawing and coalesces repeated requests; it does not call _Draw immediately. Minimum-size invalidation and drawing requests are separate operations.
Export Range helps the editor but does not validate code assignments. The setter rejects NaN/Infinity, clamps and avoids another signal when unchanged. ValueChanged reports the accepted state so the label agrees with the bar.
The painted bar occupies only part of the Control height, but the default hit region is the whole rectangle from Vector2.Zero to Size. Drawing outside it does not expand hit testing automatically. For custom geometry override _HasPoint(Vector2 point) using local coordinates; check minimum size, clipping and child placement separately.
Local coordinates and event handling
Mouse Position in _GuiInput() is already local to this Control; X divided by Size.X gives a relative fraction. Do not subtract GlobalPosition again. After a press, the viewport retains the mouse receiver until release, so the example can clamp while dragging past an edge. Release ends dragging; hiding or losing focus resets it too.
| MouseFilter | Behavior |
|---|---|
| Stop | Receives pointer input and stops ordinary upward propagation |
| Pass | Unhandled input may reach the parent; it does not click a sibling underneath |
| Ignore | Excludes the element from hit testing; suitable for decoration above a button |
Keyboard use requires FocusMode.All and an actual GrabFocus. The example handles ui_left/right in _GuiInput() and calls AcceptEvent so an arrow changes the value instead of moving focus or reaching _UnhandledInput(). AcceptEvent cannot cancel an earlier _Input() or change polled Input-action state.
Check press, hold, release outside, key repeat, hiding during drag and resizing. Touch, wheel, screen readers and controllers need their corresponding behavior implemented and checked separately.