UI refactor and UITheme implementations (#2712)

* UIControllerManager


Implemented UI Controller Manager

* added fetch function

* added note

* Hiding some internal stuff

* Implemented event on gamestate switch for ui

* Fix serialization field assigner emit

* fixing issues with ILEmitter stuff

* AHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH

Blame Smug

* fixing nullref

* Add checking for no backing field / property for ui system dependencies

* fixes Gamestate detection

* Implemented event on UIControllers on system load

* Updated systemload/unload listeners

* Had this backwards lol

* Fix nulling systems before calling OnSystemUnloaded, broke InventoryUIController.Hands.cs

* Created UI Window management system

- A manager that allows for easy creation and access of popup or gamestate windows

* Changing to use basewindow instead of default window

* Implemented UI Theming that isn't ass

* Updated default theme loading and validation

* Added path validation for themes

* Implemented UI Themes

* Implemented UI Theme prototypes

* Implementing theming for texture buttons and Texturerects

* fixing server error

* Remove IUILink

* Implemented default theme overriding and theme colors

* Fixing sandbox lul

* Added error for not finding UITheme

* fixing setting default theme in content

* Move entity and tile spawn window logic to controllers

* Add 2 TODOs

* Merge fixes

* Add IOnStateChanged for UI controllers

* Fix inventory window being slow to open
Caches resources when the UI theme is changed

* Remove caching on theme change
The real fix was fixing the path for resources

* Remove test method

* Fix crash when controllers implement non generic interfaces

* Add controllers frame update

* Split UserInterfaceManager into partials

- Created UI screen

* Converted more UI managers into partials

* Setup UIScreen manager system

* Added some widget utility funcs


updated adding widgets

* Started removing HUDManager

* Moved UiController Manager to Partials


Finished moving all UIController code to UIManager

* Fixed screen loading

* Fixed Screen scaling

* Fixed Screen scaling


cleanup

* wat

* IwantToDie

* Fixed resolving ResourceCache instead of IResourceCache

* Split IOnStateChanged into IOnStateEntered and IOnStateExited

* Implemented helpers for adjusting UIAutoscale for screens

* Fixed autoscale, removed archiving from autoscale

* Implemented popups and adjusted some stuff

* Fixing some popup related shinanegans

* Fixing some draw order issues

* fixing dumb shit

* Fix indentation in UserInterfaceManager.Input.cs

* Moved screen setup to post init (run after content)

* Fix updating theme

* Merge fixes

* Fix resolving sprite system on control creation

* Fix min size of tile spawn window

* Add UIController.Initialize method

* https://tenor.com/view/minor-spelling-mistake-gif-21179057

* Add doc comment to UIController

* Split UIController.cs and UISystemDependency.cs into their own files

* Add more documentation to ui controllers

* Add AttributeUsage to UISystemDependencyAttribute

* Fix method naming

* Add documentation for assigners

* Return casted widgets where relevant

* Fix entity spawner scroll (#1)

* Add CloseOnClick and CloseOnEscape for popups

* Remove named windows and popups

* Cleanup controller code

* Add IOnStateChanged, IOnSystemChanged, IOnSystemLoaded, IOnSystemUnloaded

* Add more docs to state and system change interfaces

* Fixing Window issues

* Fixing some window fuckery

* Added OnOpen event to windows, updated sandbox window

Sandbox windows now persist values and positions

* Recurse through controls to register widgets (#2)

* Allow path to be folder

* Fix local player shutdown

* Fixing escape menu

* Fix backing field in DataDefinition.Emitters

* Ent+Tile spawn no crash

* Skip no-spawn in entity spawn menu

Co-authored-by: Jezithyr <jmaster9999@gmail.com>
Co-authored-by: DrSmugleaf <DrSmugleaf@users.noreply.github.com>
Co-authored-by: Jezithyr <Jezithyr@gmail.com>
Co-authored-by: wrexbe <81056464+wrexbe@users.noreply.github.com>
Co-authored-by: Flipp Syder <76629141+vulppine@users.noreply.github.com>
Co-authored-by: wrexbe <wrexbe@protonmail.com>
This commit is contained in:
Jezithyr
2022-09-04 16:10:54 -07:00
committed by GitHub
parent 9f651646d7
commit 710371d7d1
59 changed files with 2994 additions and 1581 deletions

View File

@@ -0,0 +1,3 @@
- type: uiTheme
id: Default
path: /textures/interface/Default

View File

@@ -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;

View File

@@ -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<INetConfigurationManager>().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)
{

View File

@@ -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<EyeComponent>(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));
}
}

View File

@@ -87,6 +87,7 @@ namespace Robust.Client.Player
/// <inheritdoc />
public void Startup()
{
DebugTools.Assert(LocalPlayer == null);
LocalPlayer = new LocalPlayer();
var msgList = new MsgPlayerListReq();
@@ -97,6 +98,7 @@ namespace Robust.Client.Player
/// <inheritdoc />
public void Shutdown()
{
LocalPlayer?.DetachEntity();
LocalPlayer = null;
_sessions.Clear();
}

View File

@@ -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()
{
}
}

View File

@@ -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
{
/// <summary>
/// Screen is being (re)enabled.
/// </summary>
public abstract void Startup();
//[Optional] The UIScreen attached to this gamestate
protected virtual Type? LinkedScreenType => null;
/// <summary>
/// Screen is being disabled (NOT Destroyed).
/// Game switching to this state
/// </summary>
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();
/// <summary>
/// Game switching away from this state
/// </summary>
internal void ShutdownInternal(IUserInterfaceManager userInterfaceManager)
{
if (LinkedScreenType != null)
{
userInterfaceManager.UnloadScreen();
}
Shutdown();
}
protected abstract void Shutdown();
public virtual void FrameUpdate(FrameEventArgs e) { }
}

View File

@@ -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<StateChangedEventArgs>? OnStateChanged;
public State CurrentState { get; private set; }
@@ -42,10 +43,10 @@ namespace Robust.Client.State
var newState = _typeFactory.CreateInstance<State>(type);
var old = CurrentState;
CurrentState?.Shutdown();
CurrentState?.ShutdownInternal(_interfaceManager);
CurrentState = newState;
CurrentState.Startup();
CurrentState.StartupInternal(_interfaceManager);
OnStateChanged?.Invoke(new StateChangedEventArgs(old, CurrentState));

View File

@@ -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<IUserInterfaceManager>().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<IUserInterfaceManagerInternal>();
StyleClasses = new StyleClassCollection(this);
Children = new OrderedChildCollection(this);
Theme = UserInterfaceManagerInternal.CurrentTheme;
XamlChildren = Children;
}

View File

@@ -0,0 +1,10 @@
namespace Robust.Client.UserInterface.Controllers;
/// <summary>
/// Interface implemented by <see cref="UIController"/>s
/// Implements both <see cref="IOnStateEntered{T}"/> and <see cref="IOnStateExited{T}"/>
/// </summary>
/// <typeparam name="T">The state type</typeparam>
public interface IOnStateChanged<T> : IOnStateEntered<T>, IOnStateExited<T> where T : State.State
{
}

View File

@@ -0,0 +1,16 @@
namespace Robust.Client.UserInterface.Controllers;
/// <summary>
/// Interface implemented by <see cref="UIController"/>s
/// </summary>
/// <typeparam name="T">The state type</typeparam>
public interface IOnStateEntered<T> where T : State.State
{
/// <summary>
/// Called by <see cref="UserInterfaceManager.OnStateChanged"/>
/// on <see cref="UIController"/>s that implement this method when a state
/// of the specified type is entered
/// </summary>
/// <param name="state">The state that was entered</param>
void OnStateEntered(T state);
}

View File

@@ -0,0 +1,16 @@
namespace Robust.Client.UserInterface.Controllers;
/// <summary>
/// Interface implemented by <see cref="UIController"/>s
/// </summary>
/// <typeparam name="T">The state type</typeparam>
public interface IOnStateExited<T> where T : State.State
{
/// <summary>
/// Called by <see cref="UserInterfaceManager.OnStateChanged"/>
/// on <see cref="UIController"/>s that implement this method when a state
/// of the specified type is exited
/// </summary>
/// <param name="state">The state that was exited</param>
void OnStateExited(T state);
}

View File

@@ -0,0 +1,12 @@
using Robust.Shared.GameObjects;
namespace Robust.Client.UserInterface.Controllers;
/// <summary>
/// Interface implemented by <see cref="UIController"/>s
/// Implements both <see cref="IOnSystemLoaded{T}"/> and <see cref="IOnSystemUnloaded{T}"/>
/// </summary>
/// <typeparam name="T">The entity system type</typeparam>
public interface IOnSystemChanged<T> : IOnSystemLoaded<T>, IOnSystemUnloaded<T> where T : IEntitySystem
{
}

View File

@@ -0,0 +1,18 @@
using Robust.Shared.GameObjects;
namespace Robust.Client.UserInterface.Controllers;
/// <summary>
/// Interface implemented by <see cref="UIController"/>s
/// </summary>
/// <typeparam name="T">The entity system type</typeparam>
public interface IOnSystemLoaded<T> where T : IEntitySystem
{
/// <summary>
/// Called by <see cref="UserInterfaceManager.OnSystemLoaded"/>
/// on <see cref="UIController"/>s that implement this method when a system
/// of the specified type is loaded
/// </summary>
/// <param name="system">The system that was loaded</param>
void OnSystemLoaded(T system);
}

View File

@@ -0,0 +1,18 @@
using Robust.Shared.GameObjects;
namespace Robust.Client.UserInterface.Controllers;
/// <summary>
/// Interface implemented by <see cref="UIController"/>s
/// </summary>
/// <typeparam name="T">The entity system type</typeparam>
public interface IOnSystemUnloaded<T> where T : IEntitySystem
{
/// <summary>
/// Called by <see cref="UserInterfaceManager.OnSystemUnloaded"/>
/// on <see cref="UIController"/>s that implement this method when a system
/// of the specified type is unloaded
/// </summary>
/// <param name="system">The system that was unloaded</param>
void OnSystemUnloaded(T system);
}

View File

@@ -0,0 +1,9 @@
using Robust.Client.UserInterface.Controllers;
// ReSharper disable once CheckNamespace
namespace Robust.Client.UserInterface;
public partial interface IUserInterfaceManager
{
public T GetUIController<T>() where T : UIController, new();
}

View File

@@ -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<EntityPrototype> _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<EntitySpawnWindow>();
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<EntityPrototype>())
{
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;
}
}

View File

@@ -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<ITileDefinition> _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<TileSpawnWindow>();
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<ITileDefinition> 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<TextureResource>(path);
}
_window.TileList.AddItem(entry.Name, texture);
}
}
}

View File

@@ -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?*
/// <summary>
/// Each <see cref="UIController"/> is instantiated as a singleton by <see cref="UserInterfaceManager"/>
/// <see cref="UIController"/> can use <see cref="DependencyAttribute"/> for regular IoC dependencies
/// and <see cref="UISystemDependencyAttribute"/> to depend on <see cref="EntitySystem"/>s, which will be automatically
/// injected once they are created.
/// </summary>
public abstract class UIController
{
[Dependency] protected readonly IUserInterfaceManager UIManager = default!;
public virtual void Initialize()
{
}
public virtual void FrameUpdate(FrameEventArgs args)
{
}
}

View File

