Editor/Tool/ArchToolbar.cs

Editor toolbar widget for the architecture tool. It builds and updates UI controls for view arrangement, snapping, handle mode (move/rotate/scale), section depth, and visibility filters, and redraws when the tool state changes.

Native Interop
using System;
using System.Linq;
using Editor;
using Sandbox;

namespace Sunless.Architecture;

// Only what the editor does not already own: view axis, split and fullscreen are the scene view's, and the
// document's own actions are the sidebar's. What is here is what belongs to no part and no tool.
public sealed class ArchToolbar : Widget
{
	const float Glyph = 16f;
	const float Side = 24f;

	readonly ArchTool tool;

	ArchViewAxis trackedAxis = (ArchViewAxis)(-1);
	ArchHandleMode trackedHandle = (ArchHandleMode)(-1);
	int trackedLevel = int.MinValue;
	int trackedDrafts = int.MinValue;
	bool trackedWallSnap = true;

	public ArchToolbar( ArchTool tool ) : base( null )
	{
		this.tool = tool;

		Layout = Layout.Row();
		Layout.Spacing = 4;

		Rebuild();
	}

	// Axis and level change elsewhere too - CTRL+SPACE into an elevation, a storey stepped from a subtool.
	[EditorEvent.Frame]
	void OnFrame()
	{
		if ( tool is null )
		{
			return;
		}

		if ( tool.Axis == trackedAxis && tool.Level == trackedLevel && tool.Drafts.Count == trackedDrafts
			&& ArchShapeHandles.Mode == trackedHandle && tool.SnapsToWalls == trackedWallSnap )
		{
			return;
		}

		trackedAxis = tool.Axis;
		trackedHandle = ArchShapeHandles.Mode;
		trackedLevel = tool.Level;
		trackedDrafts = tool.Drafts.Count;
		trackedWallSnap = tool.SnapsToWalls;

		Rebuild();
	}

	void Rebuild()
	{
		Layout.Clear( true );

		BuildDrawingSet();
		BuildSnap();
		BuildHandles();
		BuildSection();
		BuildVisibility();

		Layout.AddStretchCell();
	}

	// Snapping is a way of working too, and it belongs beside the handle modes for the same reason.
	void BuildSnap()
	{
		var walls = new IconButton( "polyline" )
		{
			ToolTip = "Snap to standing walls — corners first, then faces, inside the reach beside this",
			IconSize = Glyph,
			FixedSize = Side,
			IsToggle = true,
			IsActive = tool.SnapsToWalls
		};

		walls.OnToggled = value => { tool.SnapsToWalls = value; Rebuild(); };

		Layout.Add( walls );

		if ( tool.SnapsToWalls )
		{
			var reach = new ArchNumberEdit( tool.WallSnapReach > 0.01f ? tool.WallSnapReach.ToString( "0.##" ) : "",
				ArchGridService.FinestSize, 1f, "0.##", value => tool.WallSnapReach = MathF.Max( 0f, value ) )
			{
				FixedWidth = 52f,
				PlaceholderText = ArchWallSnap.Reach( 0f ).ToString( "0.##" ),
				ToolTip = "How near a corner or face has to be before it takes the cursor. Blank is one subgrid step."
			};

			reach.TextEdited += text => tool.WallSnapReach = float.TryParse( text, out var parsed ) ? MathF.Max( 0f, parsed ) : 0f;

			Layout.Add( reach );
		}

		Separator();
	}

	// Which widget a selected shape wears. It is a WAY OF WORKING, not a property of the thing picked or of
	// the tool that made it, so it stands up here beside the view controls rather than in any part's form.
	void BuildHandles()
	{
		Handle( "Move — the engine's own three-axis mover", "open_with", ArchHandleMode.Move );
		Handle( "Rotate — the engine's own ring", "rotate_right", ArchHandleMode.Turn );
		Handle( "Scale — the six-faced box, footprint and rise in one widget", "aspect_ratio", ArchHandleMode.Scale );

		Separator();
	}

