Editor/EffigyEditor/EffigySketchGridBar.cs
using Editor;
using System;
namespace Marionette.EditorTools;
/// <summary>
/// The grid switch and its spacing, floating in the corner of the viewport while a sketch is open.
///
/// WHY IT IS NOT ONLY IN SETTINGS. Both controls already live in Edit > Settings, and that is the
/// right home for them when you are setting up how the tool behaves. It is the wrong home for them
/// while you are drawing: the grid is the paper, changing paper is part of drawing, and a dialog
/// three menus away is not somewhere you go mid-sketch. Every sketcher worth using puts these two
/// on the canvas.
///
/// NOT A SECOND COPY OF THE VALUE. This reads and writes the viewport's own properties - the same
/// two the settings window sets - and re-reads them every time it is shown, so the two controls can
/// never disagree about what is true. The spacing list itself is EffigySettingsWindow's, for the
/// same reason: two lists drift, and a value that exists on one dropdown and not the other reads as
/// the setting having been lost.
///
/// AT THE END OF THE TOOL ROW, not floating on the model. It belongs to the mode you are in, the
/// way the Line and Rectangle buttons beside it do, and chrome is where a mode's controls go - a
/// panel sitting on the part is one more thing between you and the thing you are drawing. Flush
/// right, so the tools growing and shrinking as you change stage never move it.
/// </summary>
internal sealed class EffigySketchGridBar : Widget
{
/// <summary>The tool buttons' own height, so the two read as one row of controls rather than
/// as a panel parked next to them.</summary>
public const float BarHeight = EffigyToolChrome.ButtonHeight;
public const float BarWidth = 210f;
private readonly Button _toggle;
private readonly ComboBox _spacing;
/// <summary>Set while Refresh writes the controls. Both of them fire their change callbacks on
/// assignment exactly as a click does, and without this a refresh would turn straight round and
/// write the value it had only just read - harmless for the toggle, and for the dropdown a way
/// to reset the spacing to whatever happened to be first.</summary>
private bool _syncing;
/// <summary>Raised after either control has changed the viewport, so the window can save the
/// setting the same way the settings dialog's own callback does. The viewport is already
/// updated by the time this fires.</summary>
public Action Changed { get; set; }
private readonly EffigyViewport _viewport;
public EffigySketchGridBar( Widget parent, EffigyViewport viewport ) : base( parent )
{
_viewport = viewport;
// The same two flags every floating widget in this tool sets — a plain Widget paints the
// system background, which is a white slab on the 3D view.
TranslucentBackground = true;
NoSystemBackground = true;
Visible = false;
FixedHeight = BarHeight;
FixedWidth = BarWidth;
Layout = Layout.Row();
Layout.Spacing = 6;
Layout.Margin = new Sandbox.UI.Margin( 6, 2, 6, 2 );
_toggle = new Button( "Grid", "grid_on", this )
{
ToolTip = "Draw the grid on the face you are sketching on. The lines are the intervals "
+ "the cursor snaps to.",
FixedWidth = 74f,
};
_toggle.Clicked = ToggleGrid;
Layout.Add( _toggle );
_spacing = new ComboBox( this )
{
ToolTip = "How far apart the grid lines sit, in sketch units. Automatic fits the step "
+ "to the face you are on and to how close the camera is.",
};
foreach ( var value in EffigySettingsWindow.Spacings )
{
var step = value;
_spacing.AddItem( EffigySettingsWindow.Describe( step ), onSelected: () => SetSpacing( step ) );
}
Layout.Add( _spacing, 1 );
Refresh();
}
/// <summary>The tool row's background, so the gaps between the two controls disappear into the
/// bar rather than showing as a panel laid on it. A widget cannot simply decline to paint: a
/// rect it leaves alone keeps whatever the previous frame put there.</summary>
public Color GapColor { get; set; } = Theme.ControlBackground;
protected override void OnPaint()
{
Paint.ClearPen();
Paint.SetBrush( GapColor );
Paint.DrawRect( LocalRect );
}
/// <summary>Point both controls at what the viewport actually holds. Called on every show, and
/// again whenever the settings window has been the one to change it.</summary>
public void Refresh()
{
if ( !_viewport.IsValid )
return;
_syncing = true;
try
{
var on = _viewport.ShowPlaneGrid;
_toggle.Icon = on ? "grid_on" : "grid_off";
// Tinted rather than labelled on and off. The button says "Grid" either way, so the
// only thing that has to change is whether it reads as engaged - and a label that
// alternates between two words is a control you have to read to use.
_toggle.Tint = on ? Theme.Blue : Theme.TextControl.WithAlpha( 0.5f );
var spacing = _viewport.GridSpacing;
var index = 0;
for ( var i = 0; i < EffigySettingsWindow.Spacings.Length; i++ )
{
if ( MathF.Abs( EffigySettingsWindow.Spacings[i] - spacing ) < 0.0001f )
{
index = i;
break;
}
}
_spacing.CurrentIndex = index;
}
finally
{
_syncing = false;
}
}
private void ToggleGrid()
{
if ( _syncing || !_viewport.IsValid )
return;
_viewport.ShowPlaneGrid = !_viewport.ShowPlaneGrid;
Refresh();
Changed?.Invoke();
}
private void SetSpacing( float step )
{
if ( _syncing || !_viewport.IsValid )
return;
_viewport.GridSpacing = step;
// Choosing a spacing while the grid is off is asking to see it. Turning it on is what was
// meant, and leaving it off would make the dropdown look broken.
if ( !_viewport.ShowPlaneGrid )
_viewport.ShowPlaneGrid = true;
Refresh();
Changed?.Invoke();
}
}