@@ -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
{
/// <summary>
/// All registered <see cref="UIController"/> instances indexed by type
/// </summary>
private readonly Dictionary<Type, UIController> _uiControllers = new();
/// <summary>
/// Implementations of <see cref="IOnStateEntered{T}"/> to invoke when a state is entered
/// State Type -> (UIController, Caller)
/// </summary>
private readonly Dictionary<Type, Dictionary<UIController, StateChangedCaller>> _onStateEnteredDelegates = new();
/// <summary>
/// Implementations of <see cref="IOnStateExited{T}"/> to invoke when a state is exited
/// State Type -> (UIController, Caller)
/// </summary>
private readonly Dictionary<Type, Dictionary<UIController, StateChangedCaller>> _onStateExitedDelegates = new();
/// <summary>
/// Implementations of <see cref="IOnSystemLoaded{T}"/> to invoke when an entity system is loaded
/// Entity System Type -> (UIController, Caller)
/// </summary>
private readonly Dictionary<Type, Dictionary<UIController, SystemChangedCaller>> _onSystemLoadedDelegates = new();
/// <summary>
/// Implementations of <see cref="IOnSystemUnloaded{T}"/> to invoke when an entity system is unloaded
/// Entity System Type -> (UIController, Caller)
/// </summary>
private readonly Dictionary<Type, Dictionary<UIController, SystemChangedCaller>> _onSystemUnloadedDelegates = new();
/// <summary>
/// Field -> Controller -> Field assigner delegate
/// </summary>
private readonly Dictionary<Type, Dictionary<Type, AssignField<UIController, object?>>> _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<State.State>.OnStateEntered), new[] {state})
?? throw new NullReferenceException();
}
else
{
onStateChangedType = typeof(IOnStateExited<>).MakeGenericType(state);
onStateChangedMethod =
controller.GetMethod(nameof(IOnStateExited<State.State>.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<StateChangedCaller>();
}
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<IEntitySystem>.OnSystemLoaded), new[] {system})
?? throw new NullReferenceException();
}
else
{
onSystemChangedType = typeof(IOnSystemUnloaded<>).MakeGenericType(system);
onSystemChangedMethod =
controller.GetMethod(nameof(IOnSystemUnloaded<IEntitySystem>.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<SystemChangedCaller>();
}
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<T>() where T : UIController, new()
{
return (T) GetUIController(typeof(T));
}
private void _setupControllers()
{
foreach (var uiControllerType in _reflectionManager.GetAllChildren<UIController>())
{
if (uiControllerType.IsAbstract)
continue;
var newController = _typeFactory.CreateInstanceUnchecked<UIController>(uiControllerType);
RegisterUIController(uiControllerType, newController);
foreach (var fieldInfo in uiControllerType.GetAllPropertiesAndFields())
{
if (!fieldInfo.HasAttribute<UISystemDependencyAttribute>())
{
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<UIController>(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);
}
}
}
}

View File

@@ -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)

View File

@@ -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;
}
}
}

View File

@@ -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
/// </remarks>
public Direction? OverrideDirection { get; set; }
public SpriteView(IEntitySystemManager sysMan)
{
_spriteSystem = sysMan.GetEntitySystem<SpriteSystem>();
RectClipContent = true;
}
public SpriteView()
{
_spriteSystem = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SpriteSystem>();
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>();
_spriteSystem?.ForceUpdate(Sprite);
renderHandle.DrawEntity(Sprite.Owner, PixelSize / 2, Scale * UIScale, OverrideDirection);
}
}

View File

@@ -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<IResourceCache>().GetResource<TextureResource>(value);
_texturePath = value;
}
}
public Vector2 Scale
{
get => _scale;

View File

@@ -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<IResourceCache>().GetResource<TextureResource>(value);
_texturePath = value;
}
}
protected override void OnThemeUpdated()
{
if (_texturePath != null) Texture = Theme.ResolveTexture(_texturePath);
base.OnThemeUpdated();
}
/// <summary>
/// Scales the texture displayed.
/// </summary>

View File

@@ -0,0 +1,12 @@
using Robust.Shared.IoC;
namespace Robust.Client.UserInterface.Controls;
[Virtual]
public abstract class UIWidget : BoxContainer
{
protected UIWidget()
{
IoCManager.InjectDependencies(this);
}
}

View File

@@ -10,31 +10,6 @@ namespace Robust.Client.UserInterface.Controls
{
Window = window;
}
/// <summary>
/// Enable the UI autoscale system, this will scale down the UI for lower resolutions
/// </summary>
[ViewVariables]
public bool AutoScale { get; set; } = false;
/// <summary>
/// Minimum resolution to start clamping autoscale to 1
/// </summary>
[ViewVariables]
public Vector2i AutoScaleUpperCutoff { get; set; } = new Vector2i(1080, 720);
/// <summary>
/// Maximum resolution to start clamping autos scale to autoscale minimum
/// </summary>
[ViewVariables]
public Vector2i AutoScaleLowerCutoff { get; set; } = new Vector2i(520, 520);
/// <summary>
/// The minimum ui scale value that autoscale will scale to
/// </summary>
[ViewVariables]
public float AutoScaleMinimum { get; set; } = 0.5f;
public override float UIScale => UIScaleSet;
internal float UIScaleSet { get; set; }
public override IClydeWindow Window { get; }

View File

@@ -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
/// </summary>
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.</param>
public void OpenCenteredAt(Vector2 relativePosition)
{
if (!_firstTimeOpened)
{
Open();
return;
}
Measure(Vector2.Infinity);
SetSize = DesiredSize;
Open();
RecenterWindow(relativePosition);
_firstTimeOpened = false;
}
/// <summary>

View File

@@ -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;
}
}

View File

@@ -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
})
}
});
}
}

View File

@@ -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<EntityPrototype> _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<EntityPrototype>())
{
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;
}
}
}
}

View File

@@ -0,0 +1,23 @@
<EntitySpawnWindow
xmlns="https://spacestation14.io"
Title="{Loc entity-spawn-window-title}"
Size="250 300"
MinSize="250 200">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal">
<LineEdit Name="SearchBar" Access="Public" HorizontalExpand="True" PlaceHolder="{Loc entity-spawn-window-search-bar-placeholder}"/>
<Button Name="ClearButton" Access="Public" Disabled="True" Text="{Loc entity-spawn-window-clear-button}" />
</BoxContainer>
<ScrollContainer Name="PrototypeScrollContainer" Access="Public" MinSize="200 0" VerticalExpand="True">
<PrototypeListContainer Name="PrototypeList" Access="Public"/>
</ScrollContainer>
<BoxContainer Orientation="Horizontal">
<Button Name="EraseButton" Access="Public" ToggleMode="True" Text="{Loc entity-spawn-window-erase-button-text}"/>
<OptionButton Name="OverrideMenu" Access="Public" HorizontalExpand="True" ToolTip="{Loc entity-spawn-window-override-menu-tooltip}" />
</BoxContainer>
<Label Name="RotationLabel" Access="Public"/>
<DoNotMeasure Visible="False">
<EntitySpawnButton Name="MeasureButton" Access="Public" />
</DoNotMeasure>
</BoxContainer>
</EntitySpawnWindow>

View File

@@ -0,0 +1,79 @@
using System.Collections.Generic;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
namespace Robust.Client.UserInterface.CustomControls
{
[GenerateTypedNameReferences]
public sealed partial class EntitySpawnWindow : DefaultWindow
{
public static readonly string[] InitOpts =
{
"Default",
"PlaceFree",
"PlaceNearby",
"SnapgridCenter",
"SnapgridBorder",
"AlignSimilar",
"AlignTileAny",
"AlignTileEmpty",
"AlignTileNonDense",
"AlignTileDense",
"AlignWall",
"AlignWallProper",
};
public EntitySpawnButton? SelectedButton;
public EntityPrototype? SelectedPrototype;
public EntitySpawnWindow()
{
RobustXamlLoader.Load(this);
MeasureButton.Measure(Vector2.Infinity);
for (var i = 0; i < InitOpts.Length; i++)
{
OverrideMenu.AddItem(InitOpts[i], i);
}
}
// Create a spawn button and insert it into the start or end of the list.
public EntitySpawnButton InsertEntityButton(EntityPrototype prototype, bool insertFirst, int index, List<Texture> textures)
{
var button = new EntitySpawnButton
{
Prototype = prototype,
Index = index // We track this index purely for debugging.
};
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 = textures;
PrototypeList.AddChild(button);
if (insertFirst)
{
button.SetPositionInParent(0);
}
return button;
}
}
}

View File

@@ -0,0 +1,71 @@
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Maths;
namespace Robust.Client.UserInterface.CustomControls;
public 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;
}
}

View File

@@ -1,168 +0,0 @@
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Enums;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Robust.Client.Graphics;
using Robust.Client.Placement;
using Robust.Client.ResourceManagement;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using static Robust.Client.UserInterface.Controls.BoxContainer;
using Robust.Shared.Utility;
namespace Robust.Client.UserInterface.CustomControls
{
public sealed class TileSpawnWindow : DefaultWindow
{
private readonly ITileDefinitionManager __tileDefinitionManager;
private readonly IPlacementManager _placementManager;
private readonly IResourceCache _resourceCache;
private ItemList TileList;
private LineEdit SearchBar;
private Button ClearButton;
private readonly List<ITileDefinition> _shownItems = new();
private bool _clearingSelections;
public TileSpawnWindow(ITileDefinitionManager tileDefinitionManager, IPlacementManager placementManager,
IResourceCache resourceCache)
{
__tileDefinitionManager = tileDefinitionManager;
_placementManager = placementManager;
_resourceCache = resourceCache;
var vBox = new BoxContainer
{
Orientation = LayoutOrientation.Vertical
};
Contents.AddChild(vBox);
var hBox = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal
};
vBox.AddChild(hBox);
SearchBar = new LineEdit {PlaceHolder = "Search", HorizontalExpand = true};
SearchBar.OnTextChanged += OnSearchBarTextChanged;
hBox.AddChild(SearchBar);
ClearButton = new Button {Text = "Clear"};
ClearButton.OnPressed += OnClearButtonPressed;
hBox.AddChild(ClearButton);
TileList = new ItemList {VerticalExpand = true};
TileList.OnItemSelected += TileListOnOnItemSelected;
TileList.OnItemDeselected += TileListOnOnItemDeselected;
vBox.AddChild(TileList);
BuildTileList();
_placementManager.PlacementChanged += OnPlacementCanceled;
OnClose += OnWindowClosed;
Title = "Place Tiles";
SearchBar.GrabKeyboardFocus();
SetSize = (300, 300);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
_placementManager.PlacementChanged -= OnPlacementCanceled;
}
}
private void OnClearButtonPressed(BaseButton.ButtonEventArgs args)
{
TileList.ClearSelected();
_placementManager.Clear();
SearchBar.Clear();
BuildTileList("");
ClearButton.Disabled = true;
}
private void OnSearchBarTextChanged(LineEdit.LineEditEventArgs args)
{
TileList.ClearSelected();
_placementManager.Clear();
BuildTileList(args.Text);
ClearButton.Disabled = string.IsNullOrEmpty(args.Text);
}
private void BuildTileList(string? searchStr = null)
{
TileList.Clear();
IEnumerable<ITileDefinition> tileDefs = __tileDefinitionManager;
if (!string.IsNullOrEmpty(searchStr))
{
tileDefs = tileDefs.Where(s =>
s.Name.IndexOf(searchStr, StringComparison.InvariantCultureIgnoreCase) >= 0 ||
s.ID.IndexOf(searchStr, StringComparison.OrdinalIgnoreCase) >= 0);
}
tileDefs = tileDefs.OrderBy(d => d.Name);
_shownItems.Clear();
_shownItems.AddRange(tileDefs);
foreach (var entry in _shownItems)
{
Texture? texture = null;
var path = entry.Sprite?.ToString();
if (path != null)
{
texture = _resourceCache.GetResource<TextureResource>(path);
}
TileList.AddItem(entry.Name, texture);
}
}
private void OnWindowClosed()
{
TileList.ClearSelected();
_placementManager.Clear();
}
private void OnPlacementCanceled(object? sender, EventArgs e)
{
_clearingSelections = true;
TileList.ClearSelected();
_clearingSelections = false;
}
private void TileListOnOnItemSelected(ItemList.ItemListSelectedEventArgs args)
{
var definition = _shownItems[args.ItemIndex];
var newObjInfo = new PlacementInformation
{
PlacementOption = "AlignTileAny",
TileType = definition.TileId,
Range = 400,
IsTile = true
};
_placementManager.BeginPlacing(newObjInfo);
}
private void TileListOnOnItemDeselected(ItemList.ItemListDeselectedEventArgs args)
{
if (_clearingSelections)
{
return;
}
_placementManager.Clear();
}
}
}

View File

