Editor/Cabinet/ArchCabinetUi.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;
namespace Sunless.Architecture;
sealed class ArchFollowRow : Widget {
readonly IconButton badge;
readonly Action clear;
public ArchFollowRow( Widget control, bool own, string following, Action clear ) {
this.clear = clear;
Layout = Layout.Row();
Layout.Spacing = 4;
Layout.Add( control, 1 );
badge = new IconButton( own ? "link_off" : "link" ) {
FixedSize = 22f,
IconSize = 14f,
Background = Color.Transparent,
BackgroundActive = Color.Transparent,
Foreground = own ? Theme.Blue : Theme.TextControl.WithAlpha( 0.3f ),
ToolTip = own ? $"Set here — press to go back to {following}" : following,
OnClick = own ? Hand : null
};
Layout.Add( badge );
}
void Hand() {
clear?.Invoke();
badge.Icon = "link";
badge.Foreground = Theme.TextControl.WithAlpha( 0.3f );
badge.OnClick = null;
badge.Update();
}
}
public static class ArchCabinetUi {
public static void Kinds( Layout group, IReadOnlyList<ArchCabinetType> offered, string chosen, Action<ArchCabinetType> adopt ) {
using var grid = ArchIconGrid.In( group );
foreach ( var type in offered ) {
var standing = type;
grid.Pick( $"{Titled( standing )} — {standing.Detail}", $"cabinet_{standing.Name}", Glyph( standing.Tier ),
string.Equals( standing.Name, chosen, StringComparison.OrdinalIgnoreCase ), () => adopt( standing ) );
}
}
public static void Tier( Layout group, CabinetTier standing, Action<CabinetTier> assign ) {
using var grid = ArchIconGrid.In( group );
grid.Pick( "Follow the type — a larder stands tall, a wall unit hangs", "cabinet_tier_auto", "auto_awesome",
standing == CabinetTier.Auto, () => assign( CabinetTier.Auto ) );
grid.Pick( "Base — on the floor, counter over it", "cabinet_tier_base", "table_restaurant",
standing == CabinetTier.Base, () => assign( CabinetTier.Base ) );
grid.Pick( "Hanging — off the counter below, or the standard datum", "cabinet_tier_hanging", "shelves",
standing == CabinetTier.Hanging, () => assign( CabinetTier.Hanging ) );
grid.Pick( "Tall — floor to whatever covers it", "cabinet_tier_tall", "door_sliding",
standing == CabinetTier.Tall, () => assign( CabinetTier.Tall ) );
}
public static void Front( Layout group, ArchCabinetFitting fitting, Action chose, Action changed ) {
Faces( group, () => fitting, fitting, chose, changed );
}
// Writes the run's own fitting, not the type's — editing a shelf entry would silently re-cut every run on it
public static void Front( Layout group, ArchCabinetPart part, ArchCabinetType type, string mount, Action chose, Action changed ) {
using ( var grid = ArchIconGrid.In( group ) ) {
grid.Pick( $"Following {Titled( type )} — {Named( type?.On( mount ) )}", "cabinet_front_following", "link",
part.On( mount ) is null, () => {
part.Fittings?.RemoveAll( standing => ArchCabinetMounts.Same( standing.Mount, mount ) );
chose?.Invoke();
changed?.Invoke();
} );
}
Faces( group, () => Own( part, type, mount ), part.On( mount ) ?? type?.On( mount ), chose, changed );
}
static void Faces( Layout group, Func<ArchCabinetFitting> reach, ArchCabinetFitting showing, Action chose, Action changed ) {
using ( var grid = ArchIconGrid.In( group ) ) {
Face( grid, reach, showing, CabinetFront.Slab, "Slab — one flat door", "cabinet_front_slab", "crop_square", chose );
Face( grid, reach, showing, CabinetFront.Shaker, "Shaker — stiles and rails round a flush field", "cabinet_front_shaker", "border_all", chose );
Face( grid, reach, showing, CabinetFront.Panel, "Panel — a raised field inside the frame", "cabinet_front_panel", "dashboard", chose );
Face( grid, reach, showing, CabinetFront.Glass, "Glass — frame with nothing in it", "cabinet_front_glass", "window", chose );
Face( grid, reach, showing, CabinetFront.None, "Open — no front at all", "cabinet_front_open", "crop_free", chose );
}
Authored( group, reach, showing, chose, changed );
}
static void Authored( Layout group, Func<ArchCabinetFitting> reach, ArchCabinetFitting showing, Action chose, Action changed ) {
var offered = ArchCabinetFronts.Authored();
Layout row = null;
for ( var index = 0; index < offered.Count; index++ ) {
var preset = offered[index];
if ( index % 2 == 0 ) {
row = group.AddRow();
row.Spacing = 4;
}
row.Add( Card( preset, reach, showing, chose, changed ) );
}
if ( offered.Count % 2 == 1 ) {
row.AddStretchCell();
}
}
static Widget Card( ArchCabinetFrontPreset preset, Func<ArchCabinetFitting> reach, ArchCabinetFitting showing,
Action chose, Action changed ) {
var art = AssetSystem.FindByPath( preset.Prefab )?.GetAssetThumb();
var tile = new ArchTile( null, art, preset.Name, preset.Detail, !preset.Anchored, new Vector2( 96f, 104f ) ) {
LabelStrip = 26f,
ToolTip = preset.Anchored
? $"{preset.Name} — {preset.Prefab}"
: $"{preset.Name} — not anchored yet, so it lands on its own origin",
Current = () => string.Equals( showing?.Prefab, preset.Prefab, StringComparison.OrdinalIgnoreCase ),
Clicked = () => Adopt( preset, reach(), chose, changed ),
Menu = menu => menu.AddOption( $"Remove {preset.Name}", "delete", () => {
ArchCabinetFronts.Remove( preset.Name );
chose?.Invoke();
} )
};
if ( art is null ) {
tile.Glyph = rect => Paint.DrawIcon( rect, "widgets", rect.Height );
}
return tile;
}
static void Adopt( ArchCabinetFrontPreset preset, ArchCabinetFitting fitting, Action chose, Action changed ) {
fitting.Prefab = preset.Prefab;
fitting.Front = CabinetFront.None;
fitting.Fits = preset.Scales;
fitting.Anchor = preset.Anchor;
fitting.AnchorAlong = preset.AnchorAlong;
chose?.Invoke();
changed?.Invoke();
}
public static void Inside( Layout group, ArchCabinetPart part, ArchCabinetType type, Action changed, Action refresh = null ) {
var following = Titled( type );
var settled = refresh ?? changed;
group.Add( new ArchFollowRow(
ArchPartUi.Check( "Interior — panels, shelves and the faces inside them", part.Interior || Inherited( type ).Interior,
value => part.Interior = value, changed ),
part.Interior,
$"Following {following} — {(Inherited( type ).Interior ? "on" : "off")}",
() => { part.Interior = false; settled?.Invoke(); } ) );
group.Add( new ArchFollowRow(
ArchPartUi.Check( "Handles — a bar on every front", part.Handles ?? Inherited( type ).Handles,
value => part.Handles = value, changed ),
part.Handles is not null,
$"Following {following} — {(Inherited( type ).Handles ? "on" : "off")}",
() => { part.Handles = null; settled?.Invoke(); } ) );
Counted( group, "Shelves", part.Shelves, Inherited( type ).Shelves, following,
value => part.Shelves = value, changed, settled );
Counted( group, "Drawer rows", part.DrawerRows, Inherited( type ).DrawerRows, following,
value => part.DrawerRows = value, changed, settled );
}
public static void Sizes( Layout group, ArchCabinetPart part, ArchCabinetType type, Action changed,
ArchCabinetShape resolved = null, Action refresh = null ) {
var following = Titled( type );
var settled = refresh ?? changed;
Measured( group, "Module width", part.ModuleWidth, resolved?.Cells.FirstOrDefault()?.Width ?? Inherited( type ).Width,
following, value => part.ModuleWidth = value, changed, settled );
Measured( group, "Depth", part.Depth, resolved?.Depth ?? Inherited( type ).Depth,
following, value => part.Depth = value, changed, settled );
Measured( group, "Height", part.Height, resolved is null ? Stood( type ) : resolved.Head - resolved.Seat,
Reaches( type, resolved ), value => part.Height = value, changed, settled );
Seat( group, part, resolved, changed, settled );
}
// Typing a seat pins it; the badge unpins
public static void Seat( Layout group, ArchCabinetPart part, ArchCabinetShape resolved, Action changed, Action settled ) {
group.Add( new ArchFollowRow(
ArchPartUi.Number( "Seat height", part.Seated ? part.BaseHeight : 0f, resolved?.Seat ?? 0f, value => {
part.BaseHeight = value;
part.Seated = value > 0.001f;
}, changed ),
part.Seated,
$"Following {Derives( resolved )} — {resolved?.Seat ?? 0f:0.##}",
() => {
part.Seated = false;
part.BaseHeight = 0f;
settled?.Invoke();
} ) );
}
public static void Worktop( Layout group, ArchCabinetPart part, ArchCabinetType type, Action changed,
ArchCabinetShape resolved = null, Action refresh = null ) {
var following = Titled( type );
var settled = refresh ?? changed;
Measured( group, "Counter depth", part.TopDepth, resolved?.TopDepth ?? Counter( type ),
following, value => part.TopDepth = value, changed, settled );
Optional( group, "Thickness", part.TopThickness, resolved?.Thickness ?? Inherited( type ).TopThickness,
following, value => part.TopThickness = value, changed, settled );
Optional( group, "Past the ends", part.TopEndOversail, resolved?.EndOversail ?? Inherited( type ).TopEndOversail,
following, value => part.TopEndOversail = value, changed, settled );
Optional( group, "Past the back", part.TopBackOversail, resolved?.BackOversail ?? Inherited( type ).TopBackOversail,
following, value => part.TopBackOversail = value, changed, settled );
}
public static void Finish( Layout group, ArchCabinetPart part, ArchCabinetType type, Action changed, Action refresh ) {
var offered = ArchPrefabGroups.Offered( type?.In( CabinetSlot.Module )?.Prefab );
if ( offered.Count == 0 ) {
group.Add( ArchPartUi.Wrapped( "This type's art carries no material groups." ) );
return;
}
using var grid = ArchIconGrid.In( group );
grid.Pick( $"Following {Titled( type )} — {Finished( type?.MaterialGroup )}", "cabinet_finish_following", "link",
string.IsNullOrWhiteSpace( part.MaterialGroup ), () => {
part.MaterialGroup = "";
refresh?.Invoke();
changed?.Invoke();
} );
foreach ( var name in offered ) {
var standing = name;
grid.Pick( $"{standing} — every carcass in this run wears it", $"cabinet_finish_{standing}".ToLowerInvariant(),
"palette", string.Equals( part.MaterialGroup, standing, StringComparison.OrdinalIgnoreCase ), () => {
part.MaterialGroup = standing;
refresh?.Invoke();
changed?.Invoke();
} );
}
}
static string Finished( string group ) => string.IsNullOrWhiteSpace( group ) ? "as modelled" : group;
public static void Fittings( Layout group, ArchCabinetPart part, ArchCabinetType type, ArchCabinetShape resolved,
Action changed, Action refresh ) {
var module = resolved?.Cells.FirstOrDefault();
if ( module is null ) {
group.Add( ArchPartUi.Wrapped( "Nothing standing yet — a run has to resolve before its mounts can be dressed." ) );
return;
}
foreach ( var mount in module.Mounts.Where( mount => Dressable( mount.Name ) ) ) {
group.Add( Mounted( part, type, mount.Name, changed, refresh ) );
}
}
static bool Dressable( string mount ) {
return ArchCabinetMounts.Same( mount, ArchCabinetMounts.Front )
|| ArchCabinetMounts.Same( mount, ArchCabinetMounts.FrontUpper )
|| ArchCabinetMounts.Same( mount, ArchCabinetMounts.Drawer );
}
static Widget Mounted( ArchCabinetPart part, ArchCabinetType type, string mount, Action changed, Action refresh ) {
var own = part.On( mount );
var standing = own ?? type?.On( mount );
var holder = new Widget( null ) { Layout = Layout.Row() };
holder.Layout.Spacing = 4;
holder.Layout.Add( new Label( mount ) { MinimumWidth = 88 } );
holder.Layout.Add( new Label( Named( standing ) ) { MinimumWidth = 60 }, 1 );
holder.Layout.Add( new IconButton( "edit" ) {
FixedSize = 22f,
IconSize = 14f,
ToolTip = $"Dress the {mount.ToLowerInvariant()}",
OnClick = () => Dress( part, type, mount, changed, refresh )
} );
return new ArchFollowRow( holder, own is not null,
$"Following {Titled( type )} — {Named( type?.On( mount ) )}",
() => {
part.Fittings?.RemoveAll( fitting => ArchCabinetMounts.Same( fitting.Mount, mount ) );
refresh?.Invoke();
changed?.Invoke();
} );
}
static void Dress( ArchCabinetPart part, ArchCabinetType type, string mount, Action changed, Action refresh ) {
var panel = new ToolSidebarWidget();
ArchSidebarSection.Show( panel, $"cabinet.mount.{mount}", mount, group =>
Front( group, part, type, mount, refresh, changed ) );
var popup = new PopupWidget( null ) { Layout = Layout.Column() };
popup.Layout.Add( panel );
popup.FixedWidth = ArchPartUi.PanelWidth + 24f;
popup.OpenAtCursor( false );
}
static ArchCabinetFitting Own( ArchCabinetPart part, ArchCabinetType type, string mount ) {
if ( part.On( mount ) is { } standing ) {
return standing;
}
var following = type?.On( mount );
var made = new ArchCabinetFitting {
Mount = mount,
Front = following?.Front ?? CabinetFront.Slab,
Prefab = following?.Prefab ?? "",
Fits = following?.Fits ?? false,
Anchor = following?.Anchor ?? Vector3.Zero,
AnchorAlong = following?.AnchorAlong ?? Vector3.Zero
};
part.Fittings ??= new List<ArchCabinetFitting>();
part.Fittings.Add( made );
return made;
}
static void Measured( Layout group, string label, float own, float inherited, string following,
Action<float> assign, Action changed, Action settled ) {
var control = ArchPartUi.Number( label, own, inherited, assign, changed );
group.Add( new ArchFollowRow( control, own > 0.001f, $"Following {following} — {inherited:0.##}",
() => Hand( control, () => assign( 0f ), settled ) ) );
}
static void Optional( Layout group, string label, float? own, float inherited, string following,
Action<float?> assign, Action changed, Action settled ) {
var control = ArchPartUi.OptionalNumber( label, own, inherited, assign, changed );
group.Add( new ArchFollowRow( control, own is not null, $"Following {following} — {inherited:0.##}",
() => Hand( control, () => assign( null ), settled ) ) );
}
static void Counted( Layout group, string label, int? own, int inherited, string following,
Action<int?> assign, Action changed, Action settled ) {
var control = ArchPartUi.Integer( label, own ?? 0, inherited, value => assign( value > 0 ? value : null ), changed );
group.Add( new ArchFollowRow( control, own is not null, $"Following {following} — {inherited}",
() => Hand( control, () => assign( null ), settled ) ) );
}
// Blanks the control immediately so the row reads as inherited before the next rebuild
static void Hand( Widget control, Action assign, Action settled ) {
assign();
foreach ( var edit in control.Children.OfType<LineEdit>() ) {
edit.Text = "";
}
settled?.Invoke();
}
public static string Titled( ArchCabinetType type ) {
if ( type is null ) {
return "None";
}
var name = type.Name.Replace( '_', ' ' );
return name.Length == 0 ? "Unnamed" : char.ToUpperInvariant( name[0] ) + name[1..];
}
public static string Glyph( CabinetTier tier ) => tier switch {
CabinetTier.Hanging => "shelves",
CabinetTier.Tall => "door_sliding",
_ => "kitchen"
};
public static string Named( CabinetFront front ) => front switch {
CabinetFront.None => "Open",
CabinetFront.Shaker => "Shaker",
CabinetFront.Panel => "Panel",
CabinetFront.Glass => "Glass",
_ => "Slab"
};
public static string Named( ArchCabinetFitting fitting ) {
if ( fitting is null ) {
return "None";
}
return fitting.Prefabbed ? ArchCabinetFronts.Resolve( fitting.Prefab )?.Name ?? fitting.Prefab : Named( fitting.Front );
}
static string Reaches( ArchCabinetType type, ArchCabinetShape resolved ) {
if ( resolved is null ) {
return Titled( type );
}
return resolved.Tier == CabinetTier.Base ? $"{Titled( type )} standing free" : "whatever covers it";
}
static string Derives( ArchCabinetShape resolved ) {
if ( resolved is null ) {
return "the floor";
}
return resolved.Tier == CabinetTier.Hanging ? "the counter below" : "the floor";
}
static ArchCabinetType Inherited( ArchCabinetType type ) => type ?? new ArchCabinetType();
static float Stood( ArchCabinetType type ) {
return type is null ? 36f : MathF.Max( 0f, type.PlinthHeight ) + type.Carcass + MathF.Max( 0f, type.TopThickness );
}
static float Counter( ArchCabinetType type ) {
return type is null ? 25f : type.TopDepth > 1f ? type.TopDepth : type.Depth + MathF.Max( 0f, type.TopOversail );
}
static void Face( ArchIconGrid grid, Func<ArchCabinetFitting> reach, ArchCabinetFitting showing, CabinetFront front,
string tooltip, string slug, string fallback, Action chose ) {
grid.Pick( tooltip, slug, fallback, showing is { Prefabbed: false } && showing.Front == front, () => {
var fitting = reach();
fitting.Front = front;
fitting.Prefab = "";
fitting.Anchor = Vector3.Zero;
fitting.AnchorAlong = Vector3.Zero;
chose?.Invoke();
} );
}
}