Editor sidebar UI for the Advanced Mapping Tools package. Builds a property sheet from [Property] attributes grouped into foldable sections, with per-row reset buttons, ShowIf/HideIf visibility, a search and changed-only filter, a simple/full view toggle, tool header with help box, and a notice widget. Fold and view state is stored in editor cookies.
namespace AdvancedMappingTools;
/// <summary>
/// Fills a sidebar with the [Property] values of an object: one foldable section per [Group],
/// one compact row per value, name on the left and control on the right. A changed value has a
/// dot and a button that puts it back to its default. Values that [ShowIf]/[HideIf] rule out are
/// hidden as long as they do not apply. The groups of the tool itself come first, the ones every
/// builder shares come last in a fixed order, so a tool opens on what makes it different.
/// </summary>
public static class PropertySheet
{
/// <summary>
/// Groups every builder has, in the order they close the sidebar.
/// </summary>
// Properties, not fields: a hotload keeps the old contents of a static field.
static string[] Shared => ["Variation", "Prop", "Placing", "Materials", "Curves", "Placement"];
public const string AdvancedTitle = "Advanced";
/// <summary>
/// Groups the simple view leaves out: how points snap and curve, random turns and spread.
/// The tool's own groups, size and materials stay.
/// </summary>
static string[] Expert => ["Variation", "Placing", "Curves", "Placement"];
/// <summary>
/// A clearer name in the sidebar for a shared group.
/// </summary>
static Dictionary<string, string> Titles => new() { ["Placement"] = "Placement & Snapping", ["Prop"] = "Size & Wear" };
/// <summary>
/// Adds the sections and returns the sheet, so a <see cref="SettingsFilter"/> can search it.
/// </summary>
public static Sheet Fill( ToolSidebarWidget sidebar, SerializedObject settings, string defaultGroup, IReadOnlySet<string> skip = null ) =>
Fill( sidebar, [settings], defaultGroup, skip );
/// <summary>
/// The values of several objects in one sheet: groups of the same name are merged, and the
/// shared groups of all of them close the sidebar together.
/// </summary>
public static Sheet Fill( ToolSidebarWidget sidebar, IEnumerable<SerializedObject> objects, string defaultGroup, IReadOnlySet<string> skip = null )
{
var sheet = new Sheet();
var list = objects.Where( x => x is not null ).ToList();
var shown = list.SelectMany( o => o.Where( x => x.HasAttribute<PropertyAttribute>() && skip?.Contains( x.Name ) != true ) ).ToList();
var groups = shown
.Where( x => !x.HasAttribute<AdvancedAttribute>() )
.GroupBy( x => string.IsNullOrEmpty( x.GroupName ) ? defaultGroup : x.GroupName )
.OrderBy( x => Array.IndexOf( Shared, x.Key ) );
foreach ( var properties in groups )
sheet.Add( sidebar, Titles.GetValueOrDefault( properties.Key, properties.Key ), properties, folded: false, expert: Expert.Contains( properties.Key ) );
// One place for what most people never touch, folded until someone opens it.
var advanced = shown.Where( x => x.HasAttribute<AdvancedAttribute>() ).ToList();
if ( advanced.Count > 0 ) sheet.Add( sidebar, AdvancedTitle, advanced, folded: true, expert: true );
foreach ( var o in list ) o.OnPropertyChanged += _ => sheet.Refresh();
sheet.Refresh();
return sheet;
}
/// <summary>
/// The rows of one object, in their sections. <see cref="Refresh"/> applies the search and
/// the conditions without building any control again, so typing in a field keeps its focus.
/// </summary>
public sealed class Sheet
{
readonly List<(SettingsSection Section, List<SettingRow> Rows, bool Expert)> _sections = [];
public string Filter { get; private set; } = "";
public bool ChangedOnly { get; private set; }
public bool Simple { get; private set; }
/// <summary>How many settings the simple view hides right now.</summary>
public int HiddenBySimple => Simple ? _sections.Where( s => s.Expert ).Sum( s => s.Rows.Count( r => r.Applies ) ) : 0;
public int ChangedCount => _sections.Sum( s => s.Rows.Count( r => r.Applies && r.Changed ) );
public event Action Refreshed;
internal void Add( ToolSidebarWidget sidebar, string title, IEnumerable<SerializedProperty> properties, bool folded, bool expert = false )
{
var section = new SettingsSection( sidebar, title, folded );
var rows = new List<SettingRow>();
foreach ( var property in properties )
{
var row = new SettingRow( section.Body, property, title ) { Changed_ = Refresh };
section.Body.Layout.Add( row );
rows.Add( row );
}
sidebar.Layout.Add( section );
_sections.Add( (section, rows, expert) );
}
public void Search( string filter, bool changedOnly, bool simple = false )
{
Filter = filter?.Trim() ?? "";
ChangedOnly = changedOnly;
Simple = simple;
Refresh();
}
public void Refresh()
{
foreach ( var (section, rows, expert) in _sections )
{
var visible = 0;
var changed = 0;
foreach ( var row in rows )
{
var show = row.Applies && row.Matches( Filter ) && (!ChangedOnly || row.Changed);
row.Visible = show;
row.Update();
if ( show ) visible++;
if ( row.Applies && row.Changed ) changed++;
}
// The simple view leaves expert groups out, unless a search or the changed filter asks for them.
section.Visible = visible > 0 && !(Simple && expert && Filter == "" && !ChangedOnly);
section.ChangedCount = changed;
// A search opens what it finds, without forgetting what was folded before.
section.ForceOpen = Filter != "" || ChangedOnly;
section.Update();
}
Refreshed?.Invoke();
}
}
}
/// <summary>
/// A titled, foldable part of a sidebar in the Davelix Construct look. Remembers being folded by
/// its title, so a section closed in one tool is closed in every tool.
/// </summary>
public sealed class SettingsSection : Widget
{
const string FoldedCookie = "AMT.FoldedSections";
const string OpenedCookie = "AMT.OpenedSections";
readonly string _title;
readonly Header _header;
public Widget Body { get; }
public int ChangedCount { get; set; }
bool _forceOpen;
public bool ForceOpen
{
get => _forceOpen;
set { _forceOpen = value; Body.Visible = !Folded || value; }
}
public SettingsSection( Widget parent, string title, bool foldedByDefault = false ) : base( parent )
{
_title = title;
Layout = Layout.Column();
Layout.Margin = new Sandbox.UI.Margin( 4, 2, 4, 2 );
Layout.Spacing = 0;
_header = Layout.Add( new Header( this ) );
Body = Layout.Add( new Widget( this ) { Layout = Layout.Column() } );
Body.Layout.Margin = new Sandbox.UI.Margin( 0, 2, 0, 6 );
Body.Layout.Spacing = 2;
_foldedByDefault = foldedByDefault;
Body.Visible = !Folded;
}
readonly bool _foldedByDefault;
bool Folded =>
_foldedByDefault
? !EditorCookie.Get<List<string>>( OpenedCookie, [] ).Contains( _title )
: EditorCookie.Get<List<string>>( FoldedCookie, [] ).Contains( _title );
void Toggle()
{
var cookie = _foldedByDefault ? OpenedCookie : FoldedCookie;
var list = EditorCookie.Get<List<string>>( cookie, [] );
if ( !list.Remove( _title ) ) list.Add( _title );
EditorCookie.Set( cookie, list );
Body.Visible = !Folded || ForceOpen;
_header.Update();
}
sealed class Header : Widget
{
readonly SettingsSection _section;
public Header( SettingsSection section ) : base( section )
{
_section = section;
FixedHeight = 28;
}
protected override void OnPaint()
{
var rect = LocalRect;
var open = _section.Body.Visible;
Paint.ClearPen();
Paint.SetBrush( IsUnderMouse ? Theme.SurfaceBackground.Lighten( 0.1f ) : Theme.SurfaceBackground );
Paint.DrawRect( rect, 4 );
Paint.SetPen( Theme.TextLight );
Paint.DrawIcon( new Rect( 6, 0, 16, rect.Height ), open ? "expand_more" : "chevron_right", 16, TextFlag.Center );
Paint.SetPen( Theme.Text );
Paint.SetDefaultFont( 8, 600, false, false );
Paint.DrawText( new Rect( 26, 0, rect.Width - 80, rect.Height ), _section._title, TextFlag.LeftCenter | TextFlag.SingleLine );
if ( _section.ChangedCount > 0 )
{
var text = $"{_section.ChangedCount} changed";
Paint.SetDefaultFont( 7, 400, false, false );
Paint.SetPen( Theme.Primary.Lighten( 0.35f ) );
Paint.DrawText( new Rect( rect.Width - 90, 0, 82, rect.Height ), text, TextFlag.RightCenter | TextFlag.SingleLine );
}
}
protected override void OnMouseEnter() => Update();
protected override void OnMouseLeave() => Update();
protected override void OnMousePress( MouseEvent e )
{
if ( e.LeftMouseButton && !_section.ForceOpen ) _section.Toggle();
}
}
}
/// <summary>
/// One value: its name on the left, the editor control on the right, and a button that puts a
/// changed value back to its default. The name's tooltip says what the value does and its default.
/// </summary>
public sealed class SettingRow : Widget
{
readonly SerializedProperty _property;
readonly string _section;
internal Action Changed_ { get; set; }
public SettingRow( Widget parent, SerializedProperty property, string section ) : base( parent )
{
_property = property;
_section = section;
// An asset picker only keeps to the panel's width in the editor's own row.
if ( IsAsset( property ) )
{
Layout = Layout.Row();
Layout.Margin = new Sandbox.UI.Margin( 0, 0, 4, 0 );
Layout.Spacing = 2;
MinimumHeight = Theme.RowHeight;
Layout.Add( ControlSheet.CreateRow( property ), 1 );
Layout.Add( new ResetButton( this ) );
return;
}
var control = ControlWidget.Create( property );
var stacked = control is not null && NeedsFullWidth( property );
Layout = stacked ? Layout.Column() : Layout.Row();
Layout.Margin = new Sandbox.UI.Margin( 8, 0, 4, stacked ? 2 : 0 );
Layout.Spacing = stacked ? 2 : 6;
MinimumHeight = Theme.RowHeight;
// A choice with long names gets the full width under its name, beside it the
// buttons would push every other control out of the panel.
var top = stacked ? Layout.AddRow() : Layout;
var name = top.Add( new NameLabel( this, property ), 2 );
name.MinimumWidth = 90;
if ( stacked ) top.Add( new ResetButton( this ) );
if ( control is not null )
{
control.MinimumHeight = Theme.RowHeight;
control.MinimumWidth = 0;
Layout.Add( control, stacked ? 0 : 3 );
}
if ( !stacked ) Layout.Add( new ResetButton( this ) );
}
static bool IsAsset( SerializedProperty property ) =>
typeof( Resource ).IsAssignableFrom( Nullable.GetUnderlyingType( property.PropertyType ) ?? property.PropertyType );
/// <summary>
/// True for a choice whose names together are too long to sit beside a name.
/// </summary>
static bool NeedsFullWidth( SerializedProperty property )
{
var type = Nullable.GetUnderlyingType( property.PropertyType ) ?? property.PropertyType;
if ( !type.IsEnum ) return false;
var names = type.GetFields( System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static )
.Select( f => f.GetCustomAttributes( false ).OfType<TitleAttribute>().FirstOrDefault()?.Value ?? f.Name )
.ToList();
// Many choices become a drop-down, which fits beside its name anyway. Segments share the width
// evenly, so one long name ("Shadows Only" beside "On" and "Off") needs the full width too.
return names.Count <= 4 && (names.Sum( x => x.Length ) > 26 || names.Max( x => x.Length ) * names.Count > 24);
}
/// <summary>False while a [ShowIf] or [HideIf] says the value does not matter.</summary>
public bool Applies => _property.ShouldShow();
public bool Changed
{
get
{
try
{
var value = _property.GetValue<object>();
var fallback = _property.GetDefault();
static bool Empty( object o ) => o is null or string { Length: 0 };
// A list counts as changed by what it holds, not by being another list object.
if ( value is System.Collections.IEnumerable list && value is not string )
{
var items = list.Cast<object>().ToList();
var defaults = (fallback as System.Collections.IEnumerable)?.Cast<object>().ToList() ?? [];
return items.Count != defaults.Count || items.Where( ( x, i ) => !Equals( x, defaults[i] ) ).Any();
}
if ( Empty( value ) && Empty( fallback ) ) return false;
if ( value is null || fallback is null ) return true;
// A default written as 384 comes back as a whole number for a float setting; compare by value.
static bool Number( object o ) => o is float or double or decimal or int or long or short or byte or uint or ulong or ushort or sbyte;
if ( Number( value ) && Number( fallback ) )
return Math.Abs( Convert.ToDouble( value ) - Convert.ToDouble( fallback ) ) > 1e-6;
return !value.Equals( fallback );
}
catch
{
return false;
}
}
}
public bool Matches( string filter ) =>
filter == ""
|| (_property.DisplayName ?? "").Contains( filter, StringComparison.OrdinalIgnoreCase )
|| (_property.Name ?? "").Contains( filter, StringComparison.OrdinalIgnoreCase )
|| (_property.Description ?? "").Contains( filter, StringComparison.OrdinalIgnoreCase )
|| _section.Contains( filter, StringComparison.OrdinalIgnoreCase );
void Reset()
{
_property.SetValue( _property.GetDefault() );
Changed_?.Invoke();
}
sealed class NameLabel : Widget
{
readonly SerializedProperty _property;
readonly SettingRow _row;
public NameLabel( SettingRow row, SerializedProperty property ) : base( row )
{
_row = row;
_property = property;
MinimumHeight = Theme.RowHeight;
var fallback = property.GetDefault();
var text = fallback is null or string { Length: 0 } ? "" : $"Default: {fallback}";
ToolTip = string.Join( "\n", new[] { property.DisplayName, property.Description, text }.Where( x => !string.IsNullOrWhiteSpace( x ) ) );
}
protected override void OnPaint()
{
var rect = LocalRect;
var changed = _row.Changed;
if ( changed )
{
Paint.ClearPen();
Paint.SetBrush( Theme.Primary );
Paint.DrawCircle( new Vector2( 2, Theme.RowHeight * 0.5f ), new Vector2( 5, 5 ) );
}
Paint.SetPen( changed ? Theme.Text : Theme.TextLight );
Paint.SetDefaultFont( 8, changed ? 500 : 400, false, false );
var area = new Rect( 9, 0, rect.Width - 9, Theme.RowHeight );
Paint.DrawText( area, Paint.GetElidedText( _property.DisplayName, area.Width, ElideMode.Right, TextFlag.SingleLine ), TextFlag.LeftCenter | TextFlag.SingleLine );
}
}
sealed class ResetButton : Widget
{
readonly SettingRow _row;
public ResetButton( SettingRow row ) : base( row )
{
_row = row;
FixedWidth = 20;
FixedHeight = Theme.RowHeight;
}
protected override void OnPaint()
{
if ( !_row.Changed ) return;
if ( IsUnderMouse )
{
Paint.ClearPen();
Paint.SetBrush( Theme.SurfaceBackground.Lighten( 0.2f ) );
Paint.DrawRect( LocalRect, 4 );
}
Paint.SetPen( IsUnderMouse ? Theme.Text : Theme.TextLight );
Paint.DrawIcon( LocalRect, "undo", 14, TextFlag.Center );
}
protected override void OnMouseEnter() { ToolTip = _row.Changed ? "Back to the default" : ""; Update(); }
protected override void OnMouseLeave() => Update();
protected override void OnMousePress( MouseEvent e )
{
if ( e.LeftMouseButton && _row.Changed ) _row.Reset();
}
}
}
/// <summary>
/// The bar on top of a settings sidebar: a search over the names and descriptions of every value,
/// and a switch that shows only the values changed from their defaults.
/// </summary>
public sealed class SettingsFilter : Widget
{
readonly List<PropertySheet.Sheet> _sheets = [];
readonly LineEdit _search;
readonly ChangedToggle _changed;
readonly ViewToggle _view;
readonly string _cookie;
/// <param name="key">Remembers the simple or full view under this name, usually the tool's type.</param>
public SettingsFilter( Widget parent, string key = null ) : base( parent )
{
Layout = Layout.Row();
Layout.Margin = new Sandbox.UI.Margin( 6, 6, 6, 4 );
Layout.Spacing = 4;
_search = Layout.Add( new LineEdit( this ) { PlaceholderText = "Find a setting…", ClearButtonEnabled = true }, 1 );
_search.FixedHeight = Theme.ControlHeight + 2;
_search.TextEdited += _ => Apply();
_changed = Layout.Add( new ChangedToggle( this ) { Toggled = Apply } );
_cookie = key is null ? null : $"AMT.SimpleView.{key}";
_view = Layout.Add( new ViewToggle( this ) { Simple = _cookie is not null && EditorCookie.Get( _cookie, false ) } );
_view.Toggled = () =>
{
if ( _cookie is not null ) EditorCookie.Set( _cookie, _view.Simple );
Apply();
};
_view.Visible = key is not null;
}
public void Watch( PropertySheet.Sheet sheet )
{
if ( sheet is null ) return;
_sheets.Add( sheet );
sheet.Refreshed += () => { _changed.Update(); _view.Update(); };
if ( _view.Simple ) sheet.Search( _search.Text, _changed.On, true );
}
int ChangedCount => _sheets.Sum( x => x.ChangedCount );
void Apply()
{
foreach ( var sheet in _sheets )
sheet.Search( _search.Text, _changed.On, _view.Simple );
}
int HiddenCount => _sheets.Sum( x => x.HiddenBySimple );
/// <summary>
/// Switches between the simple view and every setting. Shows how many settings it hides.
/// </summary>
sealed class ViewToggle : Widget
{
readonly SettingsFilter _filter;
public bool Simple { get; set; }
public Action Toggled { get; set; }
public ViewToggle( SettingsFilter filter ) : base( filter )
{
_filter = filter;
FixedHeight = Theme.ControlHeight + 2;
FixedWidth = 58;
}
protected override void OnPaint()
{
Paint.ClearPen();
Paint.SetBrush( IsUnderMouse ? Theme.SurfaceBackground.Lighten( 0.15f ) : Theme.SurfaceBackground );
Paint.DrawRect( LocalRect, 4 );
Paint.SetPen( Simple ? Theme.Primary.Lighten( 0.35f ) : Theme.TextLight );
Paint.SetDefaultFont( 8, Simple ? 600 : 400, false, false );
Paint.DrawText( LocalRect, Simple ? "Simple" : "Full", TextFlag.Center | TextFlag.SingleLine );
}
protected override void OnMouseEnter()
{
ToolTip = Simple
? $"Simple view: {_filter.HiddenCount} expert settings (snapping, curves, placing, advanced) are hidden. Click for every setting."
: "Every setting is shown. Click for the simple view, which hides snapping, curves, placing and advanced settings for this tool.";
Update();
}
protected override void OnMouseLeave() => Update();
protected override void OnMousePress( MouseEvent e )
{
if ( !e.LeftMouseButton ) return;
Simple = !Simple;
OnMouseEnter();
Toggled?.Invoke();
}
}
sealed class ChangedToggle : Widget
{
readonly SettingsFilter _filter;
public bool On { get; private set; }
public Action Toggled { get; set; }
public ChangedToggle( SettingsFilter filter ) : base( filter )
{
_filter = filter;
FixedHeight = Theme.ControlHeight + 2;
FixedWidth = 92;
ToolTip = "Show only the settings changed from their defaults";
}
protected override void OnPaint()
{
var count = _filter.ChangedCount;
Paint.ClearPen();
Paint.SetBrush( On ? Theme.Primary.WithAlpha( 0.35f ) : IsUnderMouse ? Theme.SurfaceBackground.Lighten( 0.15f ) : Theme.SurfaceBackground );
Paint.DrawRect( LocalRect, 4 );
Paint.SetPen( On ? Theme.Text : count > 0 ? Theme.Primary.Lighten( 0.35f ) : Theme.TextLight );
Paint.SetDefaultFont( 8, On ? 600 : 400, false, false );
Paint.DrawText( LocalRect, count == 0 ? "No changes" : $"{count} changed", TextFlag.Center | TextFlag.SingleLine );
}
protected override void OnMouseEnter() => Update();
protected override void OnMouseLeave() => Update();
protected override void OnMousePress( MouseEvent e )
{
if ( !e.LeftMouseButton ) return;
if ( !On && _filter.ChangedCount == 0 ) return;
On = !On;
Update();
Toggled?.Invoke();
}
}
}
/// <summary>
/// The top of a tool's sidebar: its icon, name and group, and what it does. A help button opens
/// the hints for using it, the mouse and keys; whether help is open is remembered for all tools.
/// </summary>
public sealed class ToolHeader : Widget
{
const string HelpCookie = "AMT.HelpOpen";
readonly string _icon;
readonly string _title;
readonly string _group;
readonly Widget _help;
public ToolHeader( Widget parent, TypeDescription tool, IEnumerable<string> hints = null ) : base( parent )
{
_icon = string.IsNullOrEmpty( tool?.Icon ) ? "build" : tool.Icon;
_title = tool?.Title ?? "Tool";
_group = tool is null ? "" : ToolCatalog.CategoryOf( tool );
Layout = Layout.Column();
Layout.Margin = new Sandbox.UI.Margin( 8, 8, 8, 2 );
Layout.Spacing = 4;
Layout.Add( new TitleRow( this, hints?.Any() == true ) );
if ( !string.IsNullOrWhiteSpace( tool?.Description ) )
Layout.Add( new Label( tool.Description, this ) { WordWrap = true, Color = Theme.TextLight } );
_help = Layout.Add( new HelpBox( this, hints ?? [] ) );
_helpOpen = hints?.Any() == true && EditorCookie.Get( HelpCookie, false );
_help.Visible = _helpOpen;
}
// Kept apart from the widget: a widget not on screen yet always reads as not visible.
bool _helpOpen;
bool HelpOpen => _helpOpen;
void ToggleHelp()
{
_helpOpen = !_helpOpen;
_help.Visible = _helpOpen;
EditorCookie.Set( HelpCookie, _helpOpen );
}
sealed class TitleRow : Widget
{
readonly ToolHeader _header;
readonly bool _hasHelp;
Vector2 _mouse;
public TitleRow( ToolHeader header, bool hasHelp ) : base( header )
{
_header = header;
_hasHelp = hasHelp;
FixedHeight = 36;
}
Rect HelpRect => new( Width - 28, 6, 26, 26 );
protected override void OnPaint()
{
var box = new Rect( 0, 2, 32, 32 );
Paint.ClearPen();
Paint.SetBrush( Theme.Primary.WithAlpha( 0.2f ) );
Paint.DrawRect( box, 6 );
Paint.SetPen( Theme.Primary.Lighten( 0.4f ) );
Paint.DrawIcon( box, _header._icon, 20, TextFlag.Center );
var textWidth = Width - 42 - (_hasHelp ? 32 : 0);
Paint.SetPen( Theme.Text );
Paint.SetHeadingFont( 10, 600, false, false );
Paint.DrawText( new Rect( 42, 1, textWidth, 18 ), _header._title, TextFlag.LeftCenter | TextFlag.SingleLine );
Paint.SetPen( Theme.TextLight );
Paint.SetDefaultFont( 8, 400, false, false );
Paint.DrawText( new Rect( 42, 19, textWidth, 16 ), _header._group, TextFlag.LeftCenter | TextFlag.SingleLine );
if ( !_hasHelp ) return;
var help = HelpRect;
var over = IsUnderMouse && help.IsInside( _mouse );
if ( over || _header.HelpOpen )
{
Paint.ClearPen();
Paint.SetBrush( _header.HelpOpen ? Theme.Primary.WithAlpha( 0.3f ) : Theme.SurfaceBackground.Lighten( 0.15f ) );
Paint.DrawRect( help, 4 );
}
Paint.SetPen( _header.HelpOpen ? Theme.Text : Theme.TextLight );
Paint.DrawIcon( help, "help_outline", 17, TextFlag.Center );
}
protected override void OnMouseMove( MouseEvent e )
{
_mouse = e.LocalPosition;
ToolTip = _hasHelp && HelpRect.IsInside( _mouse ) ? (_header.HelpOpen ? "Hide the help" : "How to use this tool") : "";
Update();
}
protected override void OnMouseLeave() => Update();
protected override void OnMousePress( MouseEvent e )
{
if ( !e.LeftMouseButton || !_hasHelp || !HelpRect.IsInside( e.LocalPosition ) ) return;
_header.ToggleHelp();
Update();
}
}
/// <summary>
/// The hints of a tool as a list, one line each, keys and mouse buttons in front.
/// </summary>
sealed class HelpBox : Widget
{
public HelpBox( Widget parent, IEnumerable<string> hints ) : base( parent )
{
Layout = Layout.Column();
Layout.Margin = new Sandbox.UI.Margin( 12, 8, 8, 8 );
Layout.Spacing = 3;
Layout.Add( new Label( "How to use", this ) { Color = Theme.Text } );
foreach ( var hint in hints )
Layout.Add( new Label( "• " + hint, this ) { WordWrap = true, Color = Theme.TextLight } );
}
protected override void OnPaint()
{
Paint.ClearPen();
Paint.SetBrush( Theme.SurfaceBackground );
Paint.DrawRect( LocalRect, 4 );
Paint.SetBrush( Theme.Primary.WithAlpha( 0.6f ) );
Paint.DrawRect( new Rect( 0, 0, 3, Height ), 1.5f );
}
}
}
/// <summary>
/// A short warning on top of a sidebar, with a coloured bar so it is not overlooked.
/// </summary>
public sealed class Notice : Widget
{
public Notice( Widget parent, string title, string text ) : base( parent )
{
Layout = Layout.Column();
Layout.Margin = new Sandbox.UI.Margin( 14, 8, 8, 8 );
Layout.Spacing = 2;
Layout.Add( new Label( title, this ) { Color = Theme.Yellow } );
Layout.Add( new Label( text, this ) { WordWrap = true, Color = Theme.Text } );
}
protected override void OnPaint()
{
var rect = LocalRect.Shrink( 4, 2, 4, 2 );
Paint.ClearPen();
Paint.SetBrush( Theme.Yellow.WithAlpha( 0.08f ) );
Paint.DrawRect( rect, 4 );
Paint.SetBrush( Theme.Yellow );
Paint.DrawRect( new Rect( rect.Left, rect.Top, 3, rect.Height ), 1.5f );
}
}