Editor/Tool/Subtools/ArchNumberEdit.cs
using System;
using Editor;
using Sandbox;
namespace Sunless.Architecture;
// Values step on the field's SNAP, not freely; typing still works. The drag is RATCHETED from where it was
// grabbed: a value already off the ladder is put back onto it by the first step, so 41 on a 5 degree ladder
// pulls up to 45 and down to 40 rather than losing the remainder to a rounding the instant the box is touched.
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 );
// Free of the snap, so a field can ask for several pixels PER step: pinned to the step, one pixel was
// one step and a 15 degree ladder flicked through a whole turn before the mouse had crossed the box.
this.perPixel = MathF.Max( 0.001f, perPixel );
this.format = format;
this.pulled = pulled;
ToolTip = $"Drag sideways to pull the value in steps of {this.snap:0.##}.";
}
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 = Ratcheted( travelled * perPixel );
Text = value.ToString( format );
pulled?.Invoke( value );
}
// Whole steps off the anchor, with the first one landing on the next rung in the direction of travel. An
// anchor already on the ladder has no remainder to give up, so it simply counts.
float Ratcheted( float travelled ) {
var steps = (int)(travelled / snap);
if ( steps == 0 ) {
return anchor;
}
if ( MathF.Abs( ArchGridService.Snap( anchor, snap ) - anchor ) < 0.001f ) {
return anchor + steps * snap;
}
var rung = (steps > 0 ? MathF.Ceiling( anchor / snap ) : MathF.Floor( anchor / snap )) * snap;
return rung + (steps - MathF.Sign( steps )) * snap;
}
protected override void OnMouseReleased( MouseEvent e ) {
base.OnMouseReleased( e );
dragging = false;
Cursor = CursorShape.Arrow;
}
}