	void Handle( string tooltip, string icon, ArchHandleMode mode )
	{
		var button = new IconButton( icon )
		{
			ToolTip = tooltip,
			IconSize = Glyph,
			FixedSize = Side,
			IsToggle = true,
			IsActive = ArchShapeHandles.Mode == mode
		};

		// One at a time: a lit mover, ring and box together is three sets of arrows to aim between.
		button.OnToggled = _ => { ArchShapeHandles.Mode = mode; Rebuild(); };

		Layout.Add( button );
	}

	// Splitting a viewport is the engine's; aiming every pane at one house is not.
	void BuildDrawingSet()
	{
		Plain( "The drawing set — four panes: perspective, plan, front, side", "grid_view",
			() => tool.Arrange( SceneViewWidget.ViewportLayoutMode.Four,
				ArchViewAxis.Free, ArchViewAxis.Top, ArchViewAxis.Front, ArchViewAxis.Side ) );

		Plain( "Frame every ortho pane on the building being edited", "filter_center_focus", () => tool.Frame() );

		Separator();
	}

	// Only in an elevation - in the plan the depth is the storey, and that is the level stepper.
	void BuildSection()
	{
		if ( !tool.InElevation )
		{
			return;
		}

		Layout.Add( new Label( "Section" ) );

		var depth = new ArchNumberEdit( tool.Depth( tool.Axis ).ToString( "0.##" ), ArchGridService.FinestSize, 1f, "0.##",
			value => tool.SectionDepth = value )
		{
			FixedWidth = 64f
		};

		depth.TextEdited += text => tool.SectionDepth = float.TryParse( text, out var parsed ) ? parsed : null;

		Layout.Add( depth );

		Plain( "Back to the middle of the building being edited", "recenter", () => { tool.SectionDepth = null; Rebuild(); } );

		Separator();
	}

	// The storey filter belongs beside the hide flags, not among the dock's document actions.
	void BuildVisibility()
	{
		Hide( "Roofs", "roofing", tool.HideRoofs, value => tool.HideRoofs = value );
		Hide( "Walls", "square_foot", tool.HideWalls, value => tool.HideWalls = value );
		Hide( "Floors", "layers", tool.HideFloors, value => tool.HideFloors = value );
		Hide( "Footings", "foundation", tool.HideFoundation, value => tool.HideFoundation = value );
		Hide( "Trims", "timeline", tool.HideTrim, value => tool.HideTrim = value );

		var storey = new IconButton( "filter_alt" )
		{
			ToolTip = "Only this level",
			IconSize = Glyph,
			FixedSize = Side,
			IsToggle = true,
			IsActive = tool.OnlyCurrentLevel
		};

		storey.OnToggled = value => { tool.OnlyCurrentLevel = value; tool.ApplyVisibility(); };

		Layout.Add( storey );
	}

	void Plain( string tooltip, string icon, Action clicked )
	{
		var button = new IconButton( icon )
		{
			ToolTip = tooltip,
			IconSize = Glyph,
			FixedSize = Side
		};

		button.OnClick = () => clicked();

		Layout.Add( button );
	}

	// Latching, named for what it SHOWS - a lit button meaning "hidden" would read backwards.
	void Hide( string tooltip, string icon, bool hidden, Action<bool> assign )
	{
		var button = new IconButton( icon )
		{
			ToolTip = $"Show {tooltip}",
			IconSize = Glyph,
			FixedSize = Side,
			IsToggle = true,
			IsActive = !hidden
		};

		button.OnToggled = value => { assign( !value ); tool.ApplyVisibility(); };

		Layout.Add( button );
	}

	void Separator()
	{
		Layout.AddSpacingCell( 4f );
		Layout.Add( new ArchToolbarRule() );
		Layout.AddSpacingCell( 4f );
	}
}

file sealed class ArchToolbarRule : Widget
{
	public ArchToolbarRule() : base( null )
	{
		FixedWidth = 1;
		FixedHeight = 20;
	}

	protected override void OnPaint()
	{
		Paint.ClearPen();
		Paint.SetBrush( Color.White.WithAlpha( 0.2f ) );
		Paint.DrawRect( LocalRect );
	}
}