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.
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 );
}
}