@@ -0,0 +1,13 @@
<TileSpawnWindow
xmlns="https://spacestation14.io"
Title="Place Tiles"
Size="300 300"
MinSize="300 200">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal">
<LineEdit Name="SearchBar" Access="Public" HorizontalExpand="True" PlaceHolder="Search"/>
<Button Name="ClearButton" Access="Public" Text="Clear"/>
</BoxContainer>
<ItemList Name="TileList" Access="Public" VerticalExpand="True"/>
</BoxContainer>
</TileSpawnWindow>

View File

@@ -0,0 +1,13 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
namespace Robust.Client.UserInterface.CustomControls;
[GenerateTypedNameReferences]
public sealed partial class TileSpawnWindow : DefaultWindow
{
public TileSpawnWindow()
{
RobustXamlLoader.Load(this);
}
}

View File

@@ -0,0 +1,14 @@
using System;
using Robust.Client.UserInterface.Controls;
namespace Robust.Client.UserInterface;
public partial interface IUserInterfaceManager
{
public UIScreen? ActiveScreen { get; }
public void LoadScreen<T>() where T : UIScreen, new();
internal void LoadScreenInternal(Type type);
public void UnloadScreen();
public T? GetActiveUIWidgetOrNull<T>() where T : UIWidget, new();
public T GetActiveUIWidget<T>() where T : UIWidget, new();
}

View File

@@ -0,0 +1,13 @@
using Robust.Client.UserInterface.Themes;
namespace Robust.Client.UserInterface;
public partial interface IUserInterfaceManager
{
public UITheme CurrentTheme { get;}
public UITheme GetTheme(string name);
public UITheme GetThemeOrDefault(string name);
public void SetActiveTheme(string themeName);
public UITheme DefaultTheme { get; }
public void SetDefaultTheme(string themeId);
}

View File

@@ -0,0 +1,21 @@
using System;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
namespace Robust.Client.UserInterface;
public partial interface IUserInterfaceManager
{
public T CreatePopup<T>() where T : Popup, new();
public bool RemoveFirstPopup<T>() where T : Popup, new();
public bool TryGetFirstPopup<T>(out T? popup) where T : Popup, new();
public bool TryGetFirstPopup(Type type, out Popup? popup);
public bool RemoveFirstWindow<T>() where T : BaseWindow, new();
public T CreateWindow<T>() where T : BaseWindow, new();
public void ClearWindows();
public T GetFirstWindow<T>() where T : BaseWindow, new();
public bool TryGetFirstWindow<T>(out T? window) where T : BaseWindow, new();
public bool TryGetFirstWindow(Type type, out BaseWindow? window);
}

View File

