diff --git a/Resources/EnginePrototypes/UserInterface/uiThemes.yml b/Resources/EnginePrototypes/UserInterface/uiThemes.yml new file mode 100644 index 000000000..f3acc74f4 --- /dev/null +++ b/Resources/EnginePrototypes/UserInterface/uiThemes.yml @@ -0,0 +1,3 @@ +- type: uiTheme + id: Default + path: /textures/interface/Default diff --git a/Robust.Client/ClientIoC.cs b/Robust.Client/ClientIoC.cs index 267a5a1d7..4a3e3c712 100644 --- a/Robust.Client/ClientIoC.cs +++ b/Robust.Client/ClientIoC.cs @@ -18,6 +18,7 @@ using Robust.Client.ResourceManagement; using Robust.Client.State; using Robust.Client.Timing; using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Themes; using Robust.Client.Utility; using Robust.Client.ViewVariables; using Robust.Shared; diff --git a/Robust.Client/GameController/GameController.cs b/Robust.Client/GameController/GameController.cs index 8f6765b68..6110ab2a2 100644 --- a/Robust.Client/GameController/GameController.cs +++ b/Robust.Client/GameController/GameController.cs @@ -13,6 +13,7 @@ using Robust.Client.Placement; using Robust.Client.ResourceManagement; using Robust.Client.State; using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Themes; using Robust.Client.Utility; using Robust.Client.ViewVariables; using Robust.Client.WebViewHook; @@ -128,10 +129,7 @@ namespace Robust.Client // Call Init in game assemblies. _modLoader.BroadcastRunLevel(ModRunLevel.PreInit); _modLoader.BroadcastRunLevel(ModRunLevel.Init); - _resourceCache.PreloadTextures(); - _userInterfaceManager.Initialize(); - _eyeManager.Initialize(); _networkManager.Initialize(false); IoCManager.Resolve().SetupNetworking(); _serializer.Initialize(); @@ -141,16 +139,18 @@ namespace Robust.Client _prototypeManager.LoadDirectory(new ResourcePath("/EnginePrototypes/")); _prototypeManager.LoadDirectory(Options.PrototypeDirectory); _prototypeManager.ResolveResults(); + _userInterfaceManager.Initialize(); + _eyeManager.Initialize(); _entityManager.Initialize(); _mapManager.Initialize(); _gameStateManager.Initialize(); _placementManager.Initialize(); _viewVariablesManager.Initialize(); _scriptClient.Initialize(); - _client.Initialize(); _discord.Initialize(); _modLoader.BroadcastRunLevel(ModRunLevel.PostInit); + _userInterfaceManager.PostInitialize(); if (_commandLineArgs?.Username != null) { diff --git a/Robust.Client/Player/LocalPlayer.cs b/Robust.Client/Player/LocalPlayer.cs index 941bcee1f..75e463558 100644 --- a/Robust.Client/Player/LocalPlayer.cs +++ b/Robust.Client/Player/LocalPlayer.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using Robust.Client.GameObjects; using Robust.Shared.Enums; using Robust.Shared.GameObjects; @@ -91,10 +92,6 @@ namespace Robust.Client.Player !metaData.EntityDeleted) { entMan.GetComponent(previous.Value).Current = false; - - // notify ECS Systems - entMan.EventBus.RaiseEvent(EventSource.Local, new PlayerAttachSysMessage(default)); - entMan.EventBus.RaiseLocalEvent(previous.Value, new PlayerDetachedEvent(previous.Value), true); } ControlledEntity = null; @@ -102,6 +99,8 @@ namespace Robust.Client.Player if (previous != null) { + entMan.EventBus.RaiseEvent(EventSource.Local, new PlayerAttachSysMessage(default)); + entMan.EventBus.RaiseLocalEvent(previous.Value, new PlayerDetachedEvent(previous.Value), true); EntityDetached?.Invoke(new EntityDetachedEventArgs(previous.Value)); } } diff --git a/Robust.Client/Player/PlayerManager.cs b/Robust.Client/Player/PlayerManager.cs index 561c50403..77745d8ff 100644 --- a/Robust.Client/Player/PlayerManager.cs +++ b/Robust.Client/Player/PlayerManager.cs @@ -87,6 +87,7 @@ namespace Robust.Client.Player /// public void Startup() { + DebugTools.Assert(LocalPlayer == null); LocalPlayer = new LocalPlayer(); var msgList = new MsgPlayerListReq(); @@ -97,6 +98,7 @@ namespace Robust.Client.Player /// public void Shutdown() { + LocalPlayer?.DetachEntity(); LocalPlayer = null; _sessions.Clear(); } diff --git a/Robust.Client/State/DefaultState.cs b/Robust.Client/State/DefaultState.cs index 672a92a26..7bd5f7fc1 100644 --- a/Robust.Client/State/DefaultState.cs +++ b/Robust.Client/State/DefaultState.cs @@ -3,11 +3,11 @@ namespace Robust.Client.State // Dummy state that is only used to make sure there always is *a* state. public sealed class DefaultState : State { - public override void Startup() + protected override void Startup() { } - public override void Shutdown() + protected override void Shutdown() { } } diff --git a/Robust.Client/State/State.cs b/Robust.Client/State/State.cs index 34fd41a48..a92688347 100644 --- a/Robust.Client/State/State.cs +++ b/Robust.Client/State/State.cs @@ -1,18 +1,45 @@ +using System; +using Robust.Client.UserInterface; +using Robust.Shared.IoC; using Robust.Shared.Timing; namespace Robust.Client.State { public abstract class State { - /// - /// Screen is being (re)enabled. - /// - public abstract void Startup(); + //[Optional] The UIScreen attached to this gamestate + protected virtual Type? LinkedScreenType => null; /// - /// Screen is being disabled (NOT Destroyed). + /// Game switching to this state /// - public abstract void Shutdown(); + + internal void StartupInternal(IUserInterfaceManager userInterfaceManager) + { + if (LinkedScreenType != null) + { + if (!LinkedScreenType.IsAssignableTo(typeof(UIScreen))) throw new Exception("Linked Screen type is invalid"); + userInterfaceManager.LoadScreenInternal(LinkedScreenType); + } + Startup(); + } + + protected abstract void Startup(); + + /// + /// Game switching away from this state + /// + + internal void ShutdownInternal(IUserInterfaceManager userInterfaceManager) + { + if (LinkedScreenType != null) + { + userInterfaceManager.UnloadScreen(); + } + Shutdown(); + } + + protected abstract void Shutdown(); public virtual void FrameUpdate(FrameEventArgs e) { } } diff --git a/Robust.Client/State/StateManager.cs b/Robust.Client/State/StateManager.cs index f5ede4315..83cdf31c2 100644 --- a/Robust.Client/State/StateManager.cs +++ b/Robust.Client/State/StateManager.cs @@ -1,4 +1,5 @@ using System; +using Robust.Client.UserInterface; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Timing; @@ -8,7 +9,7 @@ namespace Robust.Client.State internal sealed class StateManager : IStateManager { [Dependency] private readonly IDynamicTypeFactory _typeFactory = default!; - + [Dependency] private readonly IUserInterfaceManager _interfaceManager = default!; public event Action? OnStateChanged; public State CurrentState { get; private set; } @@ -42,10 +43,10 @@ namespace Robust.Client.State var newState = _typeFactory.CreateInstance(type); var old = CurrentState; - CurrentState?.Shutdown(); + CurrentState?.ShutdownInternal(_interfaceManager); CurrentState = newState; - CurrentState.Startup(); + CurrentState.StartupInternal(_interfaceManager); OnStateChanged?.Invoke(new StateChangedEventArgs(old, CurrentState)); diff --git a/Robust.Client/UserInterface/Control.cs b/Robust.Client/UserInterface/Control.cs index e9f822ee5..9f5f7710e 100644 --- a/Robust.Client/UserInterface/Control.cs +++ b/Robust.Client/UserInterface/Control.cs @@ -6,6 +6,7 @@ using Avalonia.Metadata; using JetBrains.Annotations; using Robust.Client.Graphics; using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.Themes; using Robust.Client.UserInterface.XAML; using Robust.Shared.Animations; using Robust.Shared.IoC; @@ -69,6 +70,22 @@ namespace Robust.Client.UserInterface // _nameScope = nameScope; //} + public UITheme Theme { get; set; } + + protected virtual void OnThemeUpdated(){} + internal void ThemeUpdateRecursive() + { + var curTheme = IoCManager.Resolve().CurrentTheme; + if (Theme == curTheme) return; //don't update themes if the themes are up to date + Theme = curTheme; + OnThemeUpdated(); + foreach (var child in Children) + { + // Don't descent into children that have a style sheet since those aren't affected. + child.ThemeUpdateRecursive(); + } + } + public NameScope? FindNameScope() { foreach (var control in this.GetSelfAndLogicalAncestors()) @@ -453,6 +470,7 @@ namespace Robust.Client.UserInterface UserInterfaceManagerInternal = IoCManager.Resolve(); StyleClasses = new StyleClassCollection(this); Children = new OrderedChildCollection(this); + Theme = UserInterfaceManagerInternal.CurrentTheme; XamlChildren = Children; } diff --git a/Robust.Client/UserInterface/Controllers/IOnStateChanged.cs b/Robust.Client/UserInterface/Controllers/IOnStateChanged.cs new file mode 100644 index 000000000..cda4d566e --- /dev/null +++ b/Robust.Client/UserInterface/Controllers/IOnStateChanged.cs @@ -0,0 +1,10 @@ +namespace Robust.Client.UserInterface.Controllers; + +/// +/// Interface implemented by s +/// Implements both and +/// +/// The state type +public interface IOnStateChanged : IOnStateEntered, IOnStateExited where T : State.State +{ +} diff --git a/Robust.Client/UserInterface/Controllers/IOnStateEntered.cs b/Robust.Client/UserInterface/Controllers/IOnStateEntered.cs new file mode 100644 index 000000000..4e2e7b5c5 --- /dev/null +++ b/Robust.Client/UserInterface/Controllers/IOnStateEntered.cs @@ -0,0 +1,16 @@ +namespace Robust.Client.UserInterface.Controllers; + +/// +/// Interface implemented by s +/// +/// The state type +public interface IOnStateEntered where T : State.State +{ + /// + /// Called by + /// on s that implement this method when a state + /// of the specified type is entered + /// + /// The state that was entered + void OnStateEntered(T state); +} diff --git a/Robust.Client/UserInterface/Controllers/IOnStateExited.cs b/Robust.Client/UserInterface/Controllers/IOnStateExited.cs new file mode 100644 index 000000000..f753fc131 --- /dev/null +++ b/Robust.Client/UserInterface/Controllers/IOnStateExited.cs @@ -0,0 +1,16 @@ +namespace Robust.Client.UserInterface.Controllers; + +/// +/// Interface implemented by s +/// +/// The state type +public interface IOnStateExited where T : State.State +{ + /// + /// Called by + /// on s that implement this method when a state + /// of the specified type is exited + /// + /// The state that was exited + void OnStateExited(T state); +} diff --git a/Robust.Client/UserInterface/Controllers/IOnSystemChanged.cs b/Robust.Client/UserInterface/Controllers/IOnSystemChanged.cs new file mode 100644 index 000000000..7b8427ca7 --- /dev/null +++ b/Robust.Client/UserInterface/Controllers/IOnSystemChanged.cs @@ -0,0 +1,12 @@ +using Robust.Shared.GameObjects; + +namespace Robust.Client.UserInterface.Controllers; + +/// +/// Interface implemented by s +/// Implements both and +/// +/// The entity system type +public interface IOnSystemChanged : IOnSystemLoaded, IOnSystemUnloaded where T : IEntitySystem +{ +} diff --git a/Robust.Client/UserInterface/Controllers/IOnSystemLoaded.cs b/Robust.Client/UserInterface/Controllers/IOnSystemLoaded.cs new file mode 100644 index 000000000..f42bbff2a --- /dev/null +++ b/Robust.Client/UserInterface/Controllers/IOnSystemLoaded.cs @@ -0,0 +1,18 @@ +using Robust.Shared.GameObjects; + +namespace Robust.Client.UserInterface.Controllers; + +/// +/// Interface implemented by s +/// +/// The entity system type +public interface IOnSystemLoaded where T : IEntitySystem +{ + /// + /// Called by + /// on s that implement this method when a system + /// of the specified type is loaded + /// + /// The system that was loaded + void OnSystemLoaded(T system); +} diff --git a/Robust.Client/UserInterface/Controllers/IOnSystemUnloaded.cs b/Robust.Client/UserInterface/Controllers/IOnSystemUnloaded.cs new file mode 100644 index 000000000..4fb3a3b1a --- /dev/null +++ b/Robust.Client/UserInterface/Controllers/IOnSystemUnloaded.cs @@ -0,0 +1,18 @@ +using Robust.Shared.GameObjects; + +namespace Robust.Client.UserInterface.Controllers; + +/// +/// Interface implemented by s +/// +/// The entity system type +public interface IOnSystemUnloaded where T : IEntitySystem +{ + /// + /// Called by + /// on s that implement this method when a system + /// of the specified type is unloaded + /// + /// The system that was unloaded + void OnSystemUnloaded(T system); +} diff --git a/Robust.Client/UserInterface/Controllers/IUserInterfaceManager.Controllers.cs b/Robust.Client/UserInterface/Controllers/IUserInterfaceManager.Controllers.cs new file mode 100644 index 000000000..d69a381eb --- /dev/null +++ b/Robust.Client/UserInterface/Controllers/IUserInterfaceManager.Controllers.cs @@ -0,0 +1,9 @@ +using Robust.Client.UserInterface.Controllers; + +// ReSharper disable once CheckNamespace +namespace Robust.Client.UserInterface; + +public partial interface IUserInterfaceManager +{ + public T GetUIController() where T : UIController, new(); +} diff --git a/Robust.Client/UserInterface/Controllers/Implementations/EntitySpawningUIController.cs b/Robust.Client/UserInterface/Controllers/Implementations/EntitySpawningUIController.cs new file mode 100644 index 000000000..7c0fc40d8 --- /dev/null +++ b/Robust.Client/UserInterface/Controllers/Implementations/EntitySpawningUIController.cs @@ -0,0 +1,351 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Robust.Client.GameObjects; +using Robust.Client.Placement; +using Robust.Client.ResourceManagement; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.CustomControls; +using Robust.Shared.Enums; +using Robust.Shared.IoC; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; +using static Robust.Client.UserInterface.Controls.BaseButton; +using static Robust.Client.UserInterface.Controls.LineEdit; + +namespace Robust.Client.UserInterface.Controllers.Implementations; + +public sealed class EntitySpawningUIController : UIController +{ + [Dependency] private readonly IPlacementManager _placement = default!; + [Dependency] private readonly IPrototypeManager _prototypes = default!; + [Dependency] private readonly IResourceCache _resources = default!; + + private EntitySpawnWindow? _window; + private readonly List _shownEntities = new(); + private bool _init; + + public override void Initialize() + { + DebugTools.Assert(_init == false); + _init = true; + + _placement.DirectionChanged += OnDirectionChanged; + _placement.PlacementChanged += ClearSelection; + } + + // The indices of the visible prototypes last time UpdateVisiblePrototypes was ran. + // This is inclusive, so end is the index of the last prototype, not right after it. + private (int start, int end) _lastEntityIndices; + + private void OnEntityEraseToggled(ButtonToggledEventArgs args) + { + if (_window == null || _window.Disposed) + return; + + _placement.Clear(); + // Only toggle the eraser back if the button is pressed. + if(args.Pressed) + _placement.ToggleEraser(); + // clearing will toggle the erase button off... + args.Button.Pressed = args.Pressed; + _window.OverrideMenu.Disabled = args.Pressed; + } + + public void ToggleWindow() + { + EnsureWindow(); + + if (_window!.IsOpen) + { + _window.Close(); + } + else + { + _window.Open(); + UpdateEntityDirectionLabel(); + _window.SearchBar.GrabKeyboardFocus(); + } + } + + private void EnsureWindow() + { + if (_window is { Disposed: false }) + return; + + _window = UIManager.CreateWindow(); + LayoutContainer.SetAnchorPreset(_window,LayoutContainer.LayoutPreset.CenterLeft); + _window.OnClose += WindowClosed; + _window.EraseButton.Pressed = _placement.Eraser; + _window.EraseButton.OnToggled += OnEntityEraseToggled; + _window.OverrideMenu.OnItemSelected += OnEntityOverrideSelected; + _window.SearchBar.OnTextChanged += OnEntitySearchChanged; + _window.ClearButton.OnPressed += OnEntityClearPressed; + _window.PrototypeScrollContainer.OnScrolled += UpdateVisiblePrototypes; + _window.OnResized += UpdateVisiblePrototypes; + BuildEntityList(); + } + + public void CloseWindow() + { + if (_window == null || _window.Disposed) + return; + + _window?.Close(); + } + + private void WindowClosed() + { + if (_window == null || _window.Disposed) + return; + + if (_window.SelectedButton != null) + { + _window.SelectedButton.ActualButton.Pressed = false; + _window.SelectedButton = null; + } + + _placement.Clear(); + } + + private void ClearSelection(object? sender, EventArgs e) + { + if (_window == null || _window.Disposed) + return; + + if (_window.SelectedButton != null) + { + _window.SelectedButton.ActualButton.Pressed = false; + _window.SelectedButton = null; + } + + _window.EraseButton.Pressed = false; + _window.OverrideMenu.Disabled = false; + } + + private void OnEntityOverrideSelected(OptionButton.ItemSelectedEventArgs args) + { + if (_window == null || _window.Disposed) + return; + + _window.OverrideMenu.SelectId(args.Id); + + if (_placement.CurrentMode != null) + { + var newObjInfo = new PlacementInformation + { + PlacementOption = EntitySpawnWindow.InitOpts[args.Id], + EntityType = _placement.CurrentPermission!.EntityType, + Range = 2, + IsTile = _placement.CurrentPermission.IsTile + }; + + _placement.Clear(); + _placement.BeginPlacing(newObjInfo); + } + } + + private void OnEntitySearchChanged(LineEditEventArgs args) + { + if (_window == null || _window.Disposed) + return; + + _placement.Clear(); + BuildEntityList(args.Text); + _window.ClearButton.Disabled = string.IsNullOrEmpty(args.Text); + } + + private void OnEntityClearPressed(ButtonEventArgs args) + { + if (_window == null || _window.Disposed) + return; + + _placement.Clear(); + _window.SearchBar.Clear(); + BuildEntityList(""); + } + + private void BuildEntityList(string? searchStr = null) + { + if (_window == null || _window.Disposed) + return; + + _shownEntities.Clear(); + _window.PrototypeList.RemoveAllChildren(); + // Reset last prototype indices so it automatically updates the entire list. + _lastEntityIndices = (0, -1); + _window.PrototypeList.RemoveAllChildren(); + _window.SelectedButton = null; + searchStr = searchStr?.ToLowerInvariant(); + + foreach (var prototype in _prototypes.EnumeratePrototypes()) + { + if (prototype.Abstract) + { + continue; + } + + if (prototype.NoSpawn) + { + continue; + } + + if (searchStr != null && !DoesEntityMatchSearch(prototype, searchStr)) + { + continue; + } + + _shownEntities.Add(prototype); + } + + _shownEntities.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal)); + + _window.PrototypeList.TotalItemCount = _shownEntities.Count; + UpdateVisiblePrototypes(); + } + + private static bool DoesEntityMatchSearch(EntityPrototype prototype, string searchStr) + { + if (string.IsNullOrEmpty(searchStr)) + return true; + + if (prototype.ID.Contains(searchStr, StringComparison.InvariantCultureIgnoreCase)) + return true; + + if (prototype.EditorSuffix != null && + prototype.EditorSuffix.Contains(searchStr, StringComparison.InvariantCultureIgnoreCase)) + return true; + + if (string.IsNullOrEmpty(prototype.Name)) + return false; + + if (prototype.Name.Contains(searchStr, StringComparison.InvariantCultureIgnoreCase)) + return true; + + return false; + } + + private void UpdateEntityDirectionLabel() + { + if (_window == null || _window.Disposed) + return; + + _window.RotationLabel.Text = _placement.Direction.ToString(); + } + + private void OnDirectionChanged(object? sender, EventArgs e) + { + UpdateEntityDirectionLabel(); + } + + // Update visible buttons in the prototype list. + private void UpdateVisiblePrototypes() + { + if (_window == null || _window.Disposed) + return; + + // Calculate index of first prototype to render based on current scroll. + var height = _window.MeasureButton.DesiredSize.Y + PrototypeListContainer.Separation; + var offset = Math.Max(-_window.PrototypeList.Position.Y, 0); + var startIndex = (int) Math.Floor(offset / height); + _window.PrototypeList.ItemOffset = startIndex; + + var (prevStart, prevEnd) = _lastEntityIndices; + + // Calculate index of final one. + var endIndex = startIndex - 1; + var spaceUsed = -height; // -height instead of 0 because else it cuts off the last button. + + while (spaceUsed < _window.PrototypeList.Parent!.Height) + { + spaceUsed += height; + endIndex += 1; + } + + endIndex = Math.Min(endIndex, _shownEntities.Count - 1); + + if (endIndex == prevEnd && startIndex == prevStart) + { + // Nothing changed so bye. + return; + } + + _lastEntityIndices = (startIndex, endIndex); + + // Delete buttons at the start of the list that are no longer visible (scrolling down). + for (var i = prevStart; i < startIndex && i <= prevEnd; i++) + { + var control = (EntitySpawnButton) _window.PrototypeList.GetChild(0); + DebugTools.Assert(control.Index == i); + _window.PrototypeList.RemoveChild(control); + } + + // Delete buttons at the end of the list that are no longer visible (scrolling up). + for (var i = prevEnd; i > endIndex && i >= prevStart; i--) + { + var control = (EntitySpawnButton) _window.PrototypeList.GetChild(_window.PrototypeList.ChildCount - 1); + DebugTools.Assert(control.Index == i); + _window.PrototypeList.RemoveChild(control); + } + + // Create buttons at the start of the list that are now visible (scrolling up). + for (var i = Math.Min(prevStart - 1, endIndex); i >= startIndex; i--) + { + InsertEntityButton(_shownEntities[i], true, i); + } + + // Create buttons at the end of the list that are now visible (scrolling down). + for (var i = Math.Max(prevEnd + 1, startIndex); i <= endIndex; i++) + { + InsertEntityButton(_shownEntities[i], false, i); + } + } + + private void InsertEntityButton(EntityPrototype prototype, bool insertFirst, int index) + { + if (_window == null || _window.Disposed) + return; + + var textures = SpriteComponent.GetPrototypeTextures(prototype, _resources).Select(o => o.Default).ToList(); + var button = _window.InsertEntityButton(prototype, insertFirst, index, textures); + + button.ActualButton.OnToggled += OnEntityButtonToggled; + } + + private void OnEntityButtonToggled(ButtonToggledEventArgs args) + { + if (_window == null || _window.Disposed) + return; + + var item = (EntitySpawnButton) args.Button.Parent!; + if (_window.SelectedButton == item) + { + _window.SelectedButton = null; + _window.SelectedPrototype = null; + _placement.Clear(); + return; + } + + if (_window.SelectedButton != null) + { + _window.SelectedButton.ActualButton.Pressed = false; + } + + _window.SelectedButton = null; + _window.SelectedPrototype = null; + + var overrideMode = EntitySpawnWindow.InitOpts[_window.OverrideMenu.SelectedId]; + var newObjInfo = new PlacementInformation + { + PlacementOption = overrideMode != "Default" ? overrideMode : item.Prototype.PlacementMode, + EntityType = item.PrototypeID, + Range = 2, + IsTile = false + }; + + _placement.BeginPlacing(newObjInfo); + + _window.SelectedButton = item; + _window.SelectedPrototype = item.Prototype; + } +} diff --git a/Robust.Client/UserInterface/Controllers/Implementations/TileSpawningUIController.cs b/Robust.Client/UserInterface/Controllers/Implementations/TileSpawningUIController.cs new file mode 100644 index 000000000..e34001691 --- /dev/null +++ b/Robust.Client/UserInterface/Controllers/Implementations/TileSpawningUIController.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Robust.Client.Graphics; +using Robust.Client.Placement; +using Robust.Client.ResourceManagement; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.CustomControls; +using Robust.Shared.Enums; +using Robust.Shared.IoC; +using Robust.Shared.Map; +using Robust.Shared.Utility; +using static Robust.Client.UserInterface.Controls.BaseButton; + +namespace Robust.Client.UserInterface.Controllers.Implementations; + +public sealed class TileSpawningUIController : UIController +{ + [Dependency] private readonly IPlacementManager _placement = default!; + [Dependency] private readonly IResourceCache _resources = default!; + [Dependency] private readonly ITileDefinitionManager _tiles = default!; + + private TileSpawnWindow? _window; + private bool _init; + + private readonly List _shownTiles = new(); + private bool _clearingTileSelections; + + public override void Initialize() + { + DebugTools.Assert(_init == false); + _init = true; + _placement.PlacementChanged += ClearTileSelection; + } + + public void ToggleWindow() + { + EnsureWindow(); + + if (_window!.IsOpen) + { + _window.Close(); + } + else + { + _window.Open(); + } + } + + private void EnsureWindow() + { + if (_window is { Disposed: false }) + return; + _window = UIManager.CreateWindow(); + LayoutContainer.SetAnchorPreset(_window,LayoutContainer.LayoutPreset.CenterLeft); + _window.SearchBar.GrabKeyboardFocus(); + _window.ClearButton.OnPressed += OnTileClearPressed; + _window.SearchBar.OnTextChanged += OnTileSearchChanged; + _window.TileList.OnItemSelected += OnTileItemSelected; + _window.TileList.OnItemDeselected += OnTileItemDeselected; + BuildTileList(); + } + + public void CloseWindow() + { + if (_window == null || _window.Disposed) return; + + _window?.Close(); + } + + private void ClearTileSelection(object? sender, EventArgs e) + { + if (_window == null || _window.Disposed) return; + _clearingTileSelections = true; + _window.TileList.ClearSelected(); + _clearingTileSelections = false; + } + + private void OnTileClearPressed(ButtonEventArgs args) + { + if (_window == null || _window.Disposed) return; + + _window.TileList.ClearSelected(); + _placement.Clear(); + _window.SearchBar.Clear(); + BuildTileList(string.Empty); + _window.ClearButton.Disabled = true; + } + + private void OnTileSearchChanged(LineEdit.LineEditEventArgs args) + { + if (_window == null || _window.Disposed) return; + + _window.TileList.ClearSelected(); + _placement.Clear(); + BuildTileList(args.Text); + _window.ClearButton.Disabled = string.IsNullOrEmpty(args.Text); + } + + private void OnTileItemSelected(ItemList.ItemListSelectedEventArgs args) + { + var definition = _shownTiles[args.ItemIndex]; + + var newObjInfo = new PlacementInformation + { + PlacementOption = "AlignTileAny", + TileType = definition.TileId, + Range = 400, + IsTile = true + }; + + _placement.BeginPlacing(newObjInfo); + } + + private void OnTileItemDeselected(ItemList.ItemListDeselectedEventArgs args) + { + if (_clearingTileSelections) + { + return; + } + + _placement.Clear(); + } + + private void BuildTileList(string? searchStr = null) + { + if (_window == null || _window.Disposed) return; + + _window.TileList.Clear(); + + IEnumerable tileDefs = _tiles; + + if (!string.IsNullOrEmpty(searchStr)) + { + tileDefs = tileDefs.Where(s => + s.Name.Contains(searchStr, StringComparison.InvariantCultureIgnoreCase) || + s.ID.Contains(searchStr, StringComparison.OrdinalIgnoreCase)); + } + + tileDefs = tileDefs.OrderBy(d => d.Name); + + _shownTiles.Clear(); + _shownTiles.AddRange(tileDefs); + + foreach (var entry in _shownTiles) + { + Texture? texture = null; + var path = entry.Sprite?.ToString(); + + if (path != null) + { + texture = _resources.GetResource(path); + } + _window.TileList.AddItem(entry.Name, texture); + } + } +} diff --git a/Robust.Client/UserInterface/Controllers/UIController.cs b/Robust.Client/UserInterface/Controllers/UIController.cs new file mode 100644 index 000000000..9e6d1476d --- /dev/null +++ b/Robust.Client/UserInterface/Controllers/UIController.cs @@ -0,0 +1,25 @@ +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Timing; + +namespace Robust.Client.UserInterface.Controllers; + +// Notices your UIController, *UwU Whats this?* +/// +/// Each is instantiated as a singleton by +/// can use for regular IoC dependencies +/// and to depend on s, which will be automatically +/// injected once they are created. +/// +public abstract class UIController +{ + [Dependency] protected readonly IUserInterfaceManager UIManager = default!; + + public virtual void Initialize() + { + } + + public virtual void FrameUpdate(FrameEventArgs args) + { + } +} diff --git a/Robust.Client/UserInterface/Controllers/UserInterfaceManager.Controllers.cs b/Robust.Client/UserInterface/Controllers/UserInterfaceManager.Controllers.cs new file mode 100644 index 000000000..345606b78 --- /dev/null +++ b/Robust.Client/UserInterface/Controllers/UserInterfaceManager.Controllers.cs @@ -0,0 +1,336 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.InteropServices; +using Robust.Client.State; +using Robust.Client.UserInterface.Controllers; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Timing; +using Robust.Shared.Utility; +using static Robust.Shared.Serialization.Manager.Definition.DataDefinition; + +// ReSharper disable once CheckNamespace +namespace Robust.Client.UserInterface; + +internal partial class UserInterfaceManager +{ + /// + /// All registered instances indexed by type + /// + private readonly Dictionary _uiControllers = new(); + + /// + /// Implementations of to invoke when a state is entered + /// State Type -> (UIController, Caller) + /// + private readonly Dictionary> _onStateEnteredDelegates = new(); + + /// + /// Implementations of to invoke when a state is exited + /// State Type -> (UIController, Caller) + /// + private readonly Dictionary> _onStateExitedDelegates = new(); + + /// + /// Implementations of to invoke when an entity system is loaded + /// Entity System Type -> (UIController, Caller) + /// + private readonly Dictionary> _onSystemLoadedDelegates = new(); + + /// + /// Implementations of to invoke when an entity system is unloaded + /// Entity System Type -> (UIController, Caller) + /// + private readonly Dictionary> _onSystemUnloadedDelegates = new(); + + /// + /// Field -> Controller -> Field assigner delegate + /// + private readonly Dictionary>> _assignerRegistry = new(); + + private delegate void StateChangedCaller(object controller, State.State state); + + private delegate void SystemChangedCaller(object controller, IEntitySystem system); + + private StateChangedCaller EmitStateChangedCaller(Type controller, Type state, bool entered) + { + if (controller.IsValueType) + { + throw new ArgumentException($"Value type controllers are not supported. Controller: {controller}"); + } + + if (state.IsValueType) + { + throw new ArgumentException($"Value type states are not supported. State: {state}"); + } + + var method = new DynamicMethod( + "StateChangedCaller", + typeof(void), + new[] {typeof(object), typeof(State.State)}, + true + ); + + var generator = method.GetILGenerator(); + + Type onStateChangedType; + MethodInfo onStateChangedMethod; + if (entered) + { + onStateChangedType = typeof(IOnStateEntered<>).MakeGenericType(state); + onStateChangedMethod = + controller.GetMethod(nameof(IOnStateEntered.OnStateEntered), new[] {state}) + ?? throw new NullReferenceException(); + } + else + { + onStateChangedType = typeof(IOnStateExited<>).MakeGenericType(state); + onStateChangedMethod = + controller.GetMethod(nameof(IOnStateExited.OnStateExited), new[] {state}) + ?? throw new NullReferenceException(); + } + + generator.Emit(OpCodes.Ldarg_0); // controller + generator.Emit(OpCodes.Castclass, onStateChangedType); + + generator.Emit(OpCodes.Ldarg_1); // state + generator.Emit(OpCodes.Castclass, state); + + generator.Emit(OpCodes.Callvirt, onStateChangedMethod); + generator.Emit(OpCodes.Ret); + + return method.CreateDelegate(); + } + + private SystemChangedCaller EmitSystemChangedCaller(Type controller, Type system, bool loaded) + { + if (controller.IsValueType) + { + throw new ArgumentException($"Value type controllers are not supported. Controller: {controller}"); + } + + if (system.IsValueType) + { + throw new ArgumentException($"Value type systems are not supported. System: {system}"); + } + + var method = new DynamicMethod( + "SystemChangedCaller", + typeof(void), + new[] {typeof(object), typeof(IEntitySystem)}, + true + ); + + var generator = method.GetILGenerator(); + + Type onSystemChangedType; + MethodInfo onSystemChangedMethod; + if (loaded) + { + onSystemChangedType = typeof(IOnSystemLoaded<>).MakeGenericType(system); + onSystemChangedMethod = + controller.GetMethod(nameof(IOnSystemLoaded.OnSystemLoaded), new[] {system}) + ?? throw new NullReferenceException(); + } + else + { + onSystemChangedType = typeof(IOnSystemUnloaded<>).MakeGenericType(system); + onSystemChangedMethod = + controller.GetMethod(nameof(IOnSystemUnloaded.OnSystemUnloaded), new[] {system}) + ?? throw new NullReferenceException(); + } + + generator.Emit(OpCodes.Ldarg_0); // controller + generator.Emit(OpCodes.Castclass, onSystemChangedType); + + generator.Emit(OpCodes.Ldarg_1); // system + generator.Emit(OpCodes.Castclass, system); + + generator.Emit(OpCodes.Callvirt, onSystemChangedMethod); + generator.Emit(OpCodes.Ret); + + return method.CreateDelegate(); + } + + private void RegisterUIController(Type type, UIController controller) + { + _uiControllers.Add(type, controller); + } + + private ref UIController GetUIControllerRef(Type type) + { + return ref CollectionsMarshal.GetValueRefOrNullRef(_uiControllers, type); + } + + private UIController GetUIController(Type type) + { + return _uiControllers[type]; + } + + public T GetUIController() where T : UIController, new() + { + return (T) GetUIController(typeof(T)); + } + + private void _setupControllers() + { + foreach (var uiControllerType in _reflectionManager.GetAllChildren()) + { + if (uiControllerType.IsAbstract) + continue; + + var newController = _typeFactory.CreateInstanceUnchecked(uiControllerType); + + RegisterUIController(uiControllerType, newController); + + foreach (var fieldInfo in uiControllerType.GetAllPropertiesAndFields()) + { + if (!fieldInfo.HasAttribute()) + { + continue; + } + + var backingField = fieldInfo; + if (fieldInfo is SpecificPropertyInfo property) + { + if (property.TryGetBackingField(out var field)) + { + backingField = field; + } + else + { + var setter = property.PropertyInfo.GetSetMethod(true); + if (setter == null) + { + throw new InvalidOperationException( + $"Property with {nameof(UISystemDependencyAttribute)} attribute did not have a backing field nor setter"); + } + } + } + + //Do not do anything if the field isn't an entity system + if (!typeof(IEntitySystem).IsAssignableFrom(backingField.FieldType)) + continue; + + var typeDict = _assignerRegistry.GetOrNew(fieldInfo.FieldType); + var assigner = EmitFieldAssigner(uiControllerType, fieldInfo.FieldType, backingField); + typeDict.Add(uiControllerType, assigner); + } + + foreach (var @interface in uiControllerType.GetInterfaces()) + { + if (!@interface.IsGenericType) + continue; + + var typeDefinition = @interface.GetGenericTypeDefinition(); + var genericType = @interface.GetGenericArguments()[0]; + if (typeDefinition == typeof(IOnStateEntered<>)) + { + var enteredCaller = EmitStateChangedCaller(uiControllerType, genericType, true); + _onStateEnteredDelegates.GetOrNew(genericType).Add(newController, enteredCaller); + } + else if (typeDefinition == typeof(IOnStateExited<>)) + { + var exitedCaller = EmitStateChangedCaller(uiControllerType, genericType, false); + _onStateExitedDelegates.GetOrNew(genericType).Add(newController, exitedCaller); + } + else if (typeDefinition == typeof(IOnSystemLoaded<>)) + { + var loadedCaller = EmitSystemChangedCaller(uiControllerType, genericType, true); + _onSystemLoadedDelegates.GetOrNew(genericType).Add(newController, loadedCaller); + } + else if (typeDefinition == typeof(IOnSystemUnloaded<>)) + { + var unloadedCaller = EmitSystemChangedCaller(uiControllerType, genericType, false); + _onSystemUnloadedDelegates.GetOrNew(genericType).Add(newController, unloadedCaller); + } + } + } + + _systemManager.SystemLoaded += OnSystemLoaded; + _systemManager.SystemUnloaded += OnSystemUnloaded; + + _stateManager.OnStateChanged += OnStateChanged; + } + + private void _initializeControllers() + { + foreach (var controller in _uiControllers.Values) + { + controller.Initialize(); + } + } + + private void UpdateControllers(FrameEventArgs args) + { + foreach (var controller in _uiControllers.Values) + { + controller.FrameUpdate(args); + } + } + + // TODO hud refactor optimize this to use an array + // TODO hud refactor BEFORE MERGE cleanup subscriptions for all implementations when switching out of gameplay state + private void OnStateChanged(StateChangedEventArgs args) + { + if (_onStateExitedDelegates.TryGetValue(args.OldState.GetType(), out var exitedDelegates)) + { + foreach (var (controller, caller) in exitedDelegates) + { + caller(controller, args.OldState); + } + } + + if (_onStateEnteredDelegates.TryGetValue(args.NewState.GetType(), out var enteredDelegates)) + { + foreach (var (controller, caller) in enteredDelegates) + { + caller(controller, args.NewState); + } + } + } + + private void OnSystemLoaded(object? sender, SystemChangedArgs args) + { + var systemType = args.System.GetType(); + + if (_assignerRegistry.TryGetValue(systemType, out var assigners)) + { + foreach (var (controllerType, assigner) in assigners) + { + assigner(ref GetUIControllerRef(controllerType), args.System); + } + } + + if (_onSystemLoadedDelegates.TryGetValue(systemType, out var delegates)) + { + foreach (var (controller, caller) in delegates) + { + caller(controller, args.System); + } + } + } + + private void OnSystemUnloaded(object? system, SystemChangedArgs args) + { + var systemType = args.System.GetType(); + + if (_onSystemUnloadedDelegates.TryGetValue(systemType, out var delegates)) + { + foreach (var (controller, caller) in delegates) + { + caller(controller, args.System); + } + } + + if (_assignerRegistry.TryGetValue(systemType, out var assigners)) + { + foreach (var (controllerType, assigner) in assigners) + { + assigner(ref GetUIControllerRef(controllerType), null); + } + } + } +} diff --git a/Robust.Client/UserInterface/Controls/Popup.cs b/Robust.Client/UserInterface/Controls/Popup.cs index d9f2a9c5c..739f36576 100644 --- a/Robust.Client/UserInterface/Controls/Popup.cs +++ b/Robust.Client/UserInterface/Controls/Popup.cs @@ -15,6 +15,10 @@ namespace Robust.Client.UserInterface.Controls private Vector2 _desiredSize; + public bool CloseOnClick { get; set; } = true; + + public bool CloseOnEscape { get; set; } = true; + public void Open(UIBox2? box = null, Vector2? altPos = null) { if (Visible) diff --git a/Robust.Client/UserInterface/Controls/ScrollContainer.cs b/Robust.Client/UserInterface/Controls/ScrollContainer.cs index a980fdf6e..50f22dd22 100644 --- a/Robust.Client/UserInterface/Controls/ScrollContainer.cs +++ b/Robust.Client/UserInterface/Controls/ScrollContainer.cs @@ -7,6 +7,7 @@ namespace Robust.Client.UserInterface.Controls [Virtual] public class ScrollContainer : Container { + private bool _queueScrolled = false; private bool _vScrollEnabled = true; private bool _hScrollEnabled = true; @@ -23,6 +24,8 @@ namespace Robust.Client.UserInterface.Controls public bool ReturnMeasure { get; set; } = false; + public event Action? OnScrolled; + public ScrollContainer() { MouseFilter = MouseFilterMode.Pass; @@ -204,6 +207,16 @@ namespace Robust.Client.UserInterface.Controls return finalSize; } + protected override void ArrangeCore(UIBox2 finalRect) + { + base.ArrangeCore(finalRect); + + if (!_queueScrolled) return; + + OnScrolled?.Invoke(); + _queueScrolled = false; + } + protected internal override void MouseWheel(GUIMouseWheelEventArgs args) { base.MouseWheel(args); @@ -261,6 +274,7 @@ namespace Robust.Client.UserInterface.Controls } InvalidateArrange(); + _queueScrolled = true; } } } diff --git a/Robust.Client/UserInterface/Controls/SpriteView.cs b/Robust.Client/UserInterface/Controls/SpriteView.cs index b3e82ce9d..6c35dc0a5 100644 --- a/Robust.Client/UserInterface/Controls/SpriteView.cs +++ b/Robust.Client/UserInterface/Controls/SpriteView.cs @@ -1,7 +1,6 @@ using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Shared.GameObjects; -using Robust.Shared.IoC; using Robust.Shared.Maths; using Robust.Shared.ViewVariables; @@ -10,7 +9,7 @@ namespace Robust.Client.UserInterface.Controls [Virtual] public class SpriteView : Control { - private readonly SpriteSystem _spriteSystem; + private SpriteSystem? _spriteSystem; private Vector2 _scale = (1, 1); @@ -36,21 +35,8 @@ namespace Robust.Client.UserInterface.Controls /// public Direction? OverrideDirection { get; set; } - public SpriteView(IEntitySystemManager sysMan) - { - _spriteSystem = sysMan.GetEntitySystem(); - RectClipContent = true; - } - public SpriteView() { - _spriteSystem = IoCManager.Resolve().GetEntitySystem(); - RectClipContent = true; - } - - public SpriteView(SpriteSystem spriteSys) - { - _spriteSystem = spriteSys; RectClipContent = true; } @@ -68,7 +54,9 @@ namespace Robust.Client.UserInterface.Controls return; } - _spriteSystem.ForceUpdate(Sprite); + _spriteSystem ??= EntitySystem.Get(); + _spriteSystem?.ForceUpdate(Sprite); + renderHandle.DrawEntity(Sprite.Owner, PixelSize / 2, Scale * UIScale, OverrideDirection); } } diff --git a/Robust.Client/UserInterface/Controls/TextureButton.cs b/Robust.Client/UserInterface/Controls/TextureButton.cs index 38547eb47..e086db3cd 100644 --- a/Robust.Client/UserInterface/Controls/TextureButton.cs +++ b/Robust.Client/UserInterface/Controls/TextureButton.cs @@ -1,5 +1,7 @@ using System; using Robust.Client.Graphics; +using Robust.Client.ResourceManagement; +using Robust.Shared.IoC; using Robust.Shared.Maths; using Robust.Shared.ViewVariables; @@ -15,6 +17,8 @@ namespace Robust.Client.UserInterface.Controls public const string StylePseudoClassHover = "hover"; public const string StylePseudoClassDisabled = "disabled"; public const string StylePseudoClassPressed = "pressed"; + private string? _texturePath; + public TextureButton() { @@ -32,6 +36,29 @@ namespace Robust.Client.UserInterface.Controls } } + public string TextureThemePath + { + set { + TextureNormal = Theme.ResolveTexture(value); + _texturePath = value; + } + } + + + protected override void OnThemeUpdated() + { + if (_texturePath != null) TextureNormal = Theme.ResolveTexture(_texturePath); + base.OnThemeUpdated(); + } + public string TexturePath + { + set + { + TextureNormal = IoCManager.Resolve().GetResource(value); + _texturePath = value; + } + } + public Vector2 Scale { get => _scale; diff --git a/Robust.Client/UserInterface/Controls/TextureRect.cs b/Robust.Client/UserInterface/Controls/TextureRect.cs index 436d2947f..daac9bbbd 100644 --- a/Robust.Client/UserInterface/Controls/TextureRect.cs +++ b/Robust.Client/UserInterface/Controls/TextureRect.cs @@ -1,5 +1,7 @@ using System; using Robust.Client.Graphics; +using Robust.Client.ResourceManagement; +using Robust.Shared.IoC; using Robust.Shared.Maths; namespace Robust.Client.UserInterface.Controls @@ -38,6 +40,35 @@ namespace Robust.Client.UserInterface.Controls } } + private string? _texturePath; + + // TODO HUD REFACTOR BEFORE MERGE use or cleanup + public string TextureThemePath + { + set + { + Texture = Theme.ResolveTexture(value); + _texturePath = value; + } + } + + // TODO HUD REFACTOR BEFORE MERGE use or cleanup + public string TexturePath + { + set + { + Texture = IoCManager.Resolve().GetResource(value); + _texturePath = value; + } + + } + + protected override void OnThemeUpdated() + { + if (_texturePath != null) Texture = Theme.ResolveTexture(_texturePath); + base.OnThemeUpdated(); + } + /// /// Scales the texture displayed. /// diff --git a/Robust.Client/UserInterface/Controls/UIWidget.cs b/Robust.Client/UserInterface/Controls/UIWidget.cs new file mode 100644 index 000000000..f0bc9bd01 --- /dev/null +++ b/Robust.Client/UserInterface/Controls/UIWidget.cs @@ -0,0 +1,12 @@ +using Robust.Shared.IoC; + +namespace Robust.Client.UserInterface.Controls; + +[Virtual] +public abstract class UIWidget : BoxContainer +{ + protected UIWidget() + { + IoCManager.InjectDependencies(this); + } +} diff --git a/Robust.Client/UserInterface/Controls/WindowRoot.cs b/Robust.Client/UserInterface/Controls/WindowRoot.cs index 08831f61a..f62459f21 100644 --- a/Robust.Client/UserInterface/Controls/WindowRoot.cs +++ b/Robust.Client/UserInterface/Controls/WindowRoot.cs @@ -10,31 +10,6 @@ namespace Robust.Client.UserInterface.Controls { Window = window; } - - /// - /// Enable the UI autoscale system, this will scale down the UI for lower resolutions - /// - [ViewVariables] - public bool AutoScale { get; set; } = false; - - /// - /// Minimum resolution to start clamping autoscale to 1 - /// - [ViewVariables] - public Vector2i AutoScaleUpperCutoff { get; set; } = new Vector2i(1080, 720); - - /// - /// Maximum resolution to start clamping autos scale to autoscale minimum - /// - [ViewVariables] - public Vector2i AutoScaleLowerCutoff { get; set; } = new Vector2i(520, 520); - - /// - /// The minimum ui scale value that autoscale will scale to - /// - [ViewVariables] - public float AutoScaleMinimum { get; set; } = 0.5f; - public override float UIScale => UIScaleSet; internal float UIScaleSet { get; set; } public override IClydeWindow Window { get; } diff --git a/Robust.Client/UserInterface/CustomControls/BaseWindow.cs b/Robust.Client/UserInterface/CustomControls/BaseWindow.cs index a5e6f0b9d..b706f82d2 100644 --- a/Robust.Client/UserInterface/CustomControls/BaseWindow.cs +++ b/Robust.Client/UserInterface/CustomControls/BaseWindow.cs @@ -17,8 +17,6 @@ namespace Robust.Client.UserInterface.CustomControls private Vector2 DragOffsetTopLeft; private Vector2 DragOffsetBottomRight; - protected bool _firstTimeOpened = true; - public bool Resizable { get; set; } = true; public bool IsOpen => Parent != null; @@ -27,6 +25,8 @@ namespace Robust.Client.UserInterface.CustomControls /// public event Action? OnClose; + public event Action? OnOpen; + public virtual void Close() { if (Parent == null) @@ -210,7 +210,6 @@ namespace Robust.Client.UserInterface.CustomControls return true; } - public void Open() { if (!Visible) @@ -225,9 +224,11 @@ namespace Robust.Client.UserInterface.CustomControls } Opened(); + OnOpen?.Invoke(); } public void OpenCentered() => OpenCenteredAt((0.5f, 0.5f)); + public void OpenToLeft() => OpenCenteredAt((0, 0.5f)); public void OpenCenteredLeft() => OpenCenteredAt((0.25f, 0.5f)); public void OpenToRight() => OpenCenteredAt((1, 0.5f)); @@ -240,17 +241,10 @@ namespace Robust.Client.UserInterface.CustomControls /// lower right. public void OpenCenteredAt(Vector2 relativePosition) { - if (!_firstTimeOpened) - { - Open(); - return; - } - Measure(Vector2.Infinity); SetSize = DesiredSize; Open(); RecenterWindow(relativePosition); - _firstTimeOpened = false; } /// diff --git a/Robust.Client/UserInterface/CustomControls/DoNotMeasure.cs b/Robust.Client/UserInterface/CustomControls/DoNotMeasure.cs new file mode 100644 index 000000000..ece0556b4 --- /dev/null +++ b/Robust.Client/UserInterface/CustomControls/DoNotMeasure.cs @@ -0,0 +1,11 @@ +using Robust.Shared.Maths; + +namespace Robust.Client.UserInterface.CustomControls; + +internal sealed class DoNotMeasure : Control +{ + protected override Vector2 MeasureOverride(Vector2 availableSize) + { + return Vector2.Zero; + } +} diff --git a/Robust.Client/UserInterface/CustomControls/EntitySpawnButton.cs b/Robust.Client/UserInterface/CustomControls/EntitySpawnButton.cs new file mode 100644 index 000000000..278dbe804 --- /dev/null +++ b/Robust.Client/UserInterface/CustomControls/EntitySpawnButton.cs @@ -0,0 +1,47 @@ +using System.Diagnostics; +using Robust.Client.UserInterface.Controls; +using Robust.Shared.Prototypes; + +namespace Robust.Client.UserInterface.CustomControls; + +[DebuggerDisplay("spawnbutton {" + nameof(Index) + "}")] +public sealed class EntitySpawnButton : Control +{ + public string PrototypeID => Prototype.ID; + public EntityPrototype Prototype { get; set; } = default!; + public Button ActualButton { get; private set; } + public Label EntityLabel { get; private set; } + public LayeredTextureRect EntityTextureRects { get; private set; } + public int Index { get; set; } + + public EntitySpawnButton() + { + AddChild(ActualButton = new Button + { + ToggleMode = true, + }); + + AddChild(new BoxContainer + { + Orientation = BoxContainer.LayoutOrientation.Horizontal, + Children = + { + (EntityTextureRects = new LayeredTextureRect + { + MinSize = (32, 32), + HorizontalAlignment = HAlignment.Center, + VerticalAlignment = VAlignment.Center, + Stretch = TextureRect.StretchMode.KeepAspectCentered, + CanShrink = true + }), + (EntityLabel = new Label + { + VerticalAlignment = VAlignment.Center, + HorizontalExpand = true, + Text = "Backpack", + ClipText = true + }) + } + }); + } +} diff --git a/Robust.Client/UserInterface/CustomControls/EntitySpawnWindow.cs b/Robust.Client/UserInterface/CustomControls/EntitySpawnWindow.cs deleted file mode 100644 index 1bcfebbf9..000000000 --- a/Robust.Client/UserInterface/CustomControls/EntitySpawnWindow.cs +++ /dev/null @@ -1,558 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using Robust.Client.GameObjects; -using Robust.Client.Placement; -using Robust.Client.ResourceManagement; -using Robust.Client.UserInterface.Controls; -using Robust.Shared.Enums; -using Robust.Shared.Localization; -using Robust.Shared.Maths; -using Robust.Shared.Prototypes; -using Robust.Shared.Timing; -using Robust.Shared.Utility; -using static Robust.Client.UserInterface.Controls.BoxContainer; - -namespace Robust.Client.UserInterface.CustomControls -{ - public sealed class EntitySpawnWindow : DefaultWindow - { - private readonly IPlacementManager placementManager; - private readonly IPrototypeManager prototypeManager; - private readonly IResourceCache resourceCache; - - private BoxContainer MainVBox; - private PrototypeListContainer PrototypeList; - private LineEdit SearchBar; - private OptionButton OverrideMenu; - private Button ClearButton; - private Button EraseButton; - private Label RotationLabel; - - private EntitySpawnButton MeasureButton; - - // List of prototypes that are visible based on current filter criteria. - private readonly List _filteredPrototypes = new(); - - // The indices of the visible prototypes last time UpdateVisiblePrototypes was ran. - // This is inclusive, so end is the index of the last prototype, not right after it. - private (int start, int end) _lastPrototypeIndices; - - private static readonly string[] initOpts = - { - "Default", - "PlaceFree", - "PlaceNearby", - "SnapgridCenter", - "SnapgridBorder", - "AlignSimilar", - "AlignTileAny", - "AlignTileEmpty", - "AlignTileNonDense", - "AlignTileDense", - "AlignWall", - "AlignWallProper", - }; - - private EntitySpawnButton? SelectedButton; - private EntityPrototype? SelectedPrototype; - - public EntitySpawnWindow(IPlacementManager placementManager, - IPrototypeManager prototypeManager, - IResourceCache resourceCache) - { - this.placementManager = placementManager; - this.prototypeManager = prototypeManager; - this.resourceCache = resourceCache; - - Title = Loc.GetString("entity-spawn-window-title"); - - - SetSize = (250, 300); - MinSize = (250, 200); - - Contents.AddChild(MainVBox = new BoxContainer - { - Orientation = LayoutOrientation.Vertical, - Name = "AAAAAA", - Children = - { - new BoxContainer - { - Orientation = LayoutOrientation.Horizontal, - Children = - { - (SearchBar = new LineEdit - { - HorizontalExpand = true, - PlaceHolder = Loc.GetString("entity-spawn-window-search-bar-placeholder") - }), - - (ClearButton = new Button - { - Disabled = true, - Text = Loc.GetString("entity-spawn-window-clear-button"), - }) - } - }, - new ScrollContainer - { - MinSize = new Vector2(200.0f, 0.0f), - VerticalExpand = true, - Children = - { - (PrototypeList = new PrototypeListContainer()) - } - }, - new BoxContainer - { - Orientation = LayoutOrientation.Horizontal, - Children = - { - (EraseButton = new Button - { - ToggleMode = true, - Text = Loc.GetString("entity-spawn-window-erase-button-text") - }), - - (OverrideMenu = new OptionButton - { - HorizontalExpand = true, - ToolTip = Loc.GetString("entity-spawn-window-override-menu-tooltip") - }) - } - }, - (RotationLabel = new Label()), - new DoNotMeasure - { - Visible = false, - Children = - { - (MeasureButton = new EntitySpawnButton()) - } - } - } - }); - - MeasureButton.Measure(Vector2.Infinity); - - for (var i = 0; i < initOpts.Length; i++) - { - OverrideMenu.AddItem(initOpts[i], i); - } - - EraseButton.Pressed = placementManager.Eraser; - EraseButton.OnToggled += OnEraseButtonToggled; - OverrideMenu.OnItemSelected += OnOverrideMenuItemSelected; - SearchBar.OnTextChanged += OnSearchBarTextChanged; - ClearButton.OnPressed += OnClearButtonPressed; - - BuildEntityList(); - - this.placementManager.PlacementChanged += OnPlacementCanceled; - this.placementManager.DirectionChanged += OnDirectionChanged; - UpdateDirectionLabel(); - - OnClose += OnWindowClosed; - - SearchBar.GrabKeyboardFocus(); - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - - if (!disposing) return; - - if (EraseButton.Pressed) - placementManager.Clear(); - - placementManager.PlacementChanged -= OnPlacementCanceled; - placementManager.DirectionChanged -= OnDirectionChanged; - } - - private void OnSearchBarTextChanged(LineEdit.LineEditEventArgs args) - { - placementManager.Clear(); - BuildEntityList(args.Text); - ClearButton.Disabled = string.IsNullOrEmpty(args.Text); - } - - private void OnOverrideMenuItemSelected(OptionButton.ItemSelectedEventArgs args) - { - OverrideMenu.SelectId(args.Id); - - if (placementManager.CurrentMode != null) - { - var newObjInfo = new PlacementInformation - { - PlacementOption = initOpts[args.Id], - EntityType = placementManager.CurrentPermission!.EntityType, - Range = 2, - IsTile = placementManager.CurrentPermission.IsTile - }; - - placementManager.Clear(); - placementManager.BeginPlacing(newObjInfo); - } - } - - private void OnClearButtonPressed(BaseButton.ButtonEventArgs args) - { - placementManager.Clear(); - SearchBar.Clear(); - BuildEntityList(""); - } - - private void OnEraseButtonToggled(BaseButton.ButtonToggledEventArgs args) - { - placementManager.Clear(); - // Only toggle the eraser back if the button is pressed. - if(args.Pressed) - placementManager.ToggleEraser(); - // clearing will toggle the erase button off... - args.Button.Pressed = args.Pressed; - OverrideMenu.Disabled = args.Pressed; - } - - private void BuildEntityList(string? searchStr = null) - { - _filteredPrototypes.Clear(); - PrototypeList.RemoveAllChildren(); - // Reset last prototype indices so it automatically updates the entire list. - _lastPrototypeIndices = (0, -1); - PrototypeList.RemoveAllChildren(); - SelectedButton = null; - searchStr = searchStr?.ToLowerInvariant(); - - foreach (var prototype in prototypeManager.EnumeratePrototypes()) - { - if (prototype.NoSpawn || prototype.Abstract) - { - continue; - } - - if (searchStr != null && !_doesPrototypeMatchSearch(prototype, searchStr)) - { - continue; - } - - _filteredPrototypes.Add(prototype); - } - - _filteredPrototypes.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal)); - - PrototypeList.TotalItemCount = _filteredPrototypes.Count; - } - - private void UpdateVisiblePrototypes() - { - // Update visible buttons in the prototype list. - - // Calculate index of first prototype to render based on current scroll. - var height = MeasureButton.DesiredSize.Y + PrototypeListContainer.Separation; - var offset = Math.Max(-PrototypeList.Position.Y, 0); - var startIndex = (int) Math.Floor(offset / height); - PrototypeList.ItemOffset = startIndex; - - var (prevStart, prevEnd) = _lastPrototypeIndices; - - // Calculate index of final one. - var endIndex = startIndex - 1; - var spaceUsed = -height; // -height instead of 0 because else it cuts off the last button. - - while (spaceUsed < PrototypeList.Parent!.Height) - { - spaceUsed += height; - endIndex += 1; - } - - endIndex = Math.Min(endIndex, _filteredPrototypes.Count - 1); - - if (endIndex == prevEnd && startIndex == prevStart) - { - // Nothing changed so bye. - return; - } - - _lastPrototypeIndices = (startIndex, endIndex); - - // Delete buttons at the start of the list that are no longer visible (scrolling down). - for (var i = prevStart; i < startIndex && i <= prevEnd; i++) - { - var control = (EntitySpawnButton) PrototypeList.GetChild(0); - DebugTools.Assert(control.Index == i); - PrototypeList.RemoveChild(control); - } - - // Delete buttons at the end of the list that are no longer visible (scrolling up). - for (var i = prevEnd; i > endIndex && i >= prevStart; i--) - { - var control = (EntitySpawnButton) PrototypeList.GetChild(PrototypeList.ChildCount - 1); - DebugTools.Assert(control.Index == i); - PrototypeList.RemoveChild(control); - } - - // Create buttons at the start of the list that are now visible (scrolling up). - for (var i = Math.Min(prevStart - 1, endIndex); i >= startIndex; i--) - { - InsertEntityButton(_filteredPrototypes[i], true, i); - } - - // Create buttons at the end of the list that are now visible (scrolling down). - for (var i = Math.Max(prevEnd + 1, startIndex); i <= endIndex; i++) - { - InsertEntityButton(_filteredPrototypes[i], false, i); - } - } - - // Create a spawn button and insert it into the start or end of the list. - private void InsertEntityButton(EntityPrototype prototype, bool insertFirst, int index) - { - var button = new EntitySpawnButton - { - Prototype = prototype, - Index = index // We track this index purely for debugging. - }; - button.ActualButton.OnToggled += OnItemButtonToggled; - var entityLabelText = string.IsNullOrEmpty(prototype.Name) ? prototype.ID : prototype.Name; - - if (!string.IsNullOrWhiteSpace(prototype.EditorSuffix)) - { - entityLabelText += $" [{prototype.EditorSuffix}]"; - } - - button.EntityLabel.Text = entityLabelText; - - if (prototype == SelectedPrototype) - { - SelectedButton = button; - SelectedButton.ActualButton.Pressed = true; - } - - var rect = button.EntityTextureRects; - rect.Textures = SpriteComponent.GetPrototypeTextures(prototype, resourceCache).Select(o => o.Default).ToList(); - - PrototypeList.AddChild(button); - if (insertFirst) - { - button.SetPositionInParent(0); - } - } - - private static bool _doesPrototypeMatchSearch(EntityPrototype prototype, string searchStr) - { - if (prototype.ID.ToLowerInvariant().Contains(searchStr)) - { - return true; - } - - if (prototype.EditorSuffix != null && - prototype.EditorSuffix.Contains(searchStr, StringComparison.CurrentCultureIgnoreCase)) - { - return true; - } - - if (string.IsNullOrEmpty(prototype.Name)) - { - return false; - } - - if (prototype.Name.ToLowerInvariant().Contains(searchStr)) - { - return true; - } - - return false; - } - - private void OnItemButtonToggled(BaseButton.ButtonToggledEventArgs args) - { - var item = (EntitySpawnButton) args.Button.Parent!; - if (SelectedButton == item) - { - SelectedButton = null; - SelectedPrototype = null; - placementManager.Clear(); - return; - } - else if (SelectedButton != null) - { - SelectedButton.ActualButton.Pressed = false; - } - - SelectedButton = null; - SelectedPrototype = null; - - var overrideMode = initOpts[OverrideMenu.SelectedId]; - var newObjInfo = new PlacementInformation - { - PlacementOption = overrideMode != "Default" ? overrideMode : item.Prototype.PlacementMode, - EntityType = item.PrototypeID, - Range = 2, - IsTile = false - }; - - placementManager.BeginPlacing(newObjInfo); - - SelectedButton = item; - SelectedPrototype = item.Prototype; - } - - protected override void FrameUpdate(FrameEventArgs args) - { - base.FrameUpdate(args); - UpdateVisiblePrototypes(); - } - - private sealed class PrototypeListContainer : Container - { - // Quick and dirty container to do virtualization of the list. - // Basically, get total item count and offset to put the current buttons at. - // Get a constant minimum height and move the buttons in the list up to match the scrollbar. - private int _totalItemCount; - private int _itemOffset; - - public int TotalItemCount - { - get => _totalItemCount; - set - { - _totalItemCount = value; - InvalidateMeasure(); - } - } - - public int ItemOffset - { - get => _itemOffset; - set - { - _itemOffset = value; - InvalidateMeasure(); - } - } - - public const float Separation = 2; - - protected override Vector2 MeasureOverride(Vector2 availableSize) - { - if (ChildCount == 0) - { - return Vector2.Zero; - } - - var first = GetChild(0); - - first.Measure(availableSize); - var (minX, minY) = first.DesiredSize; - - return (minX, minY * TotalItemCount + (TotalItemCount - 1) * Separation); - } - - protected override Vector2 ArrangeOverride(Vector2 finalSize) - { - if (ChildCount == 0) - { - return Vector2.Zero; - } - - var first = GetChild(0); - - var height = first.DesiredSize.Y; - var offset = ItemOffset * height + (ItemOffset - 1) * Separation; - - foreach (var child in Children) - { - child.Arrange(UIBox2.FromDimensions(0, offset, finalSize.X, height)); - offset += Separation + height; - } - - return finalSize; - } - } - - [DebuggerDisplay("spawnbutton {" + nameof(Index) + "}")] - private sealed class EntitySpawnButton : Control - { - public string PrototypeID => Prototype.ID; - public EntityPrototype Prototype { get; set; } = default!; - public Button ActualButton { get; private set; } - public Label EntityLabel { get; private set; } - public LayeredTextureRect EntityTextureRects { get; private set; } - public int Index { get; set; } - - public EntitySpawnButton() - { - AddChild(ActualButton = new Button - { - ToggleMode = true, - }); - - AddChild(new BoxContainer - { - Orientation = LayoutOrientation.Horizontal, - Children = - { - (EntityTextureRects = new LayeredTextureRect - { - MinSize = (32, 32), - HorizontalAlignment = HAlignment.Center, - VerticalAlignment = VAlignment.Center, - Stretch = TextureRect.StretchMode.KeepAspectCentered, - CanShrink = true - }), - (EntityLabel = new Label - { - VerticalAlignment = VAlignment.Center, - HorizontalExpand = true, - Text = "Backpack", - ClipText = true - }) - } - }); - } - } - - private void OnWindowClosed() - { - if (SelectedButton != null) - { - SelectedButton.ActualButton.Pressed = false; - SelectedButton = null; - } - placementManager.Clear(); - } - - private void OnPlacementCanceled(object? sender, EventArgs e) - { - if (SelectedButton != null) - { - SelectedButton.ActualButton.Pressed = false; - SelectedButton = null; - } - - EraseButton.Pressed = false; - OverrideMenu.Disabled = false; - } - - private void OnDirectionChanged(object? sender, EventArgs e) - { - UpdateDirectionLabel(); - } - - private void UpdateDirectionLabel() - { - RotationLabel.Text = placementManager.Direction.ToString(); - } - - private sealed class DoNotMeasure : Control - { - protected override Vector2 MeasureOverride(Vector2 availableSize) - { - return Vector2.Zero; - } - } - } -} diff --git a/Robust.Client/UserInterface/CustomControls/EntitySpawnWindow.xaml b/Robust.Client/UserInterface/CustomControls/EntitySpawnWindow.xaml new file mode 100644 index 000000000..de269d477 --- /dev/null +++ b/Robust.Client/UserInterface/CustomControls/EntitySpawnWindow.xaml @@ -0,0 +1,23 @@ + + + + +