Editor/InteriorLayoutBuilder/RoomLayoutControls.cs
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Editor;
using Sandbox;
namespace ReusableRoomLayout;
internal sealed partial class RoomLayoutControls : Widget
{
private const int ModeButtonWidth = 132;
private const int ActionButtonWidth = 132;
private const int WideActionButtonWidth = 148;
private const int DefaultMaterialsButtonWidth = 156;
private const int SmallActionButtonWidth = 96;
private const int DangerButtonWidth = 132;
private const int FieldLabelWidth = 112;
private const int LongFieldLabelWidth = 156;
private const int NumberEditWidth = 96;
private const int CheckboxWidth = 24;
private const int MaterialLabelWidth = 100;
private const int MaterialButtonWidth = 260;
private const int MaterialScaleLabelWidth = 42;
private const int MaterialScaleEditWidth = 72;
private const int SelectedLabelWidth = 260;
private const int SectionPaddingY = 20;
private const int SectionSpacing = 8;
private readonly RoomLayoutTool fixedTool;
private readonly Dictionary<MaterialButtonKey, Button> materialButtons = new();
private readonly Dictionary<MaterialButtonKey, LineEdit> materialScaleEdits = new();
private SettingsTab activeSettingsTab = SettingsTab.Wall;
private SegmentedControl settingsTabs;
private Widget wallSettingsPanel;
private Widget doorSettingsPanel;
private Widget windowSettingsPanel;
private Widget baseboardSettingsPanel;
private Widget thresholdSettingsPanel;
private Widget roofSettingsPanel;
private Widget defaultMaterialsPanel;
private Widget selectedDimensionsLabel;
private Widget selectedDoorDimensionsPanel;
private Widget selectedWindowDimensionsPanel;
private Label mode;
private Label selectedLayout;
private Button selectLayoutButton;
private LineEdit wallHeightEdit;
private LineEdit doorWidthEdit;
private LineEdit doorHeightEdit;
private LineEdit doorFrameThicknessEdit;
private LineEdit windowWidthEdit;
private LineEdit windowHeightEdit;
private LineEdit windowSillHeightEdit;
private LineEdit selectedDoorWidthEdit;
private LineEdit selectedDoorHeightEdit;
private LineEdit selectedWindowWidthEdit;
private LineEdit selectedWindowHeightEdit;
private LineEdit selectedWindowSillHeightEdit;
private Checkbox baseboardsCheckbox;
private Checkbox thresholdsCheckbox;
private Checkbox roofCheckbox;
private LineEdit baseboardHeightEdit;
private LineEdit baseboardDepthEdit;
private LineEdit thresholdDepthEdit;
private LineEdit roofThicknessEdit;
private Button deleteSelectedButton;
private Button resetLayoutButton;
private Button defaultMaterialsButton;
public RoomLayoutControls( Widget parent ) : base( parent )
{
Build();
}
public RoomLayoutControls( Widget parent, RoomLayoutTool tool ) : base( parent )
{
fixedTool = tool;
Build();
}
private void Build()
{
Layout = Layout.Column();
Layout.Margin = 0;
var scrollArea = Layout.Add( new ScrollArea( this ), 1 );
scrollArea.Canvas = new Widget( scrollArea );
scrollArea.Canvas.SetSizeMode( SizeMode.Flexible, SizeMode.CanShrink );
scrollArea.SetStyles( "QScrollArea { border: 0px; }" );
var content = scrollArea.Canvas;
content.Layout = Layout.Column();
content.Layout.Alignment = TextFlag.LeftTop;
content.Layout.Margin = 10;
content.Layout.Spacing = 8;
var modeSection = AddSection( content, "Modes", contentRows: 3 );
mode = AddStatusLabel( modeSection );
var modeRow = AddRow( modeSection );
AddButton( modeRow, "Select", "ads_click", OnSelectClicked, ModeButtonWidth );
AddButton( modeRow, "Create Room", "crop_square", OnRoomsClicked, ModeButtonWidth );
AddButton( modeRow, "Create Door", "door_front", OnDoorsClicked, ModeButtonWidth );
var modeRow2 = AddRow( modeSection );
AddButton( modeRow2, "Create Window", "window", OnWindowsClicked, ModeButtonWidth );
AddButton( modeRow2, "Create Corridor", "timeline", OnCorridorsClicked, ModeButtonWidth );
AddButton( modeRow2, "Create Cutout", "stairs", OnFloorCutoutsClicked, ModeButtonWidth );
AddFloorSection( AddSection( content, "Floors", contentRows: 2 ) );
var actionSection = AddSection( content, "Actions", contentRows: 1 );
var buildRow = AddRow( actionSection );
AddButton( buildRow, "Rebuild", "construction", OnBuildClicked, ActionButtonWidth );
AddButton( buildRow, "Clear Generated", "layers_clear", OnClearGeneratedClicked, WideActionButtonWidth );
defaultMaterialsButton = AddButton( buildRow, "Default Materials", "tune", ToggleDefaultMaterialsPanel, DefaultMaterialsButtonWidth );
defaultMaterialsButton.ToolTip = "Change inherited material defaults.";
AddDefaultMaterialsSection( content );
AddSettingsTabsSection( AddSection( content, "Settings", contentRows: 4 ) );
AddSelectedSection( AddSection( content, "Selected", contentRows: 14 ) );
AddResetLayoutSection( AddSection( content, "Layout", contentRows: 1 ) );
}
[EditorEvent.Frame]
public void Frame()
{
var tool = GetTool();
tool?.EnsureLayoutLoadedForControls();
mode.Text = tool is null ? "Tool inactive" : $"Active: {tool.ModeName}";
selectedLayout.Text = tool is null ? "Selected: none" : $"Selected: {tool.SelectedLayoutName}";
if ( deleteSelectedButton is not null )
{
deleteSelectedButton.Enabled = tool?.HasSelectedLayout ?? false;
}
if ( selectLayoutButton is not null )
{
selectLayoutButton.Enabled = tool?.HasSelectableLayoutItems ?? false;
}
if ( defaultMaterialsButton is not null )
{
defaultMaterialsButton.Enabled = tool is not null;
defaultMaterialsButton.Text = defaultMaterialsPanel?.Visible == true ? "Hide Defaults" : "Default Materials";
}
if ( resetLayoutButton is not null )
{
resetLayoutButton.Enabled = tool is not null;
}
UpdateFloorControls( tool );
foreach ( var entry in materialButtons )
{
var key = entry.Key;
var button = entry.Value;
var thresholdDisabled = key.Slot == RoomLayoutMaterialSlot.Threshold && tool?.ThresholdsEnabled == false;
var roofDisabled = key.Slot == RoomLayoutMaterialSlot.Roof && tool?.RoofEnabled == false;
var enabled = tool is not null &&
!thresholdDisabled &&
!roofDisabled &&
(key.Scope == MaterialScope.Defaults || tool.CanSetSelectedMaterialSlot( key.Slot ));
var path = tool is null ? "" : MaterialPathForButton( tool, key );
button.Enabled = enabled;
button.Text = MaterialButtonText( path, key.Scope == MaterialScope.Selected );
button.ToolTip = thresholdDisabled
? "Enable thresholds in the Thresholds tab to use this material."
: roofDisabled
? "Enable roof in the Roof tab to use this material."
: MaterialButtonTooltip( path, key.Scope == MaterialScope.Selected );
}
foreach ( var entry in materialScaleEdits )
{
UpdateMaterialScaleEdit( entry.Key, entry.Value, tool );
}
UpdateFloatEdit( wallHeightEdit, tool, tool?.WallHeight );
UpdateFloatEdit( doorWidthEdit, tool, tool?.DoorWidth );
UpdateFloatEdit( doorHeightEdit, tool, tool?.DoorHeight );
UpdateFloatEdit( doorFrameThicknessEdit, tool, tool?.DoorFrameThickness );
UpdateFloatEdit( windowWidthEdit, tool, tool?.WindowWidth );
UpdateFloatEdit( windowHeightEdit, tool, tool?.WindowHeight );
UpdateFloatEdit( windowSillHeightEdit, tool, tool?.WindowSillHeight );
UpdateFloatEdit( windowFrameThicknessEdit, tool, tool?.WindowFrameThickness );
UpdateFloatEdit( selectedDoorWidthEdit, tool, tool?.SelectedDoorWidth );
UpdateFloatEdit( selectedDoorHeightEdit, tool, tool?.SelectedDoorHeight );
UpdateFloatEdit( selectedWindowWidthEdit, tool, tool?.SelectedWindowWidth );
UpdateFloatEdit( selectedWindowHeightEdit, tool, tool?.SelectedWindowHeight );
UpdateFloatEdit( selectedWindowSillHeightEdit, tool, tool?.SelectedWindowSillHeight );
UpdateCheckbox( baseboardsCheckbox, tool, tool?.BaseboardsEnabled );
UpdateCheckbox( thresholdsCheckbox, tool, tool?.ThresholdsEnabled );
UpdateCheckbox( roofCheckbox, tool, tool?.RoofEnabled );
UpdateFloatEdit( baseboardHeightEdit, tool, tool?.BaseboardHeight );
UpdateFloatEdit( baseboardDepthEdit, tool, tool?.BaseboardDepth );
UpdateFloatEdit( thresholdDepthEdit, tool, tool?.ThresholdDepth );
UpdateFloatEdit( roofThicknessEdit, tool, tool?.RoofThickness );
SetFloatEditEditable( baseboardHeightEdit, tool is not null && tool.BaseboardsEnabled );
SetFloatEditEditable( baseboardDepthEdit, tool is not null && tool.BaseboardsEnabled );
SetFloatEditEditable( thresholdDepthEdit, tool is not null && tool.ThresholdsEnabled );
SetFloatEditEditable( roofThicknessEdit, tool is not null && tool.RoofEnabled );
SetFloatEditEditable( selectedDoorWidthEdit, tool?.HasSelectedDoor == true );
SetFloatEditEditable( selectedDoorHeightEdit, tool?.HasSelectedDoor == true );
SetFloatEditEditable( selectedWindowWidthEdit, tool?.HasSelectedWindow == true );
SetFloatEditEditable( selectedWindowHeightEdit, tool?.HasSelectedWindow == true );
SetFloatEditEditable( selectedWindowSillHeightEdit, tool?.HasSelectedWindow == true );
var showSelectedDimensions = tool?.HasSelectedDoor == true || tool?.HasSelectedWindow == true;
if ( selectedDimensionsLabel is not null )
{
selectedDimensionsLabel.Visible = showSelectedDimensions;
}
if ( selectedDoorDimensionsPanel is not null )
{
selectedDoorDimensionsPanel.Visible = tool?.HasSelectedDoor == true;
}
if ( selectedWindowDimensionsPanel is not null )
{
selectedWindowDimensionsPanel.Visible = tool?.HasSelectedWindow == true;
}
}
private static void UpdateCheckbox( Checkbox checkbox, RoomLayoutTool tool, bool? value )
{
if ( checkbox is null )
{
return;
}
checkbox.Enabled = tool is not null;
if ( tool is not null && value.HasValue )
{
checkbox.State = value.Value ? CheckState.On : CheckState.Off;
}
}
private static void UpdateFloatEdit( LineEdit edit, RoomLayoutTool tool, float? value )
{
if ( edit is null )
{
return;
}
edit.ReadOnly = tool is null;
if ( tool is not null && value.HasValue && !edit.IsFocused )
{
edit.Value = value.Value.ToString( "0.###", CultureInfo.InvariantCulture );
}
}
private static void UpdateMaterialScaleEdit( MaterialButtonKey key, LineEdit edit, RoomLayoutTool tool )
{
if ( edit is null )
{
return;
}
var thresholdDisabled = key.Slot == RoomLayoutMaterialSlot.Threshold && tool?.ThresholdsEnabled == false;
var roofDisabled = key.Slot == RoomLayoutMaterialSlot.Roof && tool?.RoofEnabled == false;
var enabled = tool is not null &&
!thresholdDisabled &&
!roofDisabled &&
(key.Scope == MaterialScope.Defaults || tool.CanSetSelectedMaterialSlot( key.Slot ));
edit.ReadOnly = !enabled;
if ( tool is null || edit.IsFocused )
{
return;
}
if ( key.Scope == MaterialScope.Defaults )
{
edit.PlaceholderText = "128";
edit.Value = tool.GetMaterialScale( key.Slot ).ToString( "0.###", CultureInfo.InvariantCulture );
edit.ToolTip = "World units before this material repeats.";
return;
}
var selectedScale = tool.GetSelectedMaterialScale( key.Slot );
var inheritedScale = tool.GetEffectiveSelectedMaterialScale( key.Slot );
edit.PlaceholderText = inheritedScale.ToString( "0.###", CultureInfo.InvariantCulture );
edit.Value = selectedScale > 0.0f
? selectedScale.ToString( "0.###", CultureInfo.InvariantCulture )
: "";
edit.ToolTip = selectedScale > 0.0f
? "World units before this material repeats."
: "Clear to inherit the default material scale.";
}
private static void SetFloatEditEditable( LineEdit edit, bool editable )
{
if ( edit is not null )
{
edit.ReadOnly = !editable;
}
}
private RoomLayoutTool GetTool()
{
if ( fixedTool is { IsLive: true } )
{
return fixedTool;
}
return RoomLayoutTool.LiveTool;
}
private static Layout AddRow( Widget parent )
{
var row = parent.Layout.AddRow( 0 );
row.Spacing = 8;
row.Alignment = TextFlag.LeftCenter;
return row;
}
private SectionWidget AddSection( Widget parent, string title, int contentRows )
{
var section = new SectionWidget( parent );
section.SetSizeMode( SizeMode.Flexible, SizeMode.CanShrink );
section.Layout = Layout.Column();
section.Layout.Alignment = TextFlag.LeftTop;
section.Layout.Margin = 10;
section.Layout.Spacing = 8;
section.FixedHeight = SectionHeight( contentRows );
parent.Layout.Add( section, 0 );
var label = new Label( title, section );
label.SetStyles( "font-weight: 600; color: #f2f2f2;" );
section.Layout.Add( label, 0 );
return section;
}
private static float SectionHeight( int contentRows )
{
var rowCount = Math.Max( 1, contentRows + 1 );
var gaps = Math.Max( 0, rowCount - 1 );
return SectionPaddingY + Theme.RowHeight * rowCount + SectionSpacing * gaps;
}
private Button AddButton( Layout row, string label, string icon, Action action, int fixedWidth = ActionButtonWidth )
{
var button = row.Add( new RoomLayoutButton( label, this ) );
button.Icon = icon;
button.FixedWidth = fixedWidth;
button.FixedHeight = Theme.RowHeight;
button.Clicked += action;
return button;
}
private LineEdit AddCompactFloatEdit( Layout row, string labelText, string placeholder, Action action, int labelWidth = FieldLabelWidth )
{
var label = row.Add( new Label( labelText, this ) );
label.FixedWidth = labelWidth;
var edit = row.Add( new LineEdit( this ) );
edit.FixedWidth = NumberEditWidth;
edit.FixedHeight = Theme.RowHeight;
edit.PlaceholderText = placeholder;
edit.EditingFinished += action;
edit.ReturnPressed += action;
return edit;
}
private Checkbox AddCompactCheckbox( Layout row, string labelText, Action action, int labelWidth = FieldLabelWidth )
{
var label = row.Add( new Label( labelText, this ) );
label.FixedWidth = labelWidth;
var checkbox = row.Add( new Checkbox( this ) );
checkbox.FixedWidth = CheckboxWidth;
checkbox.Clicked += action;
return checkbox;
}
private void AddSettingsTabsSection( Widget parent )
{
var tabRow = AddRow( parent );
settingsTabs = tabRow.Add( new SegmentedControl( parent ) );
settingsTabs.FixedWidth = 608;
settingsTabs.AddOption( "Wall", "texture" );
settingsTabs.AddOption( "Doors", "door_front" );
settingsTabs.AddOption( "Windows", "window" );
settingsTabs.AddOption( "Baseboards", "border_bottom" );
settingsTabs.AddOption( "Thresholds", "door_front" );
settingsTabs.AddOption( "Roof", "roofing" );
settingsTabs.Selected = SettingsTabLabel( activeSettingsTab );
settingsTabs.OnSelectedChanged = OnSettingsTabChanged;
wallSettingsPanel = AddSettingsPanel( parent );
var wallRow = AddRow( wallSettingsPanel );
wallHeightEdit = AddCompactFloatEdit( wallRow, "Wall Height", "128", OnWallHeightEdited );
doorSettingsPanel = AddSettingsPanel( parent );
var doorSizeRow = AddRow( doorSettingsPanel );
doorWidthEdit = AddCompactFloatEdit( doorSizeRow, "Default Door Width", "64", OnDoorWidthEdited, LongFieldLabelWidth );
doorHeightEdit = AddCompactFloatEdit( doorSizeRow, "Default Door Height", "96", OnDoorHeightEdited, LongFieldLabelWidth );
var doorFrameRow = AddRow( doorSettingsPanel );
doorFrameThicknessEdit = AddCompactFloatEdit( doorFrameRow, "Door Frame Thickness", "8", OnDoorFrameThicknessEdited, LongFieldLabelWidth );
windowSettingsPanel = AddSettingsPanel( parent );
var windowSizeRow = AddRow( windowSettingsPanel );
windowWidthEdit = AddCompactFloatEdit( windowSizeRow, "Default Window Width", "64", OnWindowWidthEdited, LongFieldLabelWidth );
windowHeightEdit = AddCompactFloatEdit( windowSizeRow, "Default Window Height", "64", OnWindowHeightEdited, LongFieldLabelWidth );
var windowDetailRow = AddRow( windowSettingsPanel );
windowSillHeightEdit = AddCompactFloatEdit( windowDetailRow, "Default Sill Height", "48", OnWindowSillHeightEdited, LongFieldLabelWidth );
windowFrameThicknessEdit = AddCompactFloatEdit( windowDetailRow, "Window Frame Thickness", "2", OnWindowFrameThicknessEdited, LongFieldLabelWidth );
baseboardSettingsPanel = AddSettingsPanel( parent );
var baseboardToggleRow = AddRow( baseboardSettingsPanel );
baseboardsCheckbox = AddCompactCheckbox( baseboardToggleRow, "Baseboards", OnBaseboardsToggled );
var baseboardSizeRow = AddRow( baseboardSettingsPanel );
baseboardHeightEdit = AddCompactFloatEdit( baseboardSizeRow, "Baseboard Height", "4", OnBaseboardHeightEdited );
baseboardDepthEdit = AddCompactFloatEdit( baseboardSizeRow, "Baseboard Depth", "2", OnBaseboardDepthEdited );
thresholdSettingsPanel = AddSettingsPanel( parent );
var thresholdToggleRow = AddRow( thresholdSettingsPanel );
thresholdsCheckbox = AddCompactCheckbox( thresholdToggleRow, "Thresholds", OnThresholdsToggled );
var thresholdSizeRow = AddRow( thresholdSettingsPanel );
thresholdDepthEdit = AddCompactFloatEdit( thresholdSizeRow, "Threshold Depth", "12", OnThresholdDepthEdited );
roofSettingsPanel = AddSettingsPanel( parent );
var roofToggleRow = AddRow( roofSettingsPanel );
roofCheckbox = AddCompactCheckbox( roofToggleRow, "Roof", OnRoofToggled );
var roofSizeRow = AddRow( roofSettingsPanel );
roofThicknessEdit = AddCompactFloatEdit( roofSizeRow, "Roof Thickness", "4", OnRoofThicknessEdited );
UpdateSettingsTabVisibility();
}
private void AddDefaultMaterialsSection( Widget parent )
{
defaultMaterialsPanel = AddSection( parent, "Default Materials", contentRows: 10 );
defaultMaterialsPanel.Visible = false;
AddMaterialPicker( defaultMaterialsPanel, "Floor", RoomLayoutMaterialSlot.Floor, MaterialScope.Defaults );
AddMaterialPicker( defaultMaterialsPanel, "Outer Wall", RoomLayoutMaterialSlot.OuterWall, MaterialScope.Defaults );
AddMaterialPicker( defaultMaterialsPanel, "Inner Wall", RoomLayoutMaterialSlot.InnerWall, MaterialScope.Defaults );
AddMaterialPicker( defaultMaterialsPanel, "Wall Cap", RoomLayoutMaterialSlot.WallCap, MaterialScope.Defaults );
AddMaterialPicker( defaultMaterialsPanel, "Baseboard", RoomLayoutMaterialSlot.Baseboard, MaterialScope.Defaults );
AddMaterialPicker( defaultMaterialsPanel, "Door Frame", RoomLayoutMaterialSlot.DoorFrame, MaterialScope.Defaults );
AddMaterialPicker( defaultMaterialsPanel, "Window Frame", RoomLayoutMaterialSlot.WindowFrame, MaterialScope.Defaults );
AddMaterialPicker( defaultMaterialsPanel, "Corridor Floor", RoomLayoutMaterialSlot.CorridorFloor, MaterialScope.Defaults );
AddMaterialPicker( defaultMaterialsPanel, "Threshold", RoomLayoutMaterialSlot.Threshold, MaterialScope.Defaults );
AddMaterialPicker( defaultMaterialsPanel, "Roof", RoomLayoutMaterialSlot.Roof, MaterialScope.Defaults );
}
private void OnSettingsTabChanged( string selected )
{
if ( TryParseSettingsTab( selected, out var tab ) )
{
SetSettingsTab( tab );
}
}
private Widget AddSettingsPanel( Widget parent )
{
var panel = new Widget( parent );
panel.Layout = Layout.Column();
panel.Layout.Spacing = 8;
parent.Layout.Add( panel, 0 );
return panel;
}
private void SetSettingsTab( SettingsTab tab )
{
activeSettingsTab = tab;
if ( settingsTabs is not null )
{
settingsTabs.Selected = SettingsTabLabel( tab );
}
UpdateSettingsTabVisibility();
}
private void UpdateSettingsTabVisibility()
{
if ( wallSettingsPanel is null )
{
return;
}
wallSettingsPanel.Visible = activeSettingsTab == SettingsTab.Wall;
doorSettingsPanel.Visible = activeSettingsTab == SettingsTab.Doors;
windowSettingsPanel.Visible = activeSettingsTab == SettingsTab.Windows;
baseboardSettingsPanel.Visible = activeSettingsTab == SettingsTab.Baseboards;
thresholdSettingsPanel.Visible = activeSettingsTab == SettingsTab.Thresholds;
roofSettingsPanel.Visible = activeSettingsTab == SettingsTab.Roof;
}
private void AddSelectedSection( Widget parent )
{
var selectedRow = AddRow( parent );
selectedLayout = selectedRow.Add( new Label( "Selected: none", parent ) );
selectedLayout.FixedWidth = SelectedLabelWidth;
selectLayoutButton = AddButton( selectedRow, "Select...", "ads_click", ShowLayoutSelectionMenu, SmallActionButtonWidth );
deleteSelectedButton = AddButton( selectedRow, "Delete", "delete", OnDeleteSelectedClicked, SmallActionButtonWidth );
ApplySubtleDangerStyle( deleteSelectedButton );
deleteSelectedButton.ToolTip = "Delete the selected room, door, window, or corridor.";
selectedDimensionsLabel = new Label( "Dimensions", parent );
selectedDimensionsLabel.SetStyles( "font-weight: 600; color: #f2f2f2;" );
parent.Layout.Add( selectedDimensionsLabel, 0 );
selectedDoorDimensionsPanel = AddSettingsPanel( parent );
var selectedDoorRow = AddRow( selectedDoorDimensionsPanel );
selectedDoorWidthEdit = AddCompactFloatEdit( selectedDoorRow, "Door Width", "64", OnSelectedDoorWidthEdited );
selectedDoorHeightEdit = AddCompactFloatEdit( selectedDoorRow, "Door Height", "96", OnSelectedDoorHeightEdited );
selectedWindowDimensionsPanel = AddSettingsPanel( parent );
var selectedWindowSizeRow = AddRow( selectedWindowDimensionsPanel );
selectedWindowWidthEdit = AddCompactFloatEdit( selectedWindowSizeRow, "Window Width", "64", OnSelectedWindowWidthEdited );
selectedWindowHeightEdit = AddCompactFloatEdit( selectedWindowSizeRow, "Window Height", "64", OnSelectedWindowHeightEdited );
var selectedWindowSillRow = AddRow( selectedWindowDimensionsPanel );
selectedWindowSillHeightEdit = AddCompactFloatEdit( selectedWindowSillRow, "Window Sill Height", "48", OnSelectedWindowSillHeightEdited, LongFieldLabelWidth );
selectedDimensionsLabel.Visible = false;
selectedDoorDimensionsPanel.Visible = false;
selectedWindowDimensionsPanel.Visible = false;
var materialLabel = new Label( "Material Overrides", parent );
materialLabel.SetStyles( "font-weight: 600; color: #f2f2f2;" );
parent.Layout.Add( materialLabel, 0 );
AddMaterialPicker( parent, "Floor", RoomLayoutMaterialSlot.Floor, MaterialScope.Selected );
AddMaterialPicker( parent, "Outer Wall", RoomLayoutMaterialSlot.OuterWall, MaterialScope.Selected );
AddMaterialPicker( parent, "Inner Wall", RoomLayoutMaterialSlot.InnerWall, MaterialScope.Selected );
AddMaterialPicker( parent, "Wall Cap", RoomLayoutMaterialSlot.WallCap, MaterialScope.Selected );
AddMaterialPicker( parent, "Baseboard", RoomLayoutMaterialSlot.Baseboard, MaterialScope.Selected );
AddMaterialPicker( parent, "Threshold", RoomLayoutMaterialSlot.Threshold, MaterialScope.Selected );
AddMaterialPicker( parent, "Door Frame", RoomLayoutMaterialSlot.DoorFrame, MaterialScope.Selected );
AddMaterialPicker( parent, "Window Frame", RoomLayoutMaterialSlot.WindowFrame, MaterialScope.Selected );
AddMaterialPicker( parent, "Roof", RoomLayoutMaterialSlot.Roof, MaterialScope.Selected );
}
private void AddResetLayoutSection( Widget parent )
{
var resetRow = AddRow( parent );
resetLayoutButton = AddButton( resetRow, "Reset Layout", "delete_forever", OnClearLayoutClicked, DangerButtonWidth );
resetLayoutButton.ToolTip = "Delete the editable layout for the active scene.";
ApplyDangerStyle( resetLayoutButton );
}
private void AddMaterialPicker( Widget parent, string label, RoomLayoutMaterialSlot slot, MaterialScope scope )
{
var row = AddFieldRow( parent, label );
var key = new MaterialButtonKey( scope, slot );
var button = row.Add( new RoomLayoutButton( "Pick", this ) );
button.Icon = "texture";
button.FixedWidth = MaterialButtonWidth;
button.FixedHeight = Theme.RowHeight;
button.Clicked += () => ShowMaterialMenu( key );
materialButtons[key] = button;
var scaleLabel = row.Add( new Label( "Scale", this ) );
scaleLabel.FixedWidth = MaterialScaleLabelWidth;
var scaleEdit = row.Add( new LineEdit( this ) );
scaleEdit.FixedWidth = MaterialScaleEditWidth;
scaleEdit.FixedHeight = Theme.RowHeight;
scaleEdit.EditingFinished += () => OnMaterialScaleEdited( key );
scaleEdit.ReturnPressed += () => OnMaterialScaleEdited( key );
materialScaleEdits[key] = scaleEdit;
}
private Layout AddFieldRow( Widget parent, string labelText )
{
var row = parent.Layout.AddRow( 0 );
row.Spacing = 8;
row.Alignment = TextFlag.LeftCenter;
var label = row.Add( new Label( labelText, this ) );
label.FixedWidth = MaterialLabelWidth;
return row;
}
private static void ApplySubtleDangerStyle( Button button )
{
button.Tint = Theme.Red.WithAlpha( 0.7f );
}
private static void ApplyDangerStyle( Button button )
{
button.Tint = Theme.Red;
}
private static Label AddStatusLabel( Widget parent )
{
var label = new Label( "", parent );
parent.Layout.Add( label, 0 );
return label;
}
private void ShowMaterialMenu( MaterialButtonKey key )
{
if ( GetTool() is null )
{
return;
}
var menu = new ContextMenu();
menu.AddOption( "Pick Material", "texture", () => OpenAssetPicker( key, AssetType.Material ) );
menu.AddSeparator();
var clearText = key.Scope == MaterialScope.Selected ? "Inherit Default" : "Clear";
menu.AddOption( clearText, "backspace", () => SetMaterialPath( key, "" ) );
var scaleText = key.Scope == MaterialScope.Selected ? "Inherit Scale" : "Reset Scale";
menu.AddOption( scaleText, "straighten", () => SetMaterialScale( key, 0.0f ) );
menu.OpenAtCursor( false );
}
private void ToggleDefaultMaterialsPanel()
{
if ( defaultMaterialsPanel is null || GetTool() is null )
{
return;
}
defaultMaterialsPanel.Visible = !defaultMaterialsPanel.Visible;
}
private void ShowLayoutSelectionMenu()
{
var tool = GetTool();
if ( tool is null )
{
return;
}
var menu = new ContextMenu();
foreach ( var option in tool.GetSelectionOptions() )
{
var captured = option;
menu.AddOption( captured.Name, captured.Icon, () => tool.SelectLayout( captured.Kind, captured.Id ) );
}
menu.OpenAtCursor( false );
}
private void OpenAssetPicker( MaterialButtonKey key, AssetType assetType )
{
var picker = AssetPicker.Create( this, assetType, new AssetPicker.PickerOptions
{
EnableCloud = true,
EnableMultiselect = false
} );
picker.Title = $"Select {assetType.FriendlyName}";
picker.SetSelection( CurrentMaterialPath( key ) );
picker.OnAssetPicked = assets => SetMaterialFromAsset( key, assets.FirstOrDefault() );
picker.Show();
}
private async void SetMaterialFromAsset( MaterialButtonKey key, Asset asset )
{
if ( asset is null )
{
return;
}
var path = asset.RelativePath;
if ( asset.Package is { } package && AssetSystem.CanCloudInstall( package ) && !AssetSystem.IsCloudInstalled( package ) )
{
var installedAsset = await AssetSystem.InstallAsync( package.FullIdent );
if ( installedAsset is not null )
{
asset = installedAsset;
path = asset.RelativePath;
}
}
path = NormalizeAssetPath( path );
await WaitForMaterialAssetReady( asset, path );
RoomLayoutGeometryBuilder.InvalidateMaterial( path );
var tool = GetTool();
var samePath = tool is not null &&
string.Equals( MaterialPathForButton( tool, key ), path, StringComparison.OrdinalIgnoreCase );
SetMaterialPath( key, path );
if ( samePath )
{
tool.RefreshGeneratedGeometry();
}
}
private static async Task WaitForMaterialAssetReady( Asset asset, string path )
{
if ( string.IsNullOrWhiteSpace( path ) )
{
return;
}
for ( var attempt = 0; attempt < 20; attempt++ )
{
if ( MaterialAssetReady( asset, path ) )
{
return;
}
await Task.Delay( 100 );
}
}
private static bool MaterialAssetReady( Asset asset, string path )
{
try
{
var indexedAsset = asset ?? AssetSystem.FindByPath( path );
if ( indexedAsset is not null && !indexedAsset.IsCompiledAndUpToDate )
{
return false;
}
if ( global::Editor.FileSystem.Content.FileExists( path ) ||
Sandbox.FileSystem.Mounted.FileExists( path ) )
{
return true;
}
return indexedAsset?.Package is { } package
? AssetSystem.IsCloudInstalled( package )
: indexedAsset is not null;
}
catch
{
return false;
}
}
private static string NormalizeAssetPath( string path )
{
if ( string.IsNullOrWhiteSpace( path ) )
{
return "";
}
path = path.Trim().Replace( '\\', '/' ).TrimStart( '/' );
if ( path.StartsWith( "assets/", StringComparison.OrdinalIgnoreCase ) )
{
path = path[7..];
}
return path.EndsWith( ".vmat_c", StringComparison.OrdinalIgnoreCase )
? path[..^2]
: path;
}
private void SetMaterialPath( MaterialButtonKey key, string path )
{
var tool = GetTool();
if ( tool is null )
{
return;
}
if ( key.Scope == MaterialScope.Defaults )
{
tool.SetMaterialPath( key.Slot, path );
return;
}
tool.SetSelectedMaterialPath( key.Slot, path );
}
private void SetMaterialScale( MaterialButtonKey key, float scale )
{
var tool = GetTool();
if ( tool is null )
{
return;
}
if ( key.Scope == MaterialScope.Defaults )
{
tool.SetMaterialScale( key.Slot, scale );
return;
}
tool.SetSelectedMaterialScale( key.Slot, scale );
}
private string CurrentMaterialPath( MaterialButtonKey key )
{
var tool = GetTool();
return tool is null ? "" : MaterialPathForButton( tool, key );
}
private static string MaterialPathForButton( RoomLayoutTool tool, MaterialButtonKey key )
{
return key.Scope == MaterialScope.Defaults
? tool.GetMaterialPath( key.Slot )
: tool.GetSelectedMaterialPath( key.Slot );
}
private static string MaterialButtonText( string path, bool allowInherit )
{
if ( string.IsNullOrWhiteSpace( path ) )
{
return allowInherit ? "Inherit" : "Built-in default";
}
var asset = AssetSystem.FindByPath( path );
if ( asset is not null )
{
return asset.Name;
}
var normalized = path.Replace( '\\', '/' );
var fileName = normalized[(normalized.LastIndexOf( '/' ) + 1)..];
return System.IO.Path.GetFileNameWithoutExtension( fileName );
}
private static string MaterialButtonTooltip( string path, bool allowInherit )
{
if ( string.IsNullOrWhiteSpace( path ) )
{
return allowInherit
? "Uses the default material for this surface."
: "Uses the Interior Layout Builder built-in fallback material.";
}
return path;
}
private void OnRoomsClicked()
{
GetTool()?.SetMode( RoomLayoutToolMode.Rooms );
}
private void OnSelectClicked()
{
GetTool()?.SetMode( RoomLayoutToolMode.Select );
}
private void OnDoorsClicked()
{
GetTool()?.SetMode( RoomLayoutToolMode.Doors );
}
private void OnCorridorsClicked()
{
GetTool()?.SetMode( RoomLayoutToolMode.Corridors );
}
private void OnWindowsClicked()
{
GetTool()?.SetMode( RoomLayoutToolMode.Windows );
}
private void OnFloorCutoutsClicked()
{
GetTool()?.SetMode( RoomLayoutToolMode.FloorCutouts );
}
private void OnBuildClicked()
{
GetTool()?.RebuildGeneratedGeometry();
}
private void OnClearGeneratedClicked()
{
GetTool()?.ClearGeneratedGeometry();
}
private void OnDeleteSelectedClicked()
{
GetTool()?.DeleteSelectedLayout();
}
private void OnClearLayoutClicked()
{
GetTool()?.ClearLayout();
}
private void OnMaterialScaleEdited( MaterialButtonKey key )
{
if ( !materialScaleEdits.TryGetValue( key, out var edit ) )
{
return;
}
if ( string.IsNullOrWhiteSpace( edit.Value ) )
{
SetMaterialScale( key, 0.0f );
return;
}
if ( TryReadFloatEdit( edit, out var scale ) )
{
SetMaterialScale( key, scale );
}
}
private void OnWallHeightEdited()
{
if ( TryReadFloatEdit( wallHeightEdit, out var height ) )
{
GetTool()?.SetWallHeight( height );
}
}
private void OnWindowWidthEdited()
{
if ( TryReadFloatEdit( windowWidthEdit, out var width ) )
{
GetTool()?.SetWindowWidth( width );
}
}
private void OnDoorWidthEdited()
{
if ( TryReadFloatEdit( doorWidthEdit, out var width ) )
{
GetTool()?.SetDoorWidth( width );
}
}
private void OnDoorHeightEdited()
{
if ( TryReadFloatEdit( doorHeightEdit, out var height ) )
{
GetTool()?.SetDoorHeight( height );
}
}
private void OnDoorFrameThicknessEdited()
{
if ( TryReadFloatEdit( doorFrameThicknessEdit, out var thickness ) )
{
GetTool()?.SetDoorFrameThickness( thickness );
}
}
private void OnSelectedDoorWidthEdited()
{
if ( TryReadFloatEdit( selectedDoorWidthEdit, out var width ) )
{
GetTool()?.SetSelectedDoorWidth( width );
}
}
private void OnSelectedDoorHeightEdited()
{
if ( TryReadFloatEdit( selectedDoorHeightEdit, out var height ) )
{
GetTool()?.SetSelectedDoorHeight( height );
}
}
private void OnSelectedWindowWidthEdited()
{
if ( TryReadFloatEdit( selectedWindowWidthEdit, out var width ) )
{
GetTool()?.SetSelectedWindowWidth( width );
}
}
private void OnSelectedWindowHeightEdited()
{
if ( TryReadFloatEdit( selectedWindowHeightEdit, out var height ) )
{
GetTool()?.SetSelectedWindowHeight( height );
}
}
private void OnSelectedWindowSillHeightEdited()
{
if ( TryReadFloatEdit( selectedWindowSillHeightEdit, out var height ) )
{
GetTool()?.SetSelectedWindowSillHeight( height );
}
}
private void OnWindowHeightEdited()
{
if ( TryReadFloatEdit( windowHeightEdit, out var height ) )
{
GetTool()?.SetWindowHeight( height );
}
}
private void OnWindowSillHeightEdited()
{
if ( TryReadFloatEdit( windowSillHeightEdit, out var height ) )
{
GetTool()?.SetWindowSillHeight( height );
}
}
private void OnBaseboardsToggled()
{
GetTool()?.SetBaseboardsEnabled( baseboardsCheckbox.State == CheckState.On );
}
private void OnThresholdsToggled()
{
GetTool()?.SetThresholdsEnabled( thresholdsCheckbox.State == CheckState.On );
}
private void OnRoofToggled()
{
GetTool()?.SetRoofEnabled( roofCheckbox.State == CheckState.On );
}
private void OnBaseboardHeightEdited()
{
if ( TryReadFloatEdit( baseboardHeightEdit, out var height ) )
{
GetTool()?.SetBaseboardHeight( height );
}
}
private void OnBaseboardDepthEdited()
{
if ( TryReadFloatEdit( baseboardDepthEdit, out var depth ) )
{
GetTool()?.SetBaseboardDepth( depth );
}
}
private void OnThresholdDepthEdited()
{
if ( TryReadFloatEdit( thresholdDepthEdit, out var depth ) )
{
GetTool()?.SetThresholdDepth( depth );
}
}
private void OnRoofThicknessEdited()
{
if ( TryReadFloatEdit( roofThicknessEdit, out var thickness ) )
{
GetTool()?.SetRoofThickness( thickness );
}
}
private static bool TryReadFloatEdit( LineEdit edit, out float value )
{
value = 0.0f;
var text = edit?.Value?.Trim();
return !string.IsNullOrEmpty( text ) &&
(float.TryParse( text, NumberStyles.Float, CultureInfo.InvariantCulture, out value ) ||
float.TryParse( text, NumberStyles.Float, CultureInfo.CurrentCulture, out value ));
}
private enum MaterialScope
{
Defaults,
Selected
}
private enum SettingsTab
{
Wall,
Doors,
Windows,
Baseboards,
Thresholds,
Roof
}
private static string SettingsTabLabel( SettingsTab tab )
{
return tab switch
{
SettingsTab.Wall => "Wall",
SettingsTab.Doors => "Doors",
SettingsTab.Windows => "Windows",
SettingsTab.Baseboards => "Baseboards",
SettingsTab.Thresholds => "Thresholds",
SettingsTab.Roof => "Roof",
_ => "Wall"
};
}
private static bool TryParseSettingsTab( string label, out SettingsTab tab )
{
tab = label switch
{
"Wall" => SettingsTab.Wall,
"Doors" => SettingsTab.Doors,
"Windows" => SettingsTab.Windows,
"Baseboards" => SettingsTab.Baseboards,
"Thresholds" => SettingsTab.Thresholds,
"Roof" => SettingsTab.Roof,
_ => SettingsTab.Wall
};
return label is "Wall" or "Doors" or "Windows" or "Baseboards" or "Thresholds" or "Roof";
}
private sealed class RoomLayoutButton : Button
{
public RoomLayoutButton( string title, Widget parent ) : base( title, parent )
{
}
protected override void OnPaint()
{
var color = Tint.ToHsv();
var background = color;
if ( Enabled )
{
if ( Paint.HasPressed )
{
background = color with { Value = color.Value + 0.1f };
}
else if ( Paint.HasMouseOver )
{
background = color with { Value = color.Value + 0.2f };
}
}
else
{
background = color = Theme.SurfaceLightBackground;
}
if ( !Enabled || ReadOnly )
{
color = color.WithSaturation( 0.1f ).WithAlpha( 0.5f );
background = color.WithAlpha( 0.2f );
}
if ( background.Alpha > 0 )
{
Paint.Antialiasing = true;
Paint.ClearPen();
Paint.SetBrush( background with { Value = background.Value + 0.04f, Saturation = color.Saturation * 0.8f } );
Paint.DrawRect( LocalRect, 3 );
Paint.SetBrushLinear( LocalRect.TopLeft, LocalRect.BottomRight, background, background with { Value = background.Value - 0.03f } );
Paint.DrawRect( LocalRect.Shrink( 1, 1, 1, 1 ), 3 );
}
else
{
color = Color.White.WithAlpha( 0.5f );
}
Paint.SetDefaultFont();
Paint.SetPen( color with { Value = 0.99f, Saturation = color.Saturation * 0.20f } );
var rect = LocalRect.Shrink( 8, 0, 8, 0 );
if ( !string.IsNullOrWhiteSpace( Icon ) )
{
var iconRect = new Rect( rect.Left, rect.Center.y - 8, 16, 16 );
Paint.DrawIcon( iconRect, Icon, 16 );
rect.Left += 24;
}
Paint.DrawText( rect, Text, TextFlag.LeftCenter );
}
}
private sealed class SectionWidget : Widget
{
public SectionWidget( Widget parent ) : base( parent )
{
}
protected override void OnPaint()
{
var rect = LocalRect;
var fill = new Color( 0.155f, 0.155f, 0.155f, 1.0f );
var border = new Color( 0.255f, 0.255f, 0.255f, 1.0f );
Paint.Antialiasing = true;
Paint.ClearPen();
Paint.SetBrush( fill );
Paint.DrawRect( rect, 3 );
Paint.SetBrush( border );
Paint.DrawRect( new Rect( rect.Left, rect.Top, rect.Width, 1 ), 0 );
Paint.DrawRect( new Rect( rect.Left, rect.Bottom - 1, rect.Width, 1 ), 0 );
Paint.DrawRect( new Rect( rect.Left, rect.Top, 1, rect.Height ), 0 );
Paint.DrawRect( new Rect( rect.Right - 1, rect.Top, 1, rect.Height ), 0 );
}
}
private readonly record struct MaterialButtonKey( MaterialScope Scope, RoomLayoutMaterialSlot Slot );
}