@@ -8,9 +8,9 @@ using Robust.Shared.Map;
namespace Robust.Client.UserInterface
{
public interface IUserInterfaceManager
public partial interface IUserInterfaceManager
{
UITheme ThemeDefaults { get; }
InterfaceTheme ThemeDefaults { get; }
/// <summary>
/// Default style sheet that applies to all controls
@@ -33,6 +33,7 @@ namespace Robust.Client.UserInterface
/// happens. When focus is lost on a control, it always fires Control.ControlFocusExited.
/// </summary>
Control? ControlFocused { get; set; }
public void PostInitialize();
ViewportContainer MainViewport { get; }

View File

@@ -2,13 +2,13 @@ using Robust.Client.Graphics;
namespace Robust.Client.UserInterface
{
// DON'T USE THESE
// THEY'RE A BAD IDEA THAT NEEDS TO BE BURIED.
//THIS IS BEING DEPRECIATED BECAUSE IT'S ASS!
//UIThemes will be eventually fully replacing this functionality without giving you turbo space ass-cancer
/// <summary>
/// Fallback theme system for GUI.
/// </summary>
public abstract class UITheme
public abstract class InterfaceTheme
{
public abstract Font DefaultFont { get; }
public abstract Font LabelFont { get; }
@@ -17,7 +17,7 @@ namespace Robust.Client.UserInterface
public abstract StyleBox LineEditBox { get; }
}
public sealed class UIThemeDummy : UITheme
public sealed class InterfaceThemeDummy : InterfaceTheme
{
public override Font DefaultFont { get; } = new DummyFont();
public override Font LabelFont { get; } = new DummyFont();

View File

@@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Robust.Client.UserInterface.Themes;
[Prototype("uiTheme")]
public sealed class UITheme : IPrototype
{ //this is used for ease of access
public const string DefaultPath = "/Textures/Interface";
public const string DefaultName = "Default";
[ViewVariables]
[IdDataField]
public string ID { get; } = default!;
[DataField("path")]
private ResourcePath? _path;
[DataField("colors", readOnly: true)]
public Dictionary<string, Color>? Colors { get; }
public ResourcePath Path => _path == null ? new ResourcePath(DefaultPath+"/"+ID) : _path;
private void ValidateFilePath(IResourceCache resourceCache)
{
var foundFolders = resourceCache.ContentFindFiles(Path.ToRootedPath());
if (!foundFolders.Any()) throw new Exception("UITheme: "+ID+" not found in resources!");
}
//helper to autoresolve dependencies
public Texture ResolveTexture(string texturePath)
{
return ResolveTexture(IoCManager.Resolve<IResourceCache>(), texturePath);
}
public Texture ResolveTexture(IResourceCache cache, string texturePath)
{
return cache.TryGetResource<TextureResource>( new ResourcePath($"{Path}/{texturePath}.png"), out var texture) ? texture :
cache.GetResource<TextureResource>($"{DefaultPath}/{DefaultName}/{texturePath}.png");
}
public Color? ResolveColor(string colorName)
{
if (Colors == null) return null;
return Colors.TryGetValue(colorName, out var color) ? color : IoCManager.Resolve<IUserInterfaceManager>().DefaultTheme.ResolveColor(colorName);
}
public Color ResolveColorOrSpecified(string colorName, Color defaultColor = default)
{
var color = ResolveColor(colorName) ?? defaultColor;
return color;
}
}

View File

@@ -0,0 +1,189 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
namespace Robust.Client.UserInterface;
// ReSharper disable MemberCanBePrivate.Global
[PublicAPI]
public abstract class UIScreen : LayoutContainer
{
private IConfigurationManager _configManager = IoCManager.Resolve<IConfigurationManager>();
public Vector2i AutoscaleMaxResolution
{
get =>
new(_configManager.GetCVar<int>("interface.resolutionAutoScaleUpperCutoffX"),
_configManager.GetCVar<int>("interface.resolutionAutoScaleUpperCutoffY"));
protected set
{
_configManager.SetCVar("interface.resolutionAutoScaleUpperCutoffX", value.X);
_configManager.SetCVar("interface.resolutionAutoScaleUpperCutoffY", value.Y);
}
}
public Vector2i AutoscaleMinResolution
{
get
{
var configManager = IoCManager.Resolve<IConfigurationManager>();
return new Vector2i(configManager.GetCVar<int>("interface.resolutionAutoScaleLowerCutoffX"),
configManager.GetCVar<int>("interface.resolutionAutoScaleLowerCutoffY"));
}
protected set
{
_configManager.SetCVar("interface.resolutionAutoScaleLowerCutoffX", value.X);
_configManager.SetCVar("interface.resolutionAutoScaleLowerCutoffY", value.Y);
}
}
public float AutoscaleFloor
{
get
{
var configManager = IoCManager.Resolve<IConfigurationManager>();
return configManager.GetCVar<float>("interface.resolutionAutoScaleMinimum");
}
protected set { _configManager.SetCVar("interface.interface.resolutionAutoScaleMinimum", value); }
}
private readonly Dictionary<Type, UIWidget> _widgets = new();
protected UIScreen()
{
HorizontalAlignment = HAlignment.Stretch;
VerticalAlignment = VAlignment.Stretch;
}
public T RegisterWidget<T>() where T : UIWidget, new()
{
if (_widgets.ContainsKey(typeof(T))) throw new Exception("Hud Widget not found");
var newWidget = new T();
return newWidget;
}
public void RemoveWidget<T>() where T : UIWidget, new()
{
if (_widgets.TryGetValue(typeof(T), out var widget))
{
RemoveChild(widget);
}
_widgets.Remove(typeof(T));
}
internal void OnRemoved()
{
OnUnloaded();
}
internal void OnAdded()
{
OnLoaded();
}
public UIWidget? this[Type type]
{
get
{
if ((type.IsAbstract) || !typeof(UIWidget).IsAssignableFrom(type))
throw new Exception("Tried to fetch a non UI widget from UI Screen");
_widgets.TryGetValue(type, out var widget);
return widget;
}
}
public void AddWidget(UIWidget widget)
{
AddChild(widget);
}
public T? GetWidget<T>() where T : UIWidget, new()
{
return (T?) _widgets.GetValueOrDefault(typeof(T));
}
public T GetOrNewWidget<T>() where T : UIWidget, new()
{
if (!_widgets.TryGetValue(typeof(T), out var widget))
{
widget = new T();
}
return (T) widget;
}
public bool IsWidgetShown<T>() where T : UIWidget
{
return _widgets.TryGetValue(typeof(T), out var widget) && widget.Visible;
}
public void ShowWidget<T>(bool show) where T : UIWidget
{
_widgets[typeof(T)].Visible = show;
}
protected override void ChildAdded(Control newChild)
{
base.ChildAdded(newChild);
RegisterChildren(newChild);
if (newChild is not UIWidget widget) return;
if (!_widgets.TryAdd(widget.GetType(), widget))
throw new Exception("Tried to add duplicate widget to screen!");
}
private void RegisterChildren(Control control)
{
foreach (var child in control.Children)
{
RegisterChildren(child);
if (child is not UIWidget widget)
{
continue;
}
if (!_widgets.TryAdd(widget.GetType(), widget))
{
throw new Exception("Tried to add duplicate widget to screen!");
}
}
}
protected override void ChildRemoved(Control child)
{
base.ChildRemoved(child);
RemoveChildren(child);
if (child is not UIWidget widget) return;
_widgets.Remove(child.GetType());
}
private void RemoveChildren(Control control)
{
foreach (var child in control.Children)
{
RemoveChildren(child);
if (child is not UIWidget widget)
{
continue;
}
_widgets.Remove(widget.GetType());
}
}
protected void OnLoaded()
{
}
protected void OnUnloaded()
{
}
}

View File

@@ -0,0 +1,12 @@
using System;
namespace Robust.Client.UserInterface;
/// <summary>
/// Attribute applied to EntitySystem-typed fields inside UIControllers that should be
/// injected when the system becomes available.
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public sealed class UISystemDependencyAttribute : Attribute
{
}

View File

@@ -0,0 +1,526 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Input;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.ViewVariables;
namespace Robust.Client.UserInterface;
internal partial class UserInterfaceManager
{
private float _tooltipTimer;
private ICursor? _worldCursor;
private bool _needUpdateActiveCursor;
[ViewVariables] public Control? KeyboardFocused { get; private set; }
[ViewVariables] public Control? CurrentlyHovered { get; private set; } = default!;
private Control? _controlFocused;
[ViewVariables]
public Control? ControlFocused
{
get => _controlFocused;
set
{
if (_controlFocused == value)
return;
_controlFocused?.ControlFocusExited();
_controlFocused = value;
}
}
// set to null when not counting down
private float? _tooltipDelay;
private Tooltip _tooltip = default!;
private bool showingTooltip;
private Control? _suppliedTooltip;
private const float TooltipDelay = 1;
private static (Control control, Vector2 rel)? MouseFindControlAtPos(Control control, Vector2 position)
{
for (var i = control.ChildCount - 1; i >= 0; i--)
{
var child = control.GetChild(i);
if (!child.Visible || child.RectClipContent && !child.PixelRect.Contains((Vector2i) position))
{
continue;
}
var maybeFoundOnChild = MouseFindControlAtPos(child, position - child.PixelPosition);
if (maybeFoundOnChild != null)
{
return maybeFoundOnChild;
}
}
if (control.MouseFilter != Control.MouseFilterMode.Ignore && control.HasPoint(position / control.UIScale))
{
return (control, position);
}
return null;
}
public void KeyBindDown(BoundKeyEventArgs args)
{
if (args.Function == EngineKeyFunctions.CloseModals && _modalStack.Count != 0)
{
bool closedAny = false;
for (var i = _modalStack.Count - 1; i >= 0; i--)
{
var top = _modalStack[i];
if (top is not Popup {CloseOnEscape: false})
{
RemoveModal(top);
closedAny = true;
}
}
if (closedAny)
{
args.Handle();
}
return;
}
var control = ControlFocused ?? KeyboardFocused ?? MouseGetControl(args.PointerLocation);
if (control == null)
{
return;
}
var guiArgs = new GUIBoundKeyEventArgs(args.Function, args.State, args.PointerLocation, args.CanFocus,
args.PointerLocation.Position / control.UIScale - control.GlobalPosition,
args.PointerLocation.Position - control.GlobalPixelPosition);
_doGuiInput(control, guiArgs, (c, ev) => c.KeyBindDown(ev));
if (guiArgs.Handled)
{
args.Handle();
}
}
public void KeyBindUp(BoundKeyEventArgs args)
{
var control = ControlFocused ?? KeyboardFocused ?? MouseGetControl(args.PointerLocation);
if (control == null)
{
return;
}
var guiArgs = new GUIBoundKeyEventArgs(args.Function, args.State, args.PointerLocation, args.CanFocus,
args.PointerLocation.Position / control.UIScale - control.GlobalPosition,
args.PointerLocation.Position - control.GlobalPixelPosition);
_doGuiInput(control, guiArgs, (c, ev) => c.KeyBindUp(ev));
// Always mark this as handled.
// The only case it should not be is if we do not have a control to click on,
// in which case we never reach this.
args.Handle();
}
public void MouseMove(MouseMoveEventArgs mouseMoveEventArgs)
{
_resetTooltipTimer();
// Update which control is considered hovered.
var newHovered = MouseGetControl(mouseMoveEventArgs.Position);
if (newHovered != CurrentlyHovered)
{
_clearTooltip();
CurrentlyHovered?.MouseExited();
CurrentlyHovered = newHovered;
CurrentlyHovered?.MouseEntered();
if (CurrentlyHovered != null)
{
_tooltipDelay = CurrentlyHovered.TooltipDelay ?? TooltipDelay;
}
else
{
_tooltipDelay = null;
}
_needUpdateActiveCursor = true;
}
var target = ControlFocused ?? newHovered;
if (target != null)
{
var pos = mouseMoveEventArgs.Position.Position;
var guiArgs = new GUIMouseMoveEventArgs(mouseMoveEventArgs.Relative / target.UIScale,
target,
pos / target.UIScale, mouseMoveEventArgs.Position,
pos / target.UIScale - target.GlobalPosition,
pos - target.GlobalPixelPosition);
_doMouseGuiInput(target, guiArgs, (c, ev) => c.MouseMove(ev));
}
}
private void UpdateActiveCursor()
{
// Consider mouse input focus first so that dragging windows don't act up etc.
var cursorTarget = ControlFocused ?? CurrentlyHovered;
if (cursorTarget == null)
{
_clyde.SetCursor(_worldCursor);
return;
}
if (cursorTarget.CustomCursorShape != null)
{
_clyde.SetCursor(cursorTarget.CustomCursorShape);
return;
}
var shape = cursorTarget.DefaultCursorShape switch
{
Control.CursorShape.Arrow => StandardCursorShape.Arrow,
Control.CursorShape.IBeam => StandardCursorShape.IBeam,
Control.CursorShape.Hand => StandardCursorShape.Hand,
Control.CursorShape.Crosshair => StandardCursorShape.Crosshair,
Control.CursorShape.VResize => StandardCursorShape.VResize,
Control.CursorShape.HResize => StandardCursorShape.HResize,
_ => StandardCursorShape.Arrow
};
_clyde.SetCursor(_clyde.GetStandardCursor(shape));
}
public void MouseWheel(MouseWheelEventArgs args)
{
var control = MouseGetControl(args.Position);
if (control == null)
{
return;
}
args.Handle();
var pos = args.Position.Position;
var guiArgs = new GUIMouseWheelEventArgs(args.Delta, control,
pos / control.UIScale, args.Position,
pos / control.UIScale - control.GlobalPosition, pos - control.GlobalPixelPosition);
_doMouseGuiInput(control, guiArgs, (c, ev) => c.MouseWheel(ev), true);
}
public void TextEntered(TextEventArgs textEvent)
{
if (KeyboardFocused == null)
{
return;
}
var guiArgs = new GUITextEventArgs(KeyboardFocused, textEvent.CodePoint);
KeyboardFocused.TextEntered(guiArgs);
}
public ScreenCoordinates MousePositionScaled => ScreenToUIPosition(_inputManager.MouseScreenPosition);
private static void _doMouseGuiInput<T>(Control? control, T guiEvent, Action<Control, T> action,
bool ignoreStop = false)
where T : GUIMouseEventArgs
{
while (control != null)
{
guiEvent.SourceControl = control;
if (control.MouseFilter != Control.MouseFilterMode.Ignore)
{
action(control, guiEvent);
if (guiEvent.Handled || (!ignoreStop && control.MouseFilter == Control.MouseFilterMode.Stop))
{
break;
}
}
guiEvent.RelativePosition += control.Position;
guiEvent.RelativePixelPosition += control.PixelPosition;
control = control.Parent;
}
}
private static void _doGuiInput(
Control? control,
GUIBoundKeyEventArgs guiEvent,
Action<Control, GUIBoundKeyEventArgs> action,
bool ignoreStop = false)
{
while (control != null)
{
if (control.MouseFilter != Control.MouseFilterMode.Ignore)
{
action(control, guiEvent);
if (guiEvent.Handled || (!ignoreStop && control.MouseFilter == Control.MouseFilterMode.Stop))
{
break;
}
}
guiEvent.RelativePosition += control.Position;
guiEvent.RelativePixelPosition += control.PixelPosition;
control = control.Parent;
}
}
private void _clearTooltip()
{
if (!showingTooltip) return;
_tooltip.Visible = false;
if (_suppliedTooltip != null)
{
PopupRoot.RemoveChild(_suppliedTooltip);
_suppliedTooltip = null;
}
CurrentlyHovered?.PerformHideTooltip();
_resetTooltipTimer();
showingTooltip = false;
}
public void CursorChanged(Control control)
{
if (control == ControlFocused || control == CurrentlyHovered)
{
_needUpdateActiveCursor = true;
}
}
public void HideTooltipFor(Control control)
{
if (CurrentlyHovered == control)
{
_clearTooltip();
}
}
public bool HandleCanFocusDown(
ScreenCoordinates pointerPosition,
[NotNullWhen(true)] out (Control control, Vector2i rel)? hitData)
{
var hit = MouseGetControlAndRel(pointerPosition);
var pos = pointerPosition.Position;
// If we have a modal open and the mouse down was outside it, close said modal.
for (var i = _modalStack.Count - 1; i >= 0; i--)
{
var top = _modalStack[i];
var offset = pos - top.GlobalPixelPosition;
if (!top.HasPoint(offset / top.UIScale))
{
if (top.MouseFilter != Control.MouseFilterMode.Stop)
{
if (top is not Popup {CloseOnClick: false})
{
RemoveModal(top);
}
}
else
{
ControlFocused = top;
hitData = null;
return false; // prevent anything besides the top modal control from receiving input
}
}
else
{
break;
}
}
if (hit == null)
{
ReleaseKeyboardFocus();
hitData = null;
return false;
}
var (control, rel) = hit.Value;
if (control != KeyboardFocused)
{
ReleaseKeyboardFocus();
}
ControlFocused = control;
if (ControlFocused.CanKeyboardFocus && ControlFocused.KeyboardFocusOnClick)
{
ControlFocused.GrabKeyboardFocus();
}
hitData = (control, (Vector2i) rel);
return true;
}
public void HandleCanFocusUp()
{
ControlFocused = null;
}
public ScreenCoordinates ScreenToUIPosition(ScreenCoordinates coordinates)
{
if (!_windowsToRoot.TryGetValue(coordinates.Window, out var root))
return default;
return new ScreenCoordinates(coordinates.Position / root.UIScale, coordinates.Window);
}
public ICursor? WorldCursor
{
get => _worldCursor;
set
{
_worldCursor = value;
_needUpdateActiveCursor = true;
}
}
private (Control control, Vector2 rel)? MouseGetControlAndRel(ScreenCoordinates coordinates)
{
if (!_windowsToRoot.TryGetValue(coordinates.Window, out var root))
return null;
return MouseFindControlAtPos(root, coordinates.Position);
}
public Control? MouseGetControl(ScreenCoordinates coordinates)
{
return MouseGetControlAndRel(coordinates)?.control;
}
public Control? GetSuppliedTooltipFor(Control control)
{
return CurrentlyHovered == control ? _suppliedTooltip : null;
}
/// <summary>
/// Converts
/// </summary>
/// <param name="args">Event data values for a bound key state change.</param>
private bool OnUIKeyBindStateChanged(BoundKeyEventArgs args)
{
if (args.State == BoundKeyState.Down)
{
KeyBindDown(args);
}
else
{
KeyBindUp(args);
}
// If we are in a focused control or doing a CanFocus, return true
// So that InputManager doesn't propagate events to simulation.
if (!args.CanFocus && KeyboardFocused != null)
{
return true;
}
return false;
}
/// <inheritdoc />
public void GrabKeyboardFocus(Control control)
{
if (control == null)
{
throw new ArgumentNullException(nameof(control));
}
if (!control.CanKeyboardFocus)
{
throw new ArgumentException("Control cannot get keyboard focus.", nameof(control));
}
if (control == KeyboardFocused)
{
return;
}
ReleaseKeyboardFocus();
KeyboardFocused = control;
KeyboardFocused.KeyboardFocusEntered();
}
public void ReleaseKeyboardFocus()
{
var oldFocused = KeyboardFocused;
oldFocused?.KeyboardFocusExited();
KeyboardFocused = null;
}
public void ReleaseKeyboardFocus(Control ifControl)
{
if (ifControl == null)
{
throw new ArgumentNullException(nameof(ifControl));
}
if (ifControl == KeyboardFocused)
{
ReleaseKeyboardFocus();
}
}
private void _resetTooltipTimer()
{
_tooltipTimer = 0;
}
private void _showTooltip()
{
if (showingTooltip) return;
showingTooltip = true;
var hovered = CurrentlyHovered;
if (hovered == null)
{
return;
}
// show supplied tooltip if there is one
if (hovered.TooltipSupplier != null)
{
_suppliedTooltip = hovered.TooltipSupplier.Invoke(hovered);
if (_suppliedTooltip != null)
{
PopupRoot.AddChild(_suppliedTooltip);
Tooltips.PositionTooltip(_suppliedTooltip);
}
}
else if (!String.IsNullOrWhiteSpace(hovered.ToolTip))
{
// show simple tooltip if there is one
_tooltip.Visible = true;
_tooltip.Text = hovered.ToolTip;
Tooltips.PositionTooltip(_tooltip);
}
hovered.PerformShowTooltip();
}
public Vector2? CalcRelativeMousePositionFor(Control control, ScreenCoordinates mousePosScaled)
{
var (pos, window) = mousePosScaled;
var root = control.Root;
if (root?.Window == null || root.Window.Id != window)
return null;
return pos - control.GlobalPosition;
}
}

View File

@@ -0,0 +1,199 @@
using System;
using System.Collections.Generic;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Profiling;
namespace Robust.Client.UserInterface;
internal sealed partial class UserInterfaceManager
{
private readonly List<WindowRoot> _roots = new();
private readonly Dictionary<WindowId, WindowRoot> _windowsToRoot = new();
public IEnumerable<UIRoot> AllRoots => _roots;
private readonly List<Control> _modalStack = new();
private void RunMeasure(Control control)
{
if (control.IsMeasureValid || !control.IsInsideTree)
return;
if (control.Parent != null)
{
RunMeasure(control.Parent);
}
if (control is WindowRoot root)
{
control.Measure(root.Window.RenderTarget.Size / root.UIScale);
}
else if (control.PreviousMeasure.HasValue)
{
control.Measure(control.PreviousMeasure.Value);
}
}
private void RunArrange(Control control)
{
if (control.IsArrangeValid || !control.IsInsideTree)
return;
if (control.Parent != null)
{
RunArrange(control.Parent);
}
if (control is WindowRoot root)
{
control.Arrange(UIBox2.FromDimensions(Vector2.Zero, root.Window.RenderTarget.Size / root.UIScale));
}
else if (control.PreviousArrange.HasValue)
{
control.Arrange(control.PreviousArrange.Value);
}
}
public void Popup(string contents, string title = "Alert!")
{
var popup = new DefaultWindow
{
Title = title
};
popup.Contents.AddChild(new Label {Text = contents});
popup.OpenCentered();
}
public void ControlHidden(Control control)
{
// Does the same thing but it could later be changed so..
ControlRemovedFromTree(control);
}
public void ControlRemovedFromTree(Control control)
{
ReleaseKeyboardFocus(control);
RemoveModal(control);
if (control == CurrentlyHovered)
{
control.MouseExited();
CurrentlyHovered = null;
_clearTooltip();
}
if (control != ControlFocused) return;
ControlFocused = null;
}
public void PushModal(Control modal)
{
_modalStack.Add(modal);
}
public void RemoveModal(Control modal)
{
if (_modalStack.Remove(modal))
{
modal.ModalRemoved();
}
}
public void Render(IRenderHandle renderHandle)
{
// Render secondary windows LAST.
// This makes it so that (hopefully) the GPU will be done rendering secondary windows
// by the times we try to blit to them at the end of Clyde's render cycle,
// So that the GL driver doesn't have to block on glWaitSync.
foreach (var root in _roots)
{
if (root.Window != _clyde.MainWindow)
{
using var _ = _prof.Group("Window");
_prof.WriteValue("ID", ProfData.Int32((int) root.Window.Id));
renderHandle.RenderInRenderTarget(
root.Window.RenderTarget,
() => DoRender(root),
root.ActualBgColor);
}
}
using (_prof.Group("Main"))
{
DoRender(_windowsToRoot[_clyde.MainWindow.Id]);
}
void DoRender(WindowRoot root)
{
var total = 0;
_render(renderHandle, ref total, root, Vector2i.Zero, Color.White, null);
var drawingHandle = renderHandle.DrawingHandleScreen;
drawingHandle.SetTransform(Vector2.Zero, Angle.Zero, Vector2.One);
OnPostDrawUIRoot?.Invoke(new PostDrawUIRootEventArgs(root, drawingHandle));
_prof.WriteValue("Controls rendered", ProfData.Int32(total));
}
}
public void QueueStyleUpdate(Control control)
{
_styleUpdateQueue.Enqueue(control);
}
public void QueueMeasureUpdate(Control control)
{
_measureUpdateQueue.Enqueue(control);
_arrangeUpdateQueue.Enqueue(control);
}
public void QueueArrangeUpdate(Control control)
{
_arrangeUpdateQueue.Enqueue(control);
}
public WindowRoot CreateWindowRoot(IClydeWindow window)
{
if (_windowsToRoot.ContainsKey(window.Id))
{
throw new ArgumentException("Window already has a UI root.");
}
var newRoot = new WindowRoot(window)
{
MouseFilter = Control.MouseFilterMode.Ignore,
HorizontalAlignment = Control.HAlignment.Stretch,
VerticalAlignment = Control.VAlignment.Stretch,
UIScaleSet = window.ContentScale.X
};
_roots.Add(newRoot);
_windowsToRoot.Add(window.Id, newRoot);
newRoot.StyleSheetUpdate();
newRoot.InvalidateMeasure();
QueueMeasureUpdate(newRoot);
return newRoot;
}
public void DestroyWindowRoot(IClydeWindow window)
{
// Destroy window root if this window had one.
if (!_windowsToRoot.TryGetValue(window.Id, out var root))
return;
_windowsToRoot.Remove(window.Id);
_roots.Remove(root);
root.RemoveAllChildren();
}
public WindowRoot? GetWindowRoot(IClydeWindow window)
{
return !_windowsToRoot.TryGetValue(window.Id, out var root) ? null : root;
}
}

View File

@@ -0,0 +1,145 @@
using System;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Shared;
using Robust.Shared.Maths;
using Robust.Shared.ViewVariables;
namespace Robust.Client.UserInterface;
internal partial class UserInterfaceManager
{
[ViewVariables] public float DefaultUIScale => _clyde.DefaultWindowScale.X;
[ViewVariables] private Vector2i _resolutionAutoScaleUpper;
[ViewVariables] private Vector2i _resolutionAutoScaleLower;
[ViewVariables] private bool _autoScaleEnabled;
[ViewVariables] private float _resolutionAutoScaleMinValue;
private void _initScaling()
{
_clyde.OnWindowResized += WindowSizeChanged;
_clyde.OnWindowScaleChanged += WindowContentScaleChanged;
RegisterAutoscaleCVarListeners();
_uiScaleChanged(_configurationManager.GetCVar(CVars.DisplayUIScale));
}
private void _uiScaleChanged(float newValue)
{
foreach (var root in _roots)
{
UpdateUIScale(root);
}
}
private void WindowContentScaleChanged(WindowContentScaleEventArgs args)
{
if (_windowsToRoot.TryGetValue(args.Window.Id, out var root))
{
UpdateUIScale(root);
_fontManager.ClearFontCache();
}
}
private void RegisterAutoscaleCVarListeners()
{
_configurationManager.OnValueChanged(CVars.ResAutoScaleEnabled, i =>
{
_autoScaleEnabled = i;
foreach (var root in _roots)
{
root.UIScaleSet = 1;
_propagateUIScaleChanged(root);
root.InvalidateMeasure();
}
}, true);
_configurationManager.OnValueChanged(CVars.ResAutoScaleLowX, i =>
{
_resolutionAutoScaleLower.X = i;
foreach (var root in _roots)
{
UpdateUIScale(root);
}
}, true);
_configurationManager.OnValueChanged(CVars.ResAutoScaleLowY, i =>
{
_resolutionAutoScaleLower.Y = i;
foreach (var root in _roots)
{
UpdateUIScale(root);
}
}, true);
_configurationManager.OnValueChanged(CVars.ResAutoScaleUpperX, i =>
{
_resolutionAutoScaleUpper.X = i;
foreach (var root in _roots)
{
UpdateUIScale(root);
}
}, true);
_configurationManager.OnValueChanged(CVars.ResAutoScaleUpperY, i =>
{
_resolutionAutoScaleUpper.Y = i;
foreach (var root in _roots)
{
UpdateUIScale(root);
}
}, true);
_configurationManager.OnValueChanged(CVars.ResAutoScaleMin, i =>
{
_resolutionAutoScaleMinValue = i;
foreach (var root in _roots)
{
UpdateUIScale(root);
}
}, true);
}
private float CalculateAutoScale(WindowRoot root)
{
//Grab the OS UIScale or the value set through CVAR debug
var osScale = _configurationManager.GetCVar(CVars.DisplayUIScale);
osScale = osScale == 0f ? root.Window.ContentScale.X : osScale;
var windowSize = root.Window.RenderTarget.Size;
//Only run autoscale if it is enabled, otherwise default to just use OS UIScale
if (!_autoScaleEnabled && (windowSize.X <= 0 || windowSize.Y <= 0)) return osScale;
var maxScaleRes = _resolutionAutoScaleUpper;
var minScaleRes = _resolutionAutoScaleLower;
var autoScaleMin = _resolutionAutoScaleMinValue;
float scaleRatioX;
float scaleRatioY;
//Calculate the scale ratios and clamp it between the maximums and minimums
scaleRatioX = Math.Clamp(((float) windowSize.X - minScaleRes.X) / (maxScaleRes.X - minScaleRes.X) * osScale, autoScaleMin, osScale);
scaleRatioY = Math.Clamp(((float) windowSize.Y - minScaleRes.Y) / (maxScaleRes.Y - minScaleRes.Y) * osScale, autoScaleMin, osScale);
//Take the smallest UIScale value and use it for UI scaling
return Math.Min(scaleRatioX, scaleRatioY);
}
private void UpdateUIScale(WindowRoot root)
{
root.UIScaleSet = CalculateAutoScale(root);
_propagateUIScaleChanged(root);
root.InvalidateMeasure();
}
private static void _propagateUIScaleChanged(Control control)
{
control.UIScaleChanged();
foreach (var child in control.Children)
{
_propagateUIScaleChanged(child);
}
}
private void WindowSizeChanged(WindowResizedEventArgs windowResizedEventArgs)
{
if (!_windowsToRoot.TryGetValue(windowResizedEventArgs.Window.Id, out var root))
return;
UpdateUIScale(root);
root.InvalidateMeasure();
}
}

View File

@@ -0,0 +1,88 @@
using System.Collections.Generic;
using Robust.Client.UserInterface.Themes;
using Robust.Shared;
using Robust.Shared.Log;
namespace Robust.Client.UserInterface;
internal partial class UserInterfaceManager
{
private readonly Dictionary<string, UITheme> _themes = new();
public UITheme CurrentTheme { get; private set; } = default!;
private bool _defaultOverriden = false;
public UITheme DefaultTheme { get; private set; } = default!;
private void _initThemes()
{
DefaultTheme = _protoManager.Index<UITheme>(UITheme.DefaultName);
CurrentTheme = DefaultTheme;
foreach (var proto in _protoManager.EnumeratePrototypes<UITheme>())
{
_themes.Add(proto.ID, proto);
}
_configurationManager.OnValueChanged(CVars.InterfaceTheme, SetThemeOrPrevious, true);
}
//Try to set the current theme, if the theme is not found do nothing
public void SetActiveTheme(string themeName)
{
if (!_themes.TryGetValue(themeName, out var theme) || (theme == CurrentTheme)) return;
CurrentTheme = theme;
}
public void SetDefaultTheme(string themeId)
{
if (_defaultOverriden)
{
//this exists to stop people from misusing default theme
Logger.Error("Tried to set default theme twice!");
return;
}
if (!_protoManager.TryIndex(themeId, out UITheme? theme))
{
Logger.Error("Could not find UI theme prototype for ID:"+ themeId);
return;
}
DefaultTheme = theme;
UpdateTheme(theme);
_defaultOverriden = true;
}
private void UpdateTheme(UITheme newTheme)
{
if (newTheme == CurrentTheme) return; //do not update if the theme is unchanged
CurrentTheme = newTheme;
_userInterfaceManager.RootControl.ThemeUpdateRecursive();
}
//Try to set the current theme, if the theme is not found leave the previous theme
public void SetThemeOrPrevious(string name)
{
UpdateTheme(GetThemeOrCurrent(name));
}
//Try to set the current theme, if the theme is not found set the default theme
public void SetThemeOrDefault(string name)
{
UpdateTheme(GetThemeOrDefault(name));
}
public UITheme GetThemeOrCurrent(string name)
{
return !_themes.TryGetValue(name, out var theme) ? CurrentTheme : theme;
}
public UITheme GetThemeOrDefault(string name)
{
return !_themes.TryGetValue(name, out var theme) ? DefaultTheme : theme;
}
public UITheme GetTheme(string name)
{
return _themes[name];
}
}

View File

@@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.ViewVariables;
namespace Robust.Client.UserInterface;
internal partial class UserInterfaceManager
{
private UIScreen? _activeScreen;
public UIScreen? ActiveScreen
{
get => _activeScreen;
private set
{
if (_activeScreen == value) return;
_activeScreen?.OnRemoved();
_activeScreen = value;
_activeScreen?.OnAdded();
}
}
[ViewVariables] public Control ScreenRoot { get; private set; } = default!;
private readonly Dictionary<Type, UIScreen> _screens = new();
private void _initializeScreens()
{
foreach (var screenType in _reflectionManager.GetAllChildren<UIScreen>())
{
if (screenType.IsAbstract) continue;
_screens.Add(screenType, (UIScreen) _typeFactory.CreateInstance(screenType));
}
ScreenRoot = new Control
{
Name = "ScreenRoot"
};
RootControl.AddChild(ScreenRoot);
//This MUST be drawn before windowroot
ScreenRoot.SetPositionInParent(2);
}
public void LoadScreen<T>() where T : UIScreen, new()
{
((IUserInterfaceManager) this).LoadScreenInternal(typeof(T));
}
public T? GetActiveUIWidgetOrNull<T>() where T : UIWidget, new()
{
return (T?) _activeScreen?.GetWidget<T>();
}
public T GetActiveUIWidget<T>() where T : UIWidget, new()
{
if (_activeScreen == null) throw new Exception("No screen is currently active");
var widget = _activeScreen.GetWidget<T>();
if (widget == null) throw new Exception("No widget of type found in active screen");
return (T) widget;
}
void IUserInterfaceManager.LoadScreenInternal(Type type)
{
var screen = _screens[type];
ActiveScreen = screen;
ScreenRoot.AddChild(screen);
screen.HorizontalAlignment = Control.HAlignment.Stretch;
screen.VerticalAlignment = Control.VAlignment.Stretch;
}
public void UnloadScreen()
{
if (_activeScreen == null) return;
ScreenRoot.RemoveChild(_activeScreen);
_activeScreen = null;
}
}

View File

@@ -0,0 +1,118 @@
using System;
using System.Collections.Generic;
using Robust.Client.State;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Utility;
namespace Robust.Client.UserInterface;
internal partial class UserInterfaceManager
{
private readonly Dictionary<Type, Queue<BaseWindow>> _windowsByType = new();
private readonly Dictionary<Type, Queue<Popup>> _popupsByType = new();
public T CreatePopup<T>() where T : Popup, new()
{
var newPopup = _typeFactory.CreateInstance<T>();
_popupsByType.GetOrNew(typeof(T)).Enqueue(newPopup);
ModalRoot.AddChild(newPopup);
return newPopup;
}
public bool RemoveFirstPopup<T>() where T : Popup, new()
{
if (!_popupsByType.TryGetValue(typeof(T),out var popupQueue)) return false;
var oldPopup = popupQueue.Dequeue();
if (popupQueue.Count == 0)
{
_popupsByType.Remove(typeof(T));
}
oldPopup.Close();
oldPopup.Dispose();
return true;
}
public bool TryGetFirstPopup<T>(out T? popup) where T : Popup, new()
{
popup = null;
var success = _popupsByType.TryGetValue(typeof(T), out var win);
if (win is {Count: > 0})
{
popup = (T)win.Peek();
}
return success;
}
public bool TryGetFirstPopup(Type type, out Popup? popup)
{
popup = null;
if (!typeof(Popup).IsAssignableFrom(type)) return false;
if (!_popupsByType.TryGetValue(type, out var popupQueue) || popupQueue.Count == 0) return false;
popup = popupQueue.Peek();
return true;
}
public bool RemoveFirstWindow<T>() where T : BaseWindow, new()
{
if (!_windowsByType.TryGetValue(typeof(T),out var windowQueue)) return false;
var oldWindow = windowQueue.Dequeue();
if (windowQueue.Count == 0)
{
_windowsByType.Remove(typeof(T));
}
_uiManager.StateRoot.RemoveChild(oldWindow);
oldWindow.Dispose();
return true;
}
public T GetFirstWindow<T>() where T : BaseWindow, new()
{
if (!_windowsByType.TryGetValue(typeof(T), out var windowQueue) || windowQueue.Count == 0)
throw new Exception("Window of type" + typeof(T) + " not found!");
return (T)windowQueue.Peek();
}
public T CreateWindow<T>() where T : BaseWindow, new()
{
//If we sandbox this we break creating engine windows. The argument is type bounded anyway so it only accepts
//public classes that inherit from BaseWindow.
var newWindow = _typeFactory.CreateInstanceUnchecked<T>();
_windowsByType.GetOrNew(typeof(T)).Enqueue(newWindow);
return newWindow;
}
private void RegisterWindowOfType(BaseWindow window)
{
if (_windowsByType.ContainsKey(window.GetType())) return;
_windowsByType.GetOrNew(window.GetType()).Enqueue(window);
}
public bool TryGetFirstWindow<T>(out T? window) where T : BaseWindow, new()
{
window = null;
var success = _windowsByType.TryGetValue(typeof(T), out var win);
if (win is {Count: > 0})
{
window = (T)win.Peek();
}
return success;
}
public bool TryGetFirstWindow(Type type, out BaseWindow? window)
{
window = null;
if (!typeof(BaseWindow).IsAssignableFrom(type)) return false;
if (!_windowsByType.TryGetValue(type, out var winQueue) || winQueue.Count == 0) return false;
window = winQueue.Peek();
return true;
}
public void ClearWindows()
{
foreach (var data in _windowsByType)
{
data.Value.Dequeue().Dispose();
}
_windowsByType.Clear();
}
}

View File

@@ -1,15 +1,16 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Client.ResourceManagement;
using Robust.Client.State;
using Robust.Client.Timing;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.Input;
using Robust.Shared.Input.Binding;
using Robust.Shared.IoC;
@@ -17,12 +18,14 @@ using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Profiling;
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
using Robust.Shared.Timing;
using Robust.Shared.ViewVariables;
namespace Robust.Client.UserInterface
{
internal sealed class UserInterfaceManager : IUserInterfaceManagerInternal
internal sealed partial class UserInterfaceManager : IUserInterfaceManagerInternal
{
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly IFontManager _fontManager = default!;
@@ -33,11 +36,17 @@ namespace Robust.Client.UserInterface
[Dependency] private readonly IStateManager _stateManager = default!;
[Dependency] private readonly IClientNetManager _netManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly IUserInterfaceManagerInternal _userInterfaceManager = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IDynamicTypeFactoryInternal _typeFactory = default!;
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly ProfManager _prof = default!;
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
[Dependency] private readonly IEntitySystemManager _systemManager = default!;
[ViewVariables] public UITheme ThemeDefaults { get; private set; } = default!;
[ViewVariables] public InterfaceTheme ThemeDefaults { get; private set; } = default!;
[ViewVariables]
public Stylesheet? Stylesheet
{
@@ -56,27 +65,9 @@ namespace Robust.Client.UserInterface
}
}
[ViewVariables] public Control? KeyboardFocused { get; private set; }
private Control? _controlFocused;
[ViewVariables]
public Control? ControlFocused
{
get => _controlFocused;
set
{
if (_controlFocused == value)
return;
_controlFocused?.ControlFocusExited();
_controlFocused = value;
}
}
[ViewVariables] public ViewportContainer MainViewport { get; private set; } = default!;
[ViewVariables] public LayoutContainer StateRoot { get; private set; } = default!;
[ViewVariables] public PopupContainer ModalRoot { get; private set; } = default!;
[ViewVariables] public Control? CurrentlyHovered { get; private set; } = default!;
[ViewVariables] public float DefaultUIScale => _clyde.DefaultWindowScale.X;
[ViewVariables] public WindowRoot RootControl { get; private set; } = default!;
[ViewVariables] public LayoutContainer WindowRoot { get; private set; } = default!;
[ViewVariables] public LayoutContainer PopupRoot { get; private set; } = default!;
@@ -84,35 +75,19 @@ namespace Robust.Client.UserInterface
[ViewVariables] public IDebugMonitors DebugMonitors => _debugMonitors;
private DebugMonitors _debugMonitors = default!;
private readonly List<Control> _modalStack = new();
private bool _rendering = true;
private float _tooltipTimer;
// set to null when not counting down
private float? _tooltipDelay;
private Tooltip _tooltip = default!;
private bool showingTooltip;
private Control? _suppliedTooltip;
private const float TooltipDelay = 1;
private readonly Queue<Control> _styleUpdateQueue = new();
private readonly Queue<Control> _measureUpdateQueue = new();
private readonly Queue<Control> _arrangeUpdateQueue = new();
private Stylesheet? _stylesheet;
private ICursor? _worldCursor;
private bool _needUpdateActiveCursor;
private readonly List<WindowRoot> _roots = new();
private readonly Dictionary<WindowId, WindowRoot> _windowsToRoot = new();
public void Initialize()
{
_configurationManager.OnValueChanged(CVars.DisplayUIScale, _uiScaleChanged, true);
ThemeDefaults = new UIThemeDummy();
ThemeDefaults = new InterfaceThemeDummy();
_initScaling();
_setupControllers();
_initializeCommon();
DebugConsole = new DropDownDebugConsole();
@@ -136,17 +111,17 @@ namespace Robust.Client.UserInterface
disabled: session => _rendering = true));
_inputManager.UIKeyBindStateChanged += OnUIKeyBindStateChanged;
_uiScaleChanged(_configurationManager.GetCVar(CVars.DisplayUIScale));
_initThemes();
}
public void PostInitialize()
{
_initializeScreens();
_initializeControllers();
}
private void _initializeCommon()
{
RootControl = CreateWindowRoot(_clyde.MainWindow);
RootControl.Name = "MainWindowRoot";
_clyde.OnWindowResized += WindowSizeChanged;
_clyde.OnWindowScaleChanged += WindowContentScaleChanged;
_clyde.DestroyWindow += WindowDestroyed;
MainViewport = new MainViewportContainer(_eyeManager)
@@ -190,54 +165,11 @@ namespace Robust.Client.UserInterface
public void InitializeTesting()
{
ThemeDefaults = new UIThemeDummy();
ThemeDefaults = new InterfaceThemeDummy();
_initializeCommon();
}
public WindowRoot CreateWindowRoot(IClydeWindow window)
{
if (_windowsToRoot.ContainsKey(window.Id))
{
throw new ArgumentException("Window already has a UI root.");
}
var newRoot = new WindowRoot(window)
{
MouseFilter = Control.MouseFilterMode.Ignore,
HorizontalAlignment = Control.HAlignment.Stretch,
VerticalAlignment = Control.VAlignment.Stretch,
UIScaleSet = window.ContentScale.X
};
_roots.Add(newRoot);
_windowsToRoot.Add(window.Id, newRoot);
newRoot.StyleSheetUpdate();
newRoot.InvalidateMeasure();
QueueMeasureUpdate(newRoot);
return newRoot;
}
public void DestroyWindowRoot(IClydeWindow window)
{
// Destroy window root if this window had one.
if (!_windowsToRoot.TryGetValue(window.Id, out var root))
return;
_windowsToRoot.Remove(window.Id);
_roots.Remove(root);
root.RemoveAllChildren();
}
public WindowRoot? GetWindowRoot(IClydeWindow window)
{
return !_windowsToRoot.TryGetValue(window.Id, out var root) ? null : root;
}
public IEnumerable<UIRoot> AllRoots => _roots;
public event Action<PostDrawUIRootEventArgs>? OnPostDrawUIRoot;
private void WindowDestroyed(WindowDestroyedEventArgs args)
@@ -312,6 +244,8 @@ namespace Robust.Client.UserInterface
_prof.WriteValue("Total", ProfData.Int32(total));
}
UpdateControllers(args);
// count down tooltip delay if we're not showing one yet and
// are hovering the mouse over a control without moving it
if (_tooltipDelay != null && !showingTooltip)
@@ -330,442 +264,6 @@ namespace Robust.Client.UserInterface
}
}
private void RunMeasure(Control control)
{
if (control.IsMeasureValid || !control.IsInsideTree)
return;
if (control.Parent != null)
{
RunMeasure(control.Parent);
}
if (control is WindowRoot root)
{
control.Measure(root.Window.RenderTarget.Size / root.UIScale);
}
else if (control.PreviousMeasure.HasValue)
{
control.Measure(control.PreviousMeasure.Value);
}
}
private void RunArrange(Control control)
{
if (control.IsArrangeValid || !control.IsInsideTree)
return;
if (control.Parent != null)
{
RunArrange(control.Parent);
}
if (control is WindowRoot root)
{
control.Arrange(UIBox2.FromDimensions(Vector2.Zero, root.Window.RenderTarget.Size / root.UIScale));
}
else if (control.PreviousArrange.HasValue)
{
control.Arrange(control.PreviousArrange.Value);
}
}
public bool HandleCanFocusDown(
ScreenCoordinates pointerPosition,
[NotNullWhen(true)] out (Control control, Vector2i rel)? hitData)
{
var hit = MouseGetControlAndRel(pointerPosition);
var pos = pointerPosition.Position;
// If we have a modal open and the mouse down was outside it, close said modal.
while (_modalStack.Count != 0)
{
var top = _modalStack[^1];
var offset = pos - top.GlobalPixelPosition;
if (!top.HasPoint(offset / top.UIScale))
{
if (top.MouseFilter != Control.MouseFilterMode.Stop)
RemoveModal(top);
else
{
ControlFocused = top;
hitData = null;
return false; // prevent anything besides the top modal control from receiving input
}
}
else
{
break;
}
}
if (hit == null)
{
ReleaseKeyboardFocus();
hitData = null;
return false;
}
var (control, rel) = hit.Value;
if (control != KeyboardFocused)
{
ReleaseKeyboardFocus();
}
ControlFocused = control;
if (ControlFocused.CanKeyboardFocus && ControlFocused.KeyboardFocusOnClick)
{
ControlFocused.GrabKeyboardFocus();
}
hitData = (control, (Vector2i) rel);
return true;
}
public void HandleCanFocusUp()
{
ControlFocused = null;
}
public void KeyBindDown(BoundKeyEventArgs args)
{
if (args.Function == EngineKeyFunctions.CloseModals && _modalStack.Count != 0)
{
while (_modalStack.Count > 0)
{
var top = _modalStack[^1];
RemoveModal(top);
}
args.Handle();
return;
}
var control = ControlFocused ?? KeyboardFocused ?? MouseGetControl(args.PointerLocation);
if (control == null)
{
return;
}
var guiArgs = new GUIBoundKeyEventArgs(args.Function, args.State, args.PointerLocation, args.CanFocus,
args.PointerLocation.Position / control.UIScale - control.GlobalPosition,
args.PointerLocation.Position - control.GlobalPixelPosition);
_doGuiInput(control, guiArgs, (c, ev) => c.KeyBindDown(ev));
if (guiArgs.Handled)
{
args.Handle();
}
}
public void KeyBindUp(BoundKeyEventArgs args)
{
var control = ControlFocused ?? KeyboardFocused ?? MouseGetControl(args.PointerLocation);
if (control == null)
{
return;
}
var guiArgs = new GUIBoundKeyEventArgs(args.Function, args.State, args.PointerLocation, args.CanFocus,
args.PointerLocation.Position / control.UIScale - control.GlobalPosition,
args.PointerLocation.Position - control.GlobalPixelPosition);
_doGuiInput(control, guiArgs, (c, ev) => c.KeyBindUp(ev));
// Always mark this as handled.
// The only case it should not be is if we do not have a control to click on,
// in which case we never reach this.
args.Handle();
}
public void MouseMove(MouseMoveEventArgs mouseMoveEventArgs)
{
_resetTooltipTimer();
// Update which control is considered hovered.
var newHovered = MouseGetControl(mouseMoveEventArgs.Position);
if (newHovered != CurrentlyHovered)
{
_clearTooltip();
CurrentlyHovered?.MouseExited();
CurrentlyHovered = newHovered;
CurrentlyHovered?.MouseEntered();
if (CurrentlyHovered != null)
{
_tooltipDelay = CurrentlyHovered.TooltipDelay ?? TooltipDelay;
}
else
{
_tooltipDelay = null;
}
_needUpdateActiveCursor = true;
}
var target = ControlFocused ?? newHovered;
if (target != null)
{
var pos = mouseMoveEventArgs.Position.Position;
var guiArgs = new GUIMouseMoveEventArgs(mouseMoveEventArgs.Relative / target.UIScale,
target,
pos / target.UIScale, mouseMoveEventArgs.Position,
pos / target.UIScale - target.GlobalPosition,
pos - target.GlobalPixelPosition);
_doMouseGuiInput(target, guiArgs, (c, ev) => c.MouseMove(ev));
}
}
private void UpdateActiveCursor()
{
// Consider mouse input focus first so that dragging windows don't act up etc.
var cursorTarget = ControlFocused ?? CurrentlyHovered;
if (cursorTarget == null)
{
_clyde.SetCursor(_worldCursor);
return;
}
if (cursorTarget.CustomCursorShape != null)
{
_clyde.SetCursor(cursorTarget.CustomCursorShape);
return;
}
var shape = cursorTarget.DefaultCursorShape switch
{
Control.CursorShape.Arrow => StandardCursorShape.Arrow,
Control.CursorShape.IBeam => StandardCursorShape.IBeam,
Control.CursorShape.Hand => StandardCursorShape.Hand,
Control.CursorShape.Crosshair => StandardCursorShape.Crosshair,
Control.CursorShape.VResize => StandardCursorShape.VResize,
Control.CursorShape.HResize => StandardCursorShape.HResize,
_ => StandardCursorShape.Arrow
};
_clyde.SetCursor(_clyde.GetStandardCursor(shape));
}
public void MouseWheel(MouseWheelEventArgs args)
{
var control = MouseGetControl(args.Position);
if (control == null)
{
return;
}
args.Handle();
var pos = args.Position.Position;
var guiArgs = new GUIMouseWheelEventArgs(args.Delta, control,
pos / control.UIScale, args.Position,
pos / control.UIScale - control.GlobalPosition, pos - control.GlobalPixelPosition);
_doMouseGuiInput(control, guiArgs, (c, ev) => c.MouseWheel(ev), true);
}
public void TextEntered(TextEventArgs textEvent)
{
if (KeyboardFocused == null)
{
return;
}
var guiArgs = new GUITextEventArgs(KeyboardFocused, textEvent.CodePoint);
KeyboardFocused.TextEntered(guiArgs);
}
public void Popup(string contents, string title = "Alert!")
{
var popup = new DefaultWindow
{
Title = title
};
popup.Contents.AddChild(new Label {Text = contents});
popup.OpenCentered();
}
public Control? MouseGetControl(ScreenCoordinates coordinates)
{
return MouseGetControlAndRel(coordinates)?.control;
}
private (Control control, Vector2 rel)? MouseGetControlAndRel(ScreenCoordinates coordinates)
{
if (!_windowsToRoot.TryGetValue(coordinates.Window, out var root))
return null;
return MouseFindControlAtPos(root, coordinates.Position);
}
public ScreenCoordinates MousePositionScaled => ScreenToUIPosition(_inputManager.MouseScreenPosition);
public ScreenCoordinates ScreenToUIPosition(ScreenCoordinates coordinates)
{
if (!_windowsToRoot.TryGetValue(coordinates.Window, out var root))
return default;
return new ScreenCoordinates(coordinates.Position / root.UIScale, coordinates.Window);
}
/// <inheritdoc />
public void GrabKeyboardFocus(Control control)
{
if (control == null)
{
throw new ArgumentNullException(nameof(control));
}
if (!control.CanKeyboardFocus)
{
throw new ArgumentException("Control cannot get keyboard focus.", nameof(control));
}
if (control == KeyboardFocused)
{
return;
}
ReleaseKeyboardFocus();
KeyboardFocused = control;
KeyboardFocused.KeyboardFocusEntered();
}
public void ReleaseKeyboardFocus()
{
var oldFocused = KeyboardFocused;
oldFocused?.KeyboardFocusExited();
KeyboardFocused = null;
}
public void ReleaseKeyboardFocus(Control ifControl)
{
if (ifControl == null)
{
throw new ArgumentNullException(nameof(ifControl));
}
if (ifControl == KeyboardFocused)
{
ReleaseKeyboardFocus();
}
}
public ICursor? WorldCursor
{
get => _worldCursor;
set
{
_worldCursor = value;
_needUpdateActiveCursor = true;
}
}
public void ControlHidden(Control control)
{
// Does the same thing but it could later be changed so..
ControlRemovedFromTree(control);
}
public void ControlRemovedFromTree(Control control)
{
ReleaseKeyboardFocus(control);
RemoveModal(control);
if (control == CurrentlyHovered)
{
control.MouseExited();
CurrentlyHovered = null;
_clearTooltip();
}
if (control != ControlFocused) return;
ControlFocused = null;
}
public void PushModal(Control modal)
{
_modalStack.Add(modal);
}
public void RemoveModal(Control modal)
{
if (_modalStack.Remove(modal))
{
modal.ModalRemoved();
}
}
public void Render(IRenderHandle renderHandle)
{
// Render secondary windows LAST.
// This makes it so that (hopefully) the GPU will be done rendering secondary windows
// by the times we try to blit to them at the end of Clyde's render cycle,
// So that the GL driver doesn't have to block on glWaitSync.
foreach (var root in _roots)
{
if (root.Window != _clyde.MainWindow)
{
using var _ = _prof.Group("Window");
_prof.WriteValue("ID", ProfData.Int32((int) root.Window.Id));
renderHandle.RenderInRenderTarget(
root.Window.RenderTarget,
() => DoRender(root),
root.ActualBgColor);
}
}
using (_prof.Group("Main"))
{
DoRender(_windowsToRoot[_clyde.MainWindow.Id]);
}
void DoRender(WindowRoot root)
{
var total = 0;
_render(renderHandle, ref total, root, Vector2i.Zero, Color.White, null);
var drawingHandle = renderHandle.DrawingHandleScreen;
drawingHandle.SetTransform(Vector2.Zero, Angle.Zero, Vector2.One);
OnPostDrawUIRoot?.Invoke(new PostDrawUIRootEventArgs(root, drawingHandle));
_prof.WriteValue("Controls rendered", ProfData.Int32(total));
}
}
public void QueueStyleUpdate(Control control)
{
_styleUpdateQueue.Enqueue(control);
}
public void QueueMeasureUpdate(Control control)
{
_measureUpdateQueue.Enqueue(control);
_arrangeUpdateQueue.Enqueue(control);
}
public void QueueArrangeUpdate(Control control)
{
_arrangeUpdateQueue.Enqueue(control);
}
public void CursorChanged(Control control)
{
if (control == ControlFocused || control == CurrentlyHovered)
{
_needUpdateActiveCursor = true;
}
}
private void _render(IRenderHandle renderHandle, ref int total, Control control, Vector2i position, Color modulate,
UIBox2i? scissorBox)
{
@@ -838,243 +336,11 @@ namespace Robust.Client.UserInterface
}
}
private static (Control control, Vector2 rel)? MouseFindControlAtPos(Control control, Vector2 position)
{
for (var i = control.ChildCount - 1; i >= 0; i--)
{
var child = control.GetChild(i);
if (!child.Visible || child.RectClipContent && !child.PixelRect.Contains((Vector2i) position))
{
continue;
}
var maybeFoundOnChild = MouseFindControlAtPos(child, position - child.PixelPosition);
if (maybeFoundOnChild != null)
{
return maybeFoundOnChild;
}
}
if (control.MouseFilter != Control.MouseFilterMode.Ignore && control.HasPoint(position / control.UIScale))
{
return (control, position);
}
return null;
}
private static void _doMouseGuiInput<T>(Control? control, T guiEvent, Action<Control, T> action,
bool ignoreStop = false)
where T : GUIMouseEventArgs
{
while (control != null)
{
guiEvent.SourceControl = control;
if (control.MouseFilter != Control.MouseFilterMode.Ignore)
{
action(control, guiEvent);
if (guiEvent.Handled || (!ignoreStop && control.MouseFilter == Control.MouseFilterMode.Stop))
{
break;
}
}
guiEvent.RelativePosition += control.Position;
guiEvent.RelativePixelPosition += control.PixelPosition;
control = control.Parent;
}
}
private static void _doGuiInput(
Control? control,
GUIBoundKeyEventArgs guiEvent,
Action<Control, GUIBoundKeyEventArgs> action,
bool ignoreStop = false)
{
while (control != null)
{
if (control.MouseFilter != Control.MouseFilterMode.Ignore)
{
action(control, guiEvent);
if (guiEvent.Handled || (!ignoreStop && control.MouseFilter == Control.MouseFilterMode.Stop))
{
break;
}
}
guiEvent.RelativePosition += control.Position;
guiEvent.RelativePixelPosition += control.PixelPosition;
control = control.Parent;
}
}
private void _clearTooltip()
{
if (!showingTooltip) return;
_tooltip.Visible = false;
if (_suppliedTooltip != null)
{
PopupRoot.RemoveChild(_suppliedTooltip);
_suppliedTooltip = null;
}
CurrentlyHovered?.PerformHideTooltip();
_resetTooltipTimer();
showingTooltip = false;
}
public void HideTooltipFor(Control control)
{
if (CurrentlyHovered == control)
{
_clearTooltip();
}
}
public Control? GetSuppliedTooltipFor(Control control)
{
return CurrentlyHovered == control ? _suppliedTooltip : null;
}
public Vector2? CalcRelativeMousePositionFor(Control control, ScreenCoordinates mousePosScaled)
{
var (pos, window) = mousePosScaled;
var root = control.Root;
if (root?.Window == null || root.Window.Id != window)
return null;
return pos - control.GlobalPosition;
}
public Color GetMainClearColor() => RootControl.ActualBgColor;
private void _resetTooltipTimer()
~UserInterfaceManager()
{
_tooltipTimer = 0;
}
private void _showTooltip()
{
if (showingTooltip) return;
showingTooltip = true;
var hovered = CurrentlyHovered;
if (hovered == null)
{
return;
}
// show supplied tooltip if there is one
if (hovered.TooltipSupplier != null)
{
_suppliedTooltip = hovered.TooltipSupplier.Invoke(hovered);
if (_suppliedTooltip != null)
{
PopupRoot.AddChild(_suppliedTooltip);
Tooltips.PositionTooltip(_suppliedTooltip);
}
}
else if (!String.IsNullOrWhiteSpace(hovered.ToolTip))
{
// show simple tooltip if there is one
_tooltip.Visible = true;
_tooltip.Text = hovered.ToolTip;
Tooltips.PositionTooltip(_tooltip);
}
hovered.PerformShowTooltip();
}
private void _uiScaleChanged(float newValue)
{
foreach (var root in _roots)
{
UpdateUIScale(root);
}
}
private void WindowContentScaleChanged(WindowContentScaleEventArgs args)
{
if (_windowsToRoot.TryGetValue(args.Window.Id, out var root))
{
UpdateUIScale(root);
_fontManager.ClearFontCache();
}
}
private float CalculateAutoScale(WindowRoot root)
{
//Grab the OS UIScale or the value set through CVAR debug
var osScale = _configurationManager.GetCVar(CVars.DisplayUIScale);
osScale = osScale == 0f ? root.Window.ContentScale.X : osScale;
var windowSize = root.Window.RenderTarget.Size;
//Only run autoscale if it is enabled, otherwise default to just use OS UIScale
if (!root.AutoScale && (windowSize.X <= 0 || windowSize.Y <= 0)) return osScale;
var maxScaleRes = root.AutoScaleUpperCutoff;
var minScaleRes = root.AutoScaleLowerCutoff;
var autoScaleMin = root.AutoScaleMinimum;
float scaleRatioX;
float scaleRatioY;
//Calculate the scale ratios and clamp it between the maximums and minimums
scaleRatioX = Math.Clamp(((float) windowSize.X - minScaleRes.X) / (maxScaleRes.X - minScaleRes.X) * osScale, autoScaleMin, osScale);
scaleRatioY = Math.Clamp(((float) windowSize.Y - minScaleRes.Y) / (maxScaleRes.Y - minScaleRes.Y) * osScale, autoScaleMin, osScale);
//Take the smallest UIScale value and use it for UI scaling
return Math.Min(scaleRatioX, scaleRatioY);
}
private void UpdateUIScale(WindowRoot root)
{
root.UIScaleSet = CalculateAutoScale(root);
_propagateUIScaleChanged(root);
root.InvalidateMeasure();
}
private static void _propagateUIScaleChanged(Control control)
{
control.UIScaleChanged();
foreach (var child in control.Children)
{
_propagateUIScaleChanged(child);
}
}
private void WindowSizeChanged(WindowResizedEventArgs windowResizedEventArgs)
{
if (!_windowsToRoot.TryGetValue(windowResizedEventArgs.Window.Id, out var root))
return;
UpdateUIScale(root);
root.InvalidateMeasure();
}
/// <summary>
/// Converts
/// </summary>
/// <param name="args">Event data values for a bound key state change.</param>
private bool OnUIKeyBindStateChanged(BoundKeyEventArgs args)
{
if (args.State == BoundKeyState.Down)
{
KeyBindDown(args);
}
else
{
KeyBindUp(args);
}
// If we are in a focused control or doing a CanFocus, return true
// So that InputManager doesn't propagate events to simulation.
if (!args.CanFocus && KeyboardFocused != null)
{
return true;
}
return false;
ClearWindows();
}
}
}

