Editor/Tool/Subtools/ArchNumberEdit.cs

An editor UI control that extends LineEdit to allow numeric text input with drag-to-adjust behavior snapping to a grid. It tracks mouse press/move/release, computes a value from horizontal drag distance using per-pixel scaling and snap size, updates the displayed text with a format string, and calls a callback with the new value.

Native Interop
using System;
using Editor;
using Sandbox;

namespace Sunless.Architecture;

// Values step on the field's SNAP, not freely; typing still works.
public sealed class ArchNumberEdit : LineEdit
{
	const float Threshold = 3f;

	readonly float snap;
	readonly float perPixel;
	readonly string format;
	readonly Action<float> pulled;
	Vector2 pressed;
	float anchor;
	bool dragging;

	public ArchNumberEdit( string text, float snap, float perPixel, string format, Action<float> pulled ) : base( text )
	{
		this.snap = MathF.Max( 0.001f, snap );
		this.perPixel = MathF.Max( this.snap, perPixel );
		this.format = format;
		this.pulled = pulled;

		ToolTip = "Drag sideways to pull the value along the grid.";
	}

	protected override void OnMousePress( MouseEvent e )
	{
		base.OnMousePress( e );

		pressed = e.LocalPosition;
		anchor = float.TryParse( Text, out var parsed ) ? parsed : 0f;
		dragging = false;
	}

	protected override void OnMouseMove( MouseEvent e )
	{
		if ( (e.ButtonState & MouseButtons.Left) == 0 )
		{
			base.OnMouseMove( e );
			return;
		}

		var travelled = e.LocalPosition.x - pressed.x;

		// Below the threshold it's still a click, so caret and selection stay with the base.
		if ( !dragging && MathF.Abs( travelled ) < Threshold )
		{
			base.OnMouseMove( e );
			return;
		}

		dragging = true;
		Cursor = CursorShape.SizeAll;

		var value = ArchGridService.Snap( anchor + travelled * perPixel, snap );

		Text = value.ToString( format );
		pulled?.Invoke( value );
	}

	protected override void OnMouseReleased( MouseEvent e )
	{
		base.OnMouseReleased( e );

		dragging = false;
		Cursor = CursorShape.Arrow;
	}
}