mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-14 19:29:36 +01:00
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:
3
Resources/EnginePrototypes/UserInterface/uiThemes.yml
Normal file
3
Resources/EnginePrototypes/UserInterface/uiThemes.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
- type: uiTheme
|
||||
id: Default
|
||||
path: /textures/interface/Default
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) { }
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
10
Robust.Client/UserInterface/Controllers/IOnStateChanged.cs
Normal file
10
Robust.Client/UserInterface/Controllers/IOnStateChanged.cs
Normal 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
|
||||
{
|
||||
}
|
||||
16
Robust.Client/UserInterface/Controllers/IOnStateEntered.cs
Normal file
16
Robust.Client/UserInterface/Controllers/IOnStateEntered.cs
Normal 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);
|
||||
}
|
||||
16
Robust.Client/UserInterface/Controllers/IOnStateExited.cs
Normal file
16
Robust.Client/UserInterface/Controllers/IOnStateExited.cs
Normal 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);
|
||||
}
|
||||
12
Robust.Client/UserInterface/Controllers/IOnSystemChanged.cs
Normal file
12
Robust.Client/UserInterface/Controllers/IOnSystemChanged.cs
Normal 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
|
||||
{
|
||||
}
|
||||
18
Robust.Client/UserInterface/Controllers/IOnSystemLoaded.cs
Normal file
18
Robust.Client/UserInterface/Controllers/IOnSystemLoaded.cs
Normal 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);
|
||||
}
|
||||
18
Robust.Client/UserInterface/Controllers/IOnSystemUnloaded.cs
Normal file
18
Robust.Client/UserInterface/Controllers/IOnSystemUnloaded.cs
Normal 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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
25
Robust.Client/UserInterface/Controllers/UIController.cs
Normal file
25
Robust.Client/UserInterface/Controllers/UIController.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
12
Robust.Client/UserInterface/Controls/UIWidget.cs
Normal file
12
Robust.Client/UserInterface/Controls/UIWidget.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
@@ -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>
|
||||
|
||||
11
Robust.Client/UserInterface/CustomControls/DoNotMeasure.cs
Normal file
11
Robust.Client/UserInterface/CustomControls/DoNotMeasure.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
14
Robust.Client/UserInterface/IUserInterfaceManager.Screens.cs
Normal file
14
Robust.Client/UserInterface/IUserInterfaceManager.Screens.cs
Normal 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();
|
||||
}
|
||||
13
Robust.Client/UserInterface/IUserInterfaceManager.Themes.cs
Normal file
13
Robust.Client/UserInterface/IUserInterfaceManager.Themes.cs
Normal 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);
|
||||
}
|
||||
21
Robust.Client/UserInterface/IUserInterfaceManager.Windows.cs
Normal file
21
Robust.Client/UserInterface/IUserInterfaceManager.Windows.cs
Normal 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);
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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();
|
||||
59
Robust.Client/UserInterface/Themes/UiTheme.cs
Normal file
59
Robust.Client/UserInterface/Themes/UiTheme.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
189
Robust.Client/UserInterface/UIScreen.cs
Normal file
189
Robust.Client/UserInterface/UIScreen.cs
Normal 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()
|
||||
{
|
||||
}
|
||||
}
|
||||
12
Robust.Client/UserInterface/UISystemDependencyAttribute.cs
Normal file
12
Robust.Client/UserInterface/UISystemDependencyAttribute.cs
Normal 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
|
||||
{
|
||||
}
|
||||
526
Robust.Client/UserInterface/UserInterfaceManager.Input.cs
Normal file
526
Robust.Client/UserInterface/UserInterfaceManager.Input.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
199
Robust.Client/UserInterface/UserInterfaceManager.Layout.cs
Normal file
199
Robust.Client/UserInterface/UserInterfaceManager.Layout.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
145
Robust.Client/UserInterface/UserInterfaceManager.Scaling.cs
Normal file
145
Robust.Client/UserInterface/UserInterfaceManager.Scaling.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
88
Robust.Client/UserInterface/UserInterfaceManager.Themes.cs
Normal file
88
Robust.Client/UserInterface/UserInterfaceManager.Themes.cs
Normal 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];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
118
Robust.Client/UserInterface/UserInterfaceManager.Windows.cs
Normal file
118
Robust.Client/UserInterface/UserInterfaceManager.Windows.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
28
Robust.Client/UserInterface/XAML/UiTextureExtension.cs
Normal file
28
Robust.Client/UserInterface/XAML/UiTextureExtension.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ namespace Robust.Server.Prototypes
|
||||
public ServerPrototypeManager()
|
||||
{
|
||||
RegisterIgnore("shader");
|
||||
RegisterIgnore("uiTheme");
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?>>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user