View File

@@ -0,0 +1,28 @@
using JetBrains.Annotations;
using Robust.Client.UserInterface.Themes;
using Robust.Shared.IoC;
namespace Robust.Client.UserInterface.XAML;
[PublicAPI]
public sealed class UiTexExtension
{
public string Path { get; }
public UITheme Theme { get; }
public UiTexExtension(string path)
{
Path = path;
Theme = IoCManager.Resolve<IUserInterfaceManager>().CurrentTheme;
}
//Support for forcing a theme
public UiTexExtension(UITheme theme, string path)
{
Path = path;
Theme = theme;
}
public object ProvideValue()
{
return Theme.ResolveTexture(Path);
}
}

View File

@@ -18,6 +18,7 @@ namespace Robust.Server.Prototypes
public ServerPrototypeManager()
{
RegisterIgnore("shader");
RegisterIgnore("uiTheme");
}
public override void Initialize()

View File

@@ -1036,6 +1036,56 @@ namespace Robust.Shared
public static readonly CVarDef<float> MaxAngVelocity =
CVarDef.Create("physics.maxangvelocity", 15f);
/*
* User interface
*/
/// <summary>
/// Change the UITheme
/// </summary>
public static readonly CVarDef<string> InterfaceTheme =
CVarDef.Create("interface.theme", "", CVar.CLIENTONLY | CVar.ARCHIVE);
/// <summary>
///Minimum resolution to start clamping autoscale to 1
/// </summary>
public static readonly CVarDef<int> ResAutoScaleUpperX =
CVarDef.Create("interface.resolutionAutoScaleUpperCutoffX",1080 , CVar.CLIENTONLY);
/// <summary>
///Minimum resolution to start clamping autoscale to 1
/// </summary>
public static readonly CVarDef<int> ResAutoScaleUpperY =
CVarDef.Create("interface.resolutionAutoScaleUpperCutoffY",720 , CVar.CLIENTONLY);
/// <summary>
///Maximum resolution to start clamping autos scale to autoscale minimum
/// </summary>
public static readonly CVarDef<int> ResAutoScaleLowX =
CVarDef.Create("interface.resolutionAutoScaleLowerCutoffX",520 , CVar.CLIENTONLY);
/// <summary>
///Maximum resolution to start clamping autos scale to autoscale minimum
/// </summary>
public static readonly CVarDef<int> ResAutoScaleLowY =
CVarDef.Create("interface.resolutionAutoScaleLowerCutoffY",520 , CVar.CLIENTONLY);
/// <summary>
/// The minimum ui scale value that autoscale will scale to
/// </summary>
public static readonly CVarDef<float> ResAutoScaleMin =
CVarDef.Create("interface.resolutionAutoScaleMinimum",0.5f , CVar.CLIENTONLY);
/// <summary>
///Enable the UI autoscale system on this control, this will scale down the UI for lower resolutions
/// </summary>
public static readonly CVarDef<bool> ResAutoScaleEnabled =
CVarDef.Create("interface.resolutionAutoScaleEnabled",true , CVar.CLIENTONLY | CVar.ARCHIVE);
/*
* DISCORD
*/

View File

@@ -27,6 +27,6 @@ namespace Robust.Shared.Serialization.Manager.Definition
private delegate TValue AccessField<TTarget, TValue>(ref TTarget target);
private delegate void AssignField<TTarget, TValue>(ref TTarget target, TValue? value);
internal delegate void AssignField<TTarget, TValue>(ref TTarget target, TValue? value);
}
}

View File

@@ -205,7 +205,7 @@ namespace Robust.Shared.Serialization.Manager.Definition
return CopyDelegate;
}
private void EmitSetField(RobustILGenerator rGenerator, AbstractFieldInfo info)
private static void EmitSetField(RobustILGenerator rGenerator, AbstractFieldInfo info)
{
switch (info)
{
@@ -275,12 +275,12 @@ namespace Robust.Shared.Serialization.Manager.Definition
return method.CreateDelegate<AccessField<object, object?>>();
}
private AssignField<object, object?> EmitFieldAssigner(FieldDefinition fieldDefinition)
internal static AssignField<T, object?> EmitFieldAssigner<T>(Type type, Type fieldType, AbstractFieldInfo backingField)
{
var method = new DynamicMethod(
"AssignField",
typeof(void),
new[] {typeof(object).MakeByRefType(), typeof(object)},
new[] {typeof(T).MakeByRefType(), typeof(object)},
true);
method.DefineParameter(1, ParameterAttributes.Out, "target");
@@ -288,22 +288,22 @@ namespace Robust.Shared.Serialization.Manager.Definition
var generator = method.GetRobustGen();
if (Type.IsValueType)
if (type.IsValueType)
{
generator.DeclareLocal(Type);
generator.DeclareLocal(type);
generator.Emit(OpCodes.Ldarg_0);
generator.Emit(OpCodes.Ldind_Ref);
generator.Emit(OpCodes.Unbox_Any, Type);
generator.Emit(OpCodes.Unbox_Any, type);
generator.Emit(OpCodes.Stloc_0);
generator.Emit(OpCodes.Ldloca, 0);
generator.Emit(OpCodes.Ldarg_1);
generator.Emit(OpCodes.Unbox_Any, fieldDefinition.FieldType);
generator.Emit(OpCodes.Unbox_Any, fieldType);
EmitSetField(generator, fieldDefinition.BackingField);
EmitSetField(generator, backingField);
generator.Emit(OpCodes.Ldarg_0);
generator.Emit(OpCodes.Ldloc_0);
generator.Emit(OpCodes.Box, Type);
generator.Emit(OpCodes.Box, type);
generator.Emit(OpCodes.Stind_Ref);
generator.Emit(OpCodes.Ret);
@@ -312,16 +312,16 @@ namespace Robust.Shared.Serialization.Manager.Definition
{
generator.Emit(OpCodes.Ldarg_0);
generator.Emit(OpCodes.Ldind_Ref);
generator.Emit(OpCodes.Castclass, Type);
generator.Emit(OpCodes.Castclass, type);
generator.Emit(OpCodes.Ldarg_1);
generator.Emit(OpCodes.Unbox_Any, fieldDefinition.FieldType);
generator.Emit(OpCodes.Unbox_Any, fieldType);
EmitSetField(generator, fieldDefinition.BackingField.GetBackingField() ?? fieldDefinition.BackingField);
EmitSetField(generator, backingField.GetBackingField() ?? backingField);
generator.Emit(OpCodes.Ret);
}
return method.CreateDelegate<AssignField<object, object?>>();
return method.CreateDelegate<AssignField<T, object?>>();
}
}
}

View File

@@ -75,7 +75,7 @@ namespace Robust.Shared.Serialization.Manager.Definition
var fieldDefinition = BaseFieldDefinitions[i];
fieldAccessors[i] = EmitFieldAccessor(fieldDefinition);
fieldAssigners[i] = EmitFieldAssigner(fieldDefinition);
fieldAssigners[i] = EmitFieldAssigner<object>(Type, fieldDefinition.FieldType, fieldDefinition.BackingField);
if (fieldDefinition.Attribute.CustomTypeSerializer != null)
{

View File

@@ -43,11 +43,21 @@ namespace Robust.Shared.Serialization.TypeSerializers.Implementations
path = path.ToRootedPath();
try
{
return dependencies.Resolve<IResourceManager>().ContentFileExists(path)
? new ValidatedValueNode(node)
: new ErrorNode(node, $"File not found. ({path})");
var resourceManager = dependencies.Resolve<IResourceManager>();
if(resourceManager.ContentFileExists(path))
{
return new ValidatedValueNode(node);
}
if (node.Value.EndsWith(path.Separator) && resourceManager.ContentGetDirectoryEntries(path).Any())
{
return new ValidatedValueNode(node);
}
return new ErrorNode(node, $"File not found. ({path})");
}
catch (Exception e)
{