mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
Merge branch 'robust-client-CEF'
This commit is contained in:
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -16,3 +16,6 @@
|
||||
[submodule "Linguini"]
|
||||
path = Linguini
|
||||
url = https://github.com/space-wizards/Linguini
|
||||
[submodule "cefglue"]
|
||||
path = cefglue
|
||||
url = https://gitlab.com/xiliumhq/chromiumembedded/cefglue/
|
||||
|
||||
@@ -5551,5 +5551,10 @@ namespace OpenToolkit.GraphicsLibraryFramework
|
||||
{
|
||||
return glfwGetX11Window(window);
|
||||
}
|
||||
|
||||
public static unsafe IntPtr GetWin32Window(Window* window)
|
||||
{
|
||||
return glfwGetWin32Window(window);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,5 +406,8 @@ namespace OpenToolkit.GraphicsLibraryFramework
|
||||
|
||||
[DllImport(LibraryName)]
|
||||
public static extern uint glfwGetX11Window(Window* window);
|
||||
|
||||
[DllImport(LibraryName)]
|
||||
public static extern IntPtr glfwGetWin32Window(Window* window);
|
||||
}
|
||||
}
|
||||
|
||||
578
Robust.Client.CEF/BrowserControl.cs
Normal file
578
Robust.Client.CEF/BrowserControl.cs
Normal file
@@ -0,0 +1,578 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using Xilium.CefGlue;
|
||||
using static Robust.Client.CEF.CefKeyCodes;
|
||||
using static Robust.Client.CEF.CefKeyCodes.ChromiumKeyboardCode;
|
||||
using static Robust.Client.Input.Keyboard;
|
||||
|
||||
namespace Robust.Client.CEF
|
||||
{
|
||||
// Funny browser control to integrate in UI.
|
||||
public class BrowserControl : Control, IBrowserControl, IRawInputControl
|
||||
{
|
||||
private const int ScrollSpeed = 50;
|
||||
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
[Dependency] private readonly IInputManager _inputMgr = default!;
|
||||
|
||||
private RobustRequestHandler _requestHandler = new RobustRequestHandler(Logger.GetSawmill("root"));
|
||||
private LiveData? _data;
|
||||
private string _startUrl = "about:blank";
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public string Url
|
||||
{
|
||||
get => _data == null ? _startUrl : _data.Browser.GetMainFrame().Url;
|
||||
set
|
||||
{
|
||||
if (_data == null)
|
||||
_startUrl = value;
|
||||
else
|
||||
_data.Browser.GetMainFrame().LoadUrl(value);
|
||||
}
|
||||
}
|
||||
|
||||
[ViewVariables] public bool IsLoading => _data?.Browser.IsLoading ?? false;
|
||||
|
||||
private readonly Dictionary<Key, ChromiumKeyboardCode> _keyMap = new()
|
||||
{
|
||||
[Key.A] = VKEY_A,
|
||||
[Key.B] = VKEY_B,
|
||||
[Key.C] = VKEY_C,
|
||||
[Key.D] = VKEY_D,
|
||||
[Key.E] = VKEY_E,
|
||||
[Key.F] = VKEY_F,
|
||||
[Key.G] = VKEY_G,
|
||||
[Key.H] = VKEY_H,
|
||||
[Key.I] = VKEY_I,
|
||||
[Key.J] = VKEY_J,
|
||||
[Key.K] = VKEY_K,
|
||||
[Key.L] = VKEY_L,
|
||||
[Key.M] = VKEY_M,
|
||||
[Key.N] = VKEY_N,
|
||||
[Key.O] = VKEY_O,
|
||||
[Key.P] = VKEY_P,
|
||||
[Key.Q] = VKEY_Q,
|
||||
[Key.R] = VKEY_R,
|
||||
[Key.S] = VKEY_S,
|
||||
[Key.T] = VKEY_T,
|
||||
[Key.U] = VKEY_U,
|
||||
[Key.V] = VKEY_V,
|
||||
[Key.W] = VKEY_W,
|
||||
[Key.X] = VKEY_X,
|
||||
[Key.Y] = VKEY_Y,
|
||||
[Key.Z] = VKEY_Z,
|
||||
[Key.Num0] = VKEY_0,
|
||||
[Key.Num1] = VKEY_1,
|
||||
[Key.Num2] = VKEY_2,
|
||||
[Key.Num3] = VKEY_3,
|
||||
[Key.Num4] = VKEY_4,
|
||||
[Key.Num5] = VKEY_5,
|
||||
[Key.Num6] = VKEY_6,
|
||||
[Key.Num7] = VKEY_7,
|
||||
[Key.Num8] = VKEY_8,
|
||||
[Key.Num9] = VKEY_9,
|
||||
[Key.NumpadNum0] = VKEY_NUMPAD0,
|
||||
[Key.NumpadNum1] = VKEY_NUMPAD1,
|
||||
[Key.NumpadNum2] = VKEY_NUMPAD2,
|
||||
[Key.NumpadNum3] = VKEY_NUMPAD3,
|
||||
[Key.NumpadNum4] = VKEY_NUMPAD4,
|
||||
[Key.NumpadNum5] = VKEY_NUMPAD5,
|
||||
[Key.NumpadNum6] = VKEY_NUMPAD6,
|
||||
[Key.NumpadNum7] = VKEY_NUMPAD7,
|
||||
[Key.NumpadNum8] = VKEY_NUMPAD8,
|
||||
[Key.NumpadNum9] = VKEY_NUMPAD9,
|
||||
[Key.Escape] = VKEY_ESCAPE,
|
||||
[Key.Control] = VKEY_CONTROL,
|
||||
[Key.Shift] = VKEY_SHIFT,
|
||||
[Key.Alt] = VKEY_MENU,
|
||||
[Key.LSystem] = VKEY_LWIN,
|
||||
[Key.RSystem] = VKEY_RWIN,
|
||||
[Key.LBracket] = VKEY_OEM_4,
|
||||
[Key.RBracket] = VKEY_OEM_6,
|
||||
[Key.SemiColon] = VKEY_OEM_1,
|
||||
[Key.Comma] = VKEY_OEM_COMMA,
|
||||
[Key.Period] = VKEY_OEM_PERIOD,
|
||||
[Key.Apostrophe] = VKEY_OEM_7,
|
||||
[Key.Slash] = VKEY_OEM_2,
|
||||
[Key.BackSlash] = VKEY_OEM_5,
|
||||
[Key.Tilde] = VKEY_OEM_3,
|
||||
[Key.Equal] = VKEY_OEM_PLUS,
|
||||
[Key.Space] = VKEY_SPACE,
|
||||
[Key.Return] = VKEY_RETURN,
|
||||
[Key.BackSpace] = VKEY_BACK,
|
||||
[Key.Tab] = VKEY_TAB,
|
||||
[Key.PageUp] = VKEY_PRIOR,
|
||||
[Key.PageDown] = VKEY_NEXT,
|
||||
[Key.End] = VKEY_END,
|
||||
[Key.Home] = VKEY_HOME,
|
||||
[Key.Insert] = VKEY_INSERT,
|
||||
[Key.Delete] = VKEY_DELETE,
|
||||
[Key.Minus] = VKEY_OEM_MINUS,
|
||||
[Key.NumpadAdd] = VKEY_ADD,
|
||||
[Key.NumpadSubtract] = VKEY_SUBTRACT,
|
||||
[Key.NumpadDivide] = VKEY_DIVIDE,
|
||||
[Key.NumpadMultiply] = VKEY_MULTIPLY,
|
||||
[Key.NumpadDecimal] = VKEY_DECIMAL,
|
||||
[Key.Left] = VKEY_LEFT,
|
||||
[Key.Right] = VKEY_RIGHT,
|
||||
[Key.Up] = VKEY_UP,
|
||||
[Key.Down] = VKEY_DOWN,
|
||||
[Key.F1] = VKEY_F1,
|
||||
[Key.F2] = VKEY_F2,
|
||||
[Key.F3] = VKEY_F3,
|
||||
[Key.F4] = VKEY_F4,
|
||||
[Key.F5] = VKEY_F5,
|
||||
[Key.F6] = VKEY_F6,
|
||||
[Key.F7] = VKEY_F7,
|
||||
[Key.F8] = VKEY_F8,
|
||||
[Key.F9] = VKEY_F9,
|
||||
[Key.F10] = VKEY_F10,
|
||||
[Key.F11] = VKEY_F11,
|
||||
[Key.F12] = VKEY_F12,
|
||||
[Key.F13] = VKEY_F13,
|
||||
[Key.F14] = VKEY_F14,
|
||||
[Key.F15] = VKEY_F15,
|
||||
[Key.Pause] = VKEY_PAUSE,
|
||||
};
|
||||
|
||||
public BrowserControl()
|
||||
{
|
||||
CanKeyboardFocus = true;
|
||||
KeyboardFocusOnClick = true;
|
||||
|
||||
IoCManager.InjectDependencies(this);
|
||||
}
|
||||
|
||||
protected override void EnteredTree()
|
||||
{
|
||||
base.EnteredTree();
|
||||
|
||||
DebugTools.AssertNull(_data);
|
||||
|
||||
// A funny render handler that will allow us to render to the control.
|
||||
var renderer = new ControlRenderHandler(this);
|
||||
|
||||
// A funny web cef client. This can actually be shared by multiple browsers, but I'm not sure how the
|
||||
// rendering would work in that case? TODO CEF: Investigate a way to share the web client?
|
||||
var client = new RobustCefClient(renderer, _requestHandler);
|
||||
|
||||
var info = CefWindowInfo.Create();
|
||||
|
||||
// FUNFACT: If you DO NOT set these below and set info.Width/info.Height instead, you get an external window
|
||||
// Good to know, huh? Setup is the same, except you can pass a dummy render handler to the CEF client.
|
||||
info.SetAsWindowless(IntPtr.Zero, false); // TODO CEF: Pass parent handle?
|
||||
info.WindowlessRenderingEnabled = true;
|
||||
|
||||
var settings = new CefBrowserSettings()
|
||||
{
|
||||
WindowlessFrameRate = 60
|
||||
};
|
||||
|
||||
// Create the web browser! And by default, we go to about:blank.
|
||||
var browser = CefBrowserHost.CreateBrowserSync(info, client, settings, _startUrl);
|
||||
|
||||
var texture = _clyde.CreateBlankTexture<Bgra32>(Vector2i.One);
|
||||
|
||||
_data = new LiveData(texture, client, browser, renderer);
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
base.ExitedTree();
|
||||
|
||||
DebugTools.AssertNotNull(_data);
|
||||
|
||||
_data!.Texture.Dispose();
|
||||
_data.Browser.GetHost().CloseBrowser(true);
|
||||
_data = null;
|
||||
}
|
||||
|
||||
protected internal override void MouseMove(GUIMouseMoveEventArgs args)
|
||||
{
|
||||
base.MouseMove(args);
|
||||
|
||||
if (_data == null)
|
||||
return;
|
||||
|
||||
// Logger.Debug();
|
||||
var modifiers = CalcMouseModifiers();
|
||||
var mouseEvent = new CefMouseEvent(
|
||||
(int) args.RelativePosition.X, (int) args.RelativePosition.Y,
|
||||
modifiers);
|
||||
|
||||
_data.Browser.GetHost().SendMouseMoveEvent(mouseEvent, false);
|
||||
}
|
||||
|
||||
protected internal override void MouseExited()
|
||||
{
|
||||
base.MouseExited();
|
||||
|
||||
if (_data == null)
|
||||
return;
|
||||
|
||||
var modifiers = CalcMouseModifiers();
|
||||
|
||||
_data.Browser.GetHost().SendMouseMoveEvent(new CefMouseEvent(0, 0, modifiers), true);
|
||||
}
|
||||
|
||||
protected internal override void MouseWheel(GUIMouseWheelEventArgs args)
|
||||
{
|
||||
base.MouseWheel(args);
|
||||
|
||||
if (_data == null)
|
||||
return;
|
||||
|
||||
var modifiers = CalcMouseModifiers();
|
||||
var mouseEvent = new CefMouseEvent(
|
||||
(int) args.RelativePosition.X, (int) args.RelativePosition.Y,
|
||||
modifiers);
|
||||
|
||||
_data.Browser.GetHost().SendMouseWheelEvent(
|
||||
mouseEvent,
|
||||
(int) args.Delta.X * ScrollSpeed,
|
||||
(int) args.Delta.Y * ScrollSpeed);
|
||||
}
|
||||
|
||||
bool IRawInputControl.RawKeyEvent(in GuiRawKeyEvent guiRawEvent)
|
||||
{
|
||||
if (_data == null)
|
||||
return false;
|
||||
|
||||
var host = _data.Browser.GetHost();
|
||||
|
||||
if (guiRawEvent.Key is Key.MouseLeft or Key.MouseMiddle or Key.MouseRight)
|
||||
{
|
||||
var key = guiRawEvent.Key switch
|
||||
{
|
||||
Key.MouseLeft => CefMouseButtonType.Left,
|
||||
Key.MouseMiddle => CefMouseButtonType.Middle,
|
||||
Key.MouseRight => CefMouseButtonType.Right,
|
||||
_ => default // not possible
|
||||
};
|
||||
|
||||
var mouseEvent = new CefMouseEvent(
|
||||
guiRawEvent.MouseRelative.X, guiRawEvent.MouseRelative.Y,
|
||||
CefEventFlags.None);
|
||||
|
||||
// Logger.Debug($"MOUSE: {guiRawEvent.Action} {guiRawEvent.Key} {guiRawEvent.ScanCode} {key}");
|
||||
|
||||
// TODO: double click support?
|
||||
host.SendMouseClickEvent(mouseEvent, key, guiRawEvent.Action == RawKeyAction.Up, 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: Handle left/right modifier keys??
|
||||
if (!_keyMap.TryGetValue(guiRawEvent.Key, out var vkKey))
|
||||
vkKey = default;
|
||||
|
||||
// Logger.Debug($"{guiRawEvent.Action} {guiRawEvent.Key} {guiRawEvent.ScanCode} {vkKey}");
|
||||
|
||||
var lParam = 0;
|
||||
lParam |= (guiRawEvent.ScanCode & 0xFF) << 16;
|
||||
if (guiRawEvent.Action != RawKeyAction.Down)
|
||||
lParam |= 1 << 30;
|
||||
|
||||
if (guiRawEvent.Action == RawKeyAction.Up)
|
||||
lParam |= 1 << 31;
|
||||
|
||||
var modifiers = CalcModifiers(guiRawEvent.Key);
|
||||
|
||||
host.SendKeyEvent(new CefKeyEvent
|
||||
{
|
||||
// Repeats are sent as key downs, I guess?
|
||||
EventType = guiRawEvent.Action == RawKeyAction.Up
|
||||
? CefKeyEventType.KeyUp
|
||||
: CefKeyEventType.RawKeyDown,
|
||||
NativeKeyCode = lParam,
|
||||
// NativeKeyCode = guiRawEvent.ScanCode,
|
||||
WindowsKeyCode = (int) vkKey,
|
||||
IsSystemKey = false, // TODO
|
||||
Modifiers = modifiers
|
||||
});
|
||||
|
||||
if (guiRawEvent.Action != RawKeyAction.Up && guiRawEvent.Key == Key.Return)
|
||||
{
|
||||
host.SendKeyEvent(new CefKeyEvent
|
||||
{
|
||||
EventType = CefKeyEventType.Char,
|
||||
WindowsKeyCode = '\r',
|
||||
NativeKeyCode = lParam,
|
||||
Modifiers = modifiers
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private CefEventFlags CalcModifiers(Key key)
|
||||
{
|
||||
CefEventFlags modifiers = default;
|
||||
|
||||
if (_inputMgr.IsKeyDown(Key.Control))
|
||||
modifiers |= CefEventFlags.ControlDown;
|
||||
|
||||
if (_inputMgr.IsKeyDown(Key.Alt))
|
||||
modifiers |= CefEventFlags.AltDown;
|
||||
|
||||
if (_inputMgr.IsKeyDown(Key.Shift))
|
||||
modifiers |= CefEventFlags.ShiftDown;
|
||||
|
||||
if (_inputMgr.IsKeyDown(Key.Shift))
|
||||
modifiers |= CefEventFlags.ShiftDown;
|
||||
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
private CefEventFlags CalcMouseModifiers()
|
||||
{
|
||||
CefEventFlags modifiers = default;
|
||||
|
||||
if (_inputMgr.IsKeyDown(Key.Control))
|
||||
modifiers |= CefEventFlags.ControlDown;
|
||||
|
||||
if (_inputMgr.IsKeyDown(Key.Alt))
|
||||
modifiers |= CefEventFlags.AltDown;
|
||||
|
||||
if (_inputMgr.IsKeyDown(Key.Shift))
|
||||
modifiers |= CefEventFlags.ShiftDown;
|
||||
|
||||
if (_inputMgr.IsKeyDown(Key.Shift))
|
||||
modifiers |= CefEventFlags.ShiftDown;
|
||||
|
||||
if (_inputMgr.IsKeyDown(Key.MouseLeft))
|
||||
modifiers |= CefEventFlags.LeftMouseButton;
|
||||
|
||||
if (_inputMgr.IsKeyDown(Key.MouseMiddle))
|
||||
modifiers |= CefEventFlags.MiddleMouseButton;
|
||||
|
||||
if (_inputMgr.IsKeyDown(Key.MouseRight))
|
||||
modifiers |= CefEventFlags.RightMouseButton;
|
||||
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
protected internal override void TextEntered(GUITextEventArgs args)
|
||||
{
|
||||
base.TextEntered(args);
|
||||
|
||||
if (_data == null)
|
||||
return;
|
||||
|
||||
var host = _data.Browser.GetHost();
|
||||
|
||||
Span<char> buf = stackalloc char[2];
|
||||
var written = args.AsRune.EncodeToUtf16(buf);
|
||||
|
||||
for (var i = 0; i < written; i++)
|
||||
{
|
||||
host.SendKeyEvent(new CefKeyEvent
|
||||
{
|
||||
EventType = CefKeyEventType.Char,
|
||||
WindowsKeyCode = buf[i],
|
||||
Character = buf[i],
|
||||
UnmodifiedCharacter = buf[i]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Resized()
|
||||
{
|
||||
base.Resized();
|
||||
|
||||
if (_data == null)
|
||||
return;
|
||||
|
||||
_data.Browser.GetHost().NotifyMoveOrResizeStarted();
|
||||
_data.Browser.GetHost().WasResized();
|
||||
_data.Texture.Dispose();
|
||||
_data.Texture = _clyde.CreateBlankTexture<Bgra32>((PixelWidth, PixelHeight));
|
||||
}
|
||||
|
||||
protected internal override void Draw(DrawingHandleScreen handle)
|
||||
{
|
||||
base.Draw(handle);
|
||||
|
||||
if (_data == null)
|
||||
return;
|
||||
|
||||
var bufImg = _data.Renderer.Buffer.Buffer;
|
||||
|
||||
_data.Texture.SetSubImage(
|
||||
Vector2i.Zero,
|
||||
bufImg,
|
||||
new UIBox2i(
|
||||
0, 0,
|
||||
Math.Min(PixelWidth, bufImg.Width),
|
||||
Math.Min(PixelHeight, bufImg.Height)));
|
||||
|
||||
handle.DrawTexture(_data.Texture, Vector2.Zero);
|
||||
}
|
||||
|
||||
public void StopLoad()
|
||||
{
|
||||
if (_data == null)
|
||||
throw new InvalidOperationException();
|
||||
|
||||
_data.Browser.StopLoad();
|
||||
}
|
||||
|
||||
public void Reload()
|
||||
{
|
||||
if (_data == null)
|
||||
throw new InvalidOperationException();
|
||||
|
||||
_data.Browser.Reload();
|
||||
}
|
||||
|
||||
public bool GoBack()
|
||||
{
|
||||
if (_data == null)
|
||||
throw new InvalidOperationException();
|
||||
|
||||
if (!_data.Browser.CanGoBack)
|
||||
return false;
|
||||
|
||||
_data.Browser.GoBack();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool GoForward()
|
||||
{
|
||||
if (_data == null)
|
||||
throw new InvalidOperationException();
|
||||
|
||||
if (!_data.Browser.CanGoForward)
|
||||
return false;
|
||||
|
||||
_data.Browser.GoForward();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void ExecuteJavaScript(string code)
|
||||
{
|
||||
if (_data == null)
|
||||
throw new InvalidOperationException();
|
||||
|
||||
_data.Browser.GetMainFrame().ExecuteJavaScript(code, string.Empty, 1);
|
||||
}
|
||||
|
||||
public void AddResourceRequestHandler(Action<RequestHandlerContext> handler)
|
||||
{
|
||||
_requestHandler.AddHandler(handler);
|
||||
}
|
||||
|
||||
public void RemoveResourceRequestHandler(Action<RequestHandlerContext> handler)
|
||||
{
|
||||
_requestHandler.RemoveHandler(handler);
|
||||
}
|
||||
|
||||
private sealed class LiveData
|
||||
{
|
||||
public OwnedTexture Texture;
|
||||
public readonly RobustCefClient Client;
|
||||
public readonly CefBrowser Browser;
|
||||
public readonly ControlRenderHandler Renderer;
|
||||
|
||||
public LiveData(
|
||||
OwnedTexture texture,
|
||||
RobustCefClient client,
|
||||
CefBrowser browser,
|
||||
ControlRenderHandler renderer)
|
||||
{
|
||||
Texture = texture;
|
||||
Client = client;
|
||||
Browser = browser;
|
||||
Renderer = renderer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class ControlRenderHandler : CefRenderHandler
|
||||
{
|
||||
public ImageBuffer Buffer { get; }
|
||||
private Control _control;
|
||||
|
||||
internal ControlRenderHandler(Control control)
|
||||
{
|
||||
Buffer = new ImageBuffer(control);
|
||||
_control = control;
|
||||
}
|
||||
|
||||
protected override CefAccessibilityHandler? GetAccessibilityHandler() => null;
|
||||
|
||||
protected override void GetViewRect(CefBrowser browser, out CefRectangle rect)
|
||||
{
|
||||
if (_control.Disposed)
|
||||
{
|
||||
rect = new CefRectangle();
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO CEF: Do we need to pass real screen coords? Cause what we do already works...
|
||||
//var screenCoords = _control.ScreenCoordinates;
|
||||
//rect = new CefRectangle((int) screenCoords.X, (int) screenCoords.Y, (int)Math.Max(_control.Size.X, 1), (int)Math.Max(_control.Size.Y, 1));
|
||||
|
||||
// We do the max between size and 1 because it will LITERALLY CRASH WITHOUT AN ERROR otherwise.
|
||||
rect = new CefRectangle(0, 0, (int) Math.Max(_control.Size.X, 1), (int) Math.Max(_control.Size.Y, 1));
|
||||
}
|
||||
|
||||
protected override bool GetScreenInfo(CefBrowser browser, CefScreenInfo screenInfo)
|
||||
{
|
||||
if (_control.Disposed)
|
||||
return false;
|
||||
|
||||
// TODO CEF: Get actual scale factor?
|
||||
screenInfo.DeviceScaleFactor = 1.0f;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnPopupSize(CefBrowser browser, CefRectangle rect)
|
||||
{
|
||||
if (_control.Disposed)
|
||||
return;
|
||||
}
|
||||
|
||||
protected override void OnPaint(CefBrowser browser, CefPaintElementType type, CefRectangle[] dirtyRects,
|
||||
IntPtr buffer, int width, int height)
|
||||
{
|
||||
if (_control.Disposed)
|
||||
return;
|
||||
|
||||
foreach (var dirtyRect in dirtyRects)
|
||||
{
|
||||
Buffer.UpdateBuffer(width, height, buffer, dirtyRect);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnAcceleratedPaint(CefBrowser browser, CefPaintElementType type,
|
||||
CefRectangle[] dirtyRects, IntPtr sharedHandle)
|
||||
{
|
||||
// Unused, but we're forced to implement it so.. NOOP.
|
||||
}
|
||||
|
||||
protected override void OnScrollOffsetChanged(CefBrowser browser, double x, double y)
|
||||
{
|
||||
if (_control.Disposed)
|
||||
return;
|
||||
}
|
||||
|
||||
protected override void OnImeCompositionRangeChanged(CefBrowser browser, CefRange selectedRange,
|
||||
CefRectangle[] characterBounds)
|
||||
{
|
||||
if (_control.Disposed)
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
16
Robust.Client.CEF/BrowserWindowCreateParameters.cs
Normal file
16
Robust.Client.CEF/BrowserWindowCreateParameters.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace Robust.Client.CEF
|
||||
{
|
||||
public sealed class BrowserWindowCreateParameters
|
||||
{
|
||||
public int Width { get; set; }
|
||||
public int Height { get; set; }
|
||||
|
||||
public string Url { get; set; } = "about:blank";
|
||||
|
||||
public BrowserWindowCreateParameters(int width, int height)
|
||||
{
|
||||
Width = width;
|
||||
Height = height;
|
||||
}
|
||||
}
|
||||
}
|
||||
225
Robust.Client.CEF/CefKeyCodes.cs
Normal file
225
Robust.Client.CEF/CefKeyCodes.cs
Normal file
@@ -0,0 +1,225 @@
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Robust.Client.CEF
|
||||
{
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
||||
[SuppressMessage("ReSharper", "IdentifierTypo")]
|
||||
[SuppressMessage("ReSharper", "CA1069")]
|
||||
[SuppressMessage("ReSharper", "CommentTypo")]
|
||||
internal static class CefKeyCodes
|
||||
{
|
||||
// Taken from https://chromium.googlesource.com/chromium/src/+/refs/heads/main/ui/events/keycodes/keyboard_codes_posix.h
|
||||
// See also https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
|
||||
public enum ChromiumKeyboardCode
|
||||
{
|
||||
VKEY_CANCEL = 0x03,
|
||||
VKEY_BACK = 0x08,
|
||||
VKEY_TAB = 0x09,
|
||||
VKEY_BACKTAB = 0x0A,
|
||||
VKEY_CLEAR = 0x0C,
|
||||
VKEY_RETURN = 0x0D,
|
||||
VKEY_SHIFT = 0x10,
|
||||
VKEY_CONTROL = 0x11,
|
||||
VKEY_MENU = 0x12,
|
||||
VKEY_PAUSE = 0x13,
|
||||
VKEY_CAPITAL = 0x14,
|
||||
VKEY_KANA = 0x15,
|
||||
VKEY_HANGUL = 0x15,
|
||||
VKEY_PASTE = 0x16,
|
||||
VKEY_JUNJA = 0x17,
|
||||
VKEY_FINAL = 0x18,
|
||||
VKEY_HANJA = 0x19,
|
||||
VKEY_KANJI = 0x19,
|
||||
VKEY_ESCAPE = 0x1B,
|
||||
VKEY_CONVERT = 0x1C,
|
||||
VKEY_NONCONVERT = 0x1D,
|
||||
VKEY_ACCEPT = 0x1E,
|
||||
VKEY_MODECHANGE = 0x1F,
|
||||
VKEY_SPACE = 0x20,
|
||||
VKEY_PRIOR = 0x21,
|
||||
VKEY_NEXT = 0x22,
|
||||
VKEY_END = 0x23,
|
||||
VKEY_HOME = 0x24,
|
||||
VKEY_LEFT = 0x25,
|
||||
VKEY_UP = 0x26,
|
||||
VKEY_RIGHT = 0x27,
|
||||
VKEY_DOWN = 0x28,
|
||||
VKEY_SELECT = 0x29,
|
||||
VKEY_PRINT = 0x2A,
|
||||
VKEY_EXECUTE = 0x2B,
|
||||
VKEY_SNAPSHOT = 0x2C, // Print Screen / SysRq
|
||||
VKEY_INSERT = 0x2D,
|
||||
VKEY_DELETE = 0x2E,
|
||||
VKEY_HELP = 0x2F,
|
||||
VKEY_0 = 0x30,
|
||||
VKEY_1 = 0x31,
|
||||
VKEY_2 = 0x32,
|
||||
VKEY_3 = 0x33,
|
||||
VKEY_4 = 0x34,
|
||||
VKEY_5 = 0x35,
|
||||
VKEY_6 = 0x36,
|
||||
VKEY_7 = 0x37,
|
||||
VKEY_8 = 0x38,
|
||||
VKEY_9 = 0x39,
|
||||
VKEY_A = 0x41,
|
||||
VKEY_B = 0x42,
|
||||
VKEY_C = 0x43,
|
||||
VKEY_D = 0x44,
|
||||
VKEY_E = 0x45,
|
||||
VKEY_F = 0x46,
|
||||
VKEY_G = 0x47,
|
||||
VKEY_H = 0x48,
|
||||
VKEY_I = 0x49,
|
||||
VKEY_J = 0x4A,
|
||||
VKEY_K = 0x4B,
|
||||
VKEY_L = 0x4C,
|
||||
VKEY_M = 0x4D,
|
||||
VKEY_N = 0x4E,
|
||||
VKEY_O = 0x4F,
|
||||
VKEY_P = 0x50,
|
||||
VKEY_Q = 0x51,
|
||||
VKEY_R = 0x52,
|
||||
VKEY_S = 0x53,
|
||||
VKEY_T = 0x54,
|
||||
VKEY_U = 0x55,
|
||||
VKEY_V = 0x56,
|
||||
VKEY_W = 0x57,
|
||||
VKEY_X = 0x58,
|
||||
VKEY_Y = 0x59,
|
||||
VKEY_Z = 0x5A,
|
||||
VKEY_LWIN = 0x5B,
|
||||
VKEY_COMMAND = VKEY_LWIN, // Provide the Mac name for convenience.
|
||||
VKEY_RWIN = 0x5C,
|
||||
VKEY_APPS = 0x5D,
|
||||
VKEY_SLEEP = 0x5F,
|
||||
VKEY_NUMPAD0 = 0x60,
|
||||
VKEY_NUMPAD1 = 0x61,
|
||||
VKEY_NUMPAD2 = 0x62,
|
||||
VKEY_NUMPAD3 = 0x63,
|
||||
VKEY_NUMPAD4 = 0x64,
|
||||
VKEY_NUMPAD5 = 0x65,
|
||||
VKEY_NUMPAD6 = 0x66,
|
||||
VKEY_NUMPAD7 = 0x67,
|
||||
VKEY_NUMPAD8 = 0x68,
|
||||
VKEY_NUMPAD9 = 0x69,
|
||||
VKEY_MULTIPLY = 0x6A,
|
||||
VKEY_ADD = 0x6B,
|
||||
VKEY_SEPARATOR = 0x6C,
|
||||
VKEY_SUBTRACT = 0x6D,
|
||||
VKEY_DECIMAL = 0x6E,
|
||||
VKEY_DIVIDE = 0x6F,
|
||||
VKEY_F1 = 0x70,
|
||||
VKEY_F2 = 0x71,
|
||||
VKEY_F3 = 0x72,
|
||||
VKEY_F4 = 0x73,
|
||||
VKEY_F5 = 0x74,
|
||||
VKEY_F6 = 0x75,
|
||||
VKEY_F7 = 0x76,
|
||||
VKEY_F8 = 0x77,
|
||||
VKEY_F9 = 0x78,
|
||||
VKEY_F10 = 0x79,
|
||||
VKEY_F11 = 0x7A,
|
||||
VKEY_F12 = 0x7B,
|
||||
VKEY_F13 = 0x7C,
|
||||
VKEY_F14 = 0x7D,
|
||||
VKEY_F15 = 0x7E,
|
||||
VKEY_F16 = 0x7F,
|
||||
VKEY_F17 = 0x80,
|
||||
VKEY_F18 = 0x81,
|
||||
VKEY_F19 = 0x82,
|
||||
VKEY_F20 = 0x83,
|
||||
VKEY_F21 = 0x84,
|
||||
VKEY_F22 = 0x85,
|
||||
VKEY_F23 = 0x86,
|
||||
VKEY_F24 = 0x87,
|
||||
VKEY_NUMLOCK = 0x90,
|
||||
VKEY_SCROLL = 0x91,
|
||||
VKEY_LSHIFT = 0xA0,
|
||||
VKEY_RSHIFT = 0xA1,
|
||||
VKEY_LCONTROL = 0xA2,
|
||||
VKEY_RCONTROL = 0xA3,
|
||||
VKEY_LMENU = 0xA4,
|
||||
VKEY_RMENU = 0xA5,
|
||||
VKEY_BROWSER_BACK = 0xA6,
|
||||
VKEY_BROWSER_FORWARD = 0xA7,
|
||||
VKEY_BROWSER_REFRESH = 0xA8,
|
||||
VKEY_BROWSER_STOP = 0xA9,
|
||||
VKEY_BROWSER_SEARCH = 0xAA,
|
||||
VKEY_BROWSER_FAVORITES = 0xAB,
|
||||
VKEY_BROWSER_HOME = 0xAC,
|
||||
VKEY_VOLUME_MUTE = 0xAD,
|
||||
VKEY_VOLUME_DOWN = 0xAE,
|
||||
VKEY_VOLUME_UP = 0xAF,
|
||||
VKEY_MEDIA_NEXT_TRACK = 0xB0,
|
||||
VKEY_MEDIA_PREV_TRACK = 0xB1,
|
||||
VKEY_MEDIA_STOP = 0xB2,
|
||||
VKEY_MEDIA_PLAY_PAUSE = 0xB3,
|
||||
VKEY_MEDIA_LAUNCH_MAIL = 0xB4,
|
||||
VKEY_MEDIA_LAUNCH_MEDIA_SELECT = 0xB5,
|
||||
VKEY_MEDIA_LAUNCH_APP1 = 0xB6,
|
||||
VKEY_MEDIA_LAUNCH_APP2 = 0xB7,
|
||||
VKEY_OEM_1 = 0xBA,
|
||||
VKEY_OEM_PLUS = 0xBB,
|
||||
VKEY_OEM_COMMA = 0xBC,
|
||||
VKEY_OEM_MINUS = 0xBD,
|
||||
VKEY_OEM_PERIOD = 0xBE,
|
||||
VKEY_OEM_2 = 0xBF,
|
||||
VKEY_OEM_3 = 0xC0,
|
||||
VKEY_OEM_4 = 0xDB,
|
||||
VKEY_OEM_5 = 0xDC,
|
||||
VKEY_OEM_6 = 0xDD,
|
||||
VKEY_OEM_7 = 0xDE,
|
||||
VKEY_OEM_8 = 0xDF,
|
||||
VKEY_OEM_102 = 0xE2,
|
||||
VKEY_OEM_103 = 0xE3, // GTV KEYCODE_MEDIA_REWIND
|
||||
VKEY_OEM_104 = 0xE4, // GTV KEYCODE_MEDIA_FAST_FORWARD
|
||||
VKEY_PROCESSKEY = 0xE5,
|
||||
VKEY_PACKET = 0xE7,
|
||||
VKEY_OEM_ATTN = 0xF0, // JIS DomKey::ALPHANUMERIC
|
||||
VKEY_OEM_FINISH = 0xF1, // JIS DomKey::KATAKANA
|
||||
VKEY_OEM_COPY = 0xF2, // JIS DomKey::HIRAGANA
|
||||
VKEY_DBE_SBCSCHAR = 0xF3, // JIS DomKey::HANKAKU
|
||||
VKEY_DBE_DBCSCHAR = 0xF4, // JIS DomKey::ZENKAKU
|
||||
VKEY_OEM_BACKTAB = 0xF5, // JIS DomKey::ROMAJI
|
||||
VKEY_ATTN = 0xF6, // DomKey::ATTN or JIS DomKey::KANA_MODE
|
||||
VKEY_CRSEL = 0xF7,
|
||||
VKEY_EXSEL = 0xF8,
|
||||
VKEY_EREOF = 0xF9,
|
||||
VKEY_PLAY = 0xFA,
|
||||
VKEY_ZOOM = 0xFB,
|
||||
VKEY_NONAME = 0xFC,
|
||||
VKEY_PA1 = 0xFD,
|
||||
VKEY_OEM_CLEAR = 0xFE,
|
||||
VKEY_UNKNOWN = 0,
|
||||
|
||||
// POSIX specific VKEYs. Note that as of Windows SDK 7.1, 0x97-9F, 0xD8-DA,
|
||||
// and 0xE8 are unassigned.
|
||||
VKEY_WLAN = 0x97,
|
||||
VKEY_POWER = 0x98,
|
||||
VKEY_ASSISTANT = 0x99,
|
||||
VKEY_SETTINGS = 0x9A,
|
||||
VKEY_PRIVACY_SCREEN_TOGGLE = 0x9B,
|
||||
VKEY_BRIGHTNESS_DOWN = 0xD8,
|
||||
VKEY_BRIGHTNESS_UP = 0xD9,
|
||||
VKEY_KBD_BRIGHTNESS_DOWN = 0xDA,
|
||||
VKEY_KBD_BRIGHTNESS_UP = 0xE8,
|
||||
|
||||
// Windows does not have a specific key code for AltGr. We use the unused 0xE1
|
||||
// (VK_OEM_AX) code to represent AltGr, matching the behaviour of Firefox on
|
||||
// Linux.
|
||||
VKEY_ALTGR = 0xE1,
|
||||
|
||||
// Windows does not have a specific key code for Compose. We use the unused
|
||||
// 0xE6 (VK_ICO_CLEAR) code to represent Compose.
|
||||
VKEY_COMPOSE = 0xE6,
|
||||
|
||||
// Windows does not have specific key codes for Media Play and Media Pause. We
|
||||
// use the unused 0xE9 (VK_OEM_RESET) and 0xEA (VK_OEM_JUMP) codes to
|
||||
// represent them.
|
||||
VKEY_MEDIA_PLAY = 0xE9,
|
||||
VKEY_MEDIA_PAUSE = 0xEA,
|
||||
};
|
||||
}
|
||||
}
|
||||
186
Robust.Client.CEF/CefManager.BrowserWindow.cs
Normal file
186
Robust.Client.CEF/CefManager.BrowserWindow.cs
Normal file
@@ -0,0 +1,186 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using Xilium.CefGlue;
|
||||
|
||||
namespace Robust.Client.CEF
|
||||
{
|
||||
public partial class CefManager
|
||||
{
|
||||
[Dependency] private readonly IClydeInternal _clyde = default!;
|
||||
|
||||
private readonly List<BrowserWindowImpl> _browserWindows = new();
|
||||
|
||||
public IEnumerable<IBrowserWindow> AllBrowserWindows => _browserWindows;
|
||||
|
||||
public IBrowserWindow CreateBrowserWindow(BrowserWindowCreateParameters createParams)
|
||||
{
|
||||
var mainHWnd = (_clyde.MainWindow as IClydeWindowInternal)?.WindowsHWnd ?? 0;
|
||||
|
||||
var info = CefWindowInfo.Create();
|
||||
info.Width = createParams.Width;
|
||||
info.Height = createParams.Height;
|
||||
info.SetAsPopup(mainHWnd, "ss14cef");
|
||||
|
||||
var impl = new BrowserWindowImpl(this);
|
||||
|
||||
var lifeSpanHandler = new WindowLifeSpanHandler(impl);
|
||||
var reqHandler = new RobustRequestHandler(Logger.GetSawmill("root"));
|
||||
var client = new WindowCefClient(lifeSpanHandler, reqHandler);
|
||||
var settings = new CefBrowserSettings();
|
||||
|
||||
impl.Browser = CefBrowserHost.CreateBrowserSync(info, client, settings, createParams.Url);
|
||||
impl.RequestHandler = reqHandler;
|
||||
|
||||
_browserWindows.Add(impl);
|
||||
|
||||
return impl;
|
||||
}
|
||||
|
||||
private sealed class BrowserWindowImpl : IBrowserWindow
|
||||
{
|
||||
private readonly CefManager _manager;
|
||||
internal CefBrowser Browser = default!;
|
||||
internal RobustRequestHandler RequestHandler = default!;
|
||||
|
||||
public Action<RequestHandlerContext>? OnResourceRequest { get; set; }
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public string Url
|
||||
{
|
||||
get
|
||||
{
|
||||
CheckClosed();
|
||||
return Browser.GetMainFrame().Url;
|
||||
}
|
||||
set
|
||||
{
|
||||
CheckClosed();
|
||||
Browser.GetMainFrame().LoadUrl(value);
|
||||
}
|
||||
}
|
||||
|
||||
[ViewVariables]
|
||||
public bool IsLoading
|
||||
{
|
||||
get
|
||||
{
|
||||
CheckClosed();
|
||||
return Browser.IsLoading;
|
||||
}
|
||||
}
|
||||
|
||||
public BrowserWindowImpl(CefManager manager)
|
||||
{
|
||||
_manager = manager;
|
||||
}
|
||||
|
||||
public void StopLoad()
|
||||
{
|
||||
CheckClosed();
|
||||
Browser.StopLoad();
|
||||
}
|
||||
|
||||
public void Reload()
|
||||
{
|
||||
CheckClosed();
|
||||
Browser.Reload();
|
||||
}
|
||||
|
||||
public bool GoBack()
|
||||
{
|
||||
CheckClosed();
|
||||
if (!Browser.CanGoBack)
|
||||
return false;
|
||||
|
||||
Browser.GoBack();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool GoForward()
|
||||
{
|
||||
CheckClosed();
|
||||
if (!Browser.CanGoForward)
|
||||
return false;
|
||||
|
||||
Browser.GoForward();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void ExecuteJavaScript(string code)
|
||||
{
|
||||
CheckClosed();
|
||||
Browser.GetMainFrame().ExecuteJavaScript(code, string.Empty, 1);
|
||||
}
|
||||
|
||||
public void AddResourceRequestHandler(Action<RequestHandlerContext> handler)
|
||||
{
|
||||
RequestHandler.AddHandler(handler);
|
||||
}
|
||||
|
||||
public void RemoveResourceRequestHandler(Action<RequestHandlerContext> handler)
|
||||
{
|
||||
RequestHandler.RemoveHandler(handler);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Closed)
|
||||
return;
|
||||
|
||||
Browser.GetHost().CloseBrowser(true);
|
||||
Closed = true;
|
||||
}
|
||||
|
||||
public bool Closed { get; private set; }
|
||||
|
||||
public void OnClose()
|
||||
{
|
||||
Closed = true;
|
||||
_manager._browserWindows.Remove(this);
|
||||
Logger.Debug("Removing window");
|
||||
}
|
||||
|
||||
private void CheckClosed()
|
||||
{
|
||||
if (Closed)
|
||||
throw new ObjectDisposedException("BrowserWindow");
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class WindowCefClient : CefClient
|
||||
{
|
||||
private readonly CefLifeSpanHandler _lifeSpanHandler;
|
||||
private readonly CefRequestHandler _requestHandler;
|
||||
|
||||
public WindowCefClient(CefLifeSpanHandler lifeSpanHandler, CefRequestHandler requestHandler)
|
||||
{
|
||||
_lifeSpanHandler = lifeSpanHandler;
|
||||
_requestHandler = requestHandler;
|
||||
}
|
||||
|
||||
protected override CefLifeSpanHandler GetLifeSpanHandler() => _lifeSpanHandler;
|
||||
protected override CefRequestHandler GetRequestHandler() => _requestHandler;
|
||||
}
|
||||
|
||||
private sealed class WindowLifeSpanHandler : CefLifeSpanHandler
|
||||
{
|
||||
private readonly BrowserWindowImpl _windowImpl;
|
||||
|
||||
public WindowLifeSpanHandler(BrowserWindowImpl windowImpl)
|
||||
{
|
||||
_windowImpl = windowImpl;
|
||||
}
|
||||
|
||||
protected override void OnBeforeClose(CefBrowser browser)
|
||||
{
|
||||
base.OnBeforeClose(browser);
|
||||
|
||||
_windowImpl.OnClose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
98
Robust.Client.CEF/CefManager.cs
Normal file
98
Robust.Client.CEF/CefManager.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
// The library we're using right now. TODO CEF: Do we want to use something else? We will need to ship it ourselves if so.
|
||||
using Xilium.CefGlue;
|
||||
|
||||
namespace Robust.Client.CEF
|
||||
{
|
||||
// Register this with IoC.
|
||||
// TODO CEF: think if making this inherit CefApp is a good idea...
|
||||
// TODO CEF: A way to handle external window browsers...
|
||||
[UsedImplicitly]
|
||||
public partial class CefManager
|
||||
{
|
||||
private CefApp _app = default!;
|
||||
private bool _initialized = false;
|
||||
|
||||
/// <summary>
|
||||
/// Call this to initialize CEF.
|
||||
/// </summary>
|
||||
public void Initialize()
|
||||
{
|
||||
DebugTools.Assert(!_initialized);
|
||||
|
||||
string subProcessName;
|
||||
if (OperatingSystem.IsWindows())
|
||||
subProcessName = "Robust.Client.CEF.exe";
|
||||
else if (OperatingSystem.IsLinux())
|
||||
subProcessName = "Robust.Client.CEF";
|
||||
else
|
||||
throw new NotSupportedException("Unsupported platform for CEF!");
|
||||
|
||||
var subProcessPath = PathHelpers.ExecutableRelativeFile(subProcessName);
|
||||
|
||||
var settings = new CefSettings()
|
||||
{
|
||||
WindowlessRenderingEnabled = true, // So we can render to our UI controls.
|
||||
ExternalMessagePump = false, // Unsure, honestly. TODO CEF: Research this?
|
||||
NoSandbox = true, // Not disabling the sandbox crashes CEF.
|
||||
BrowserSubprocessPath = subProcessPath,
|
||||
LocalesDirPath = Path.Combine(PathHelpers.GetExecutableDirectory(), "locales"),
|
||||
ResourcesDirPath = PathHelpers.GetExecutableDirectory(),
|
||||
};
|
||||
|
||||
Logger.Info($"CEF Version: {CefRuntime.ChromeVersion}");
|
||||
|
||||
// --------------------------- README --------------------------------------------------
|
||||
// By the way! You're gonna need the CEF binaries in your client's bin folder.
|
||||
// More specifically, version cef_binary_91.1.21+g9dd45fe+chromium-91.0.4472.114
|
||||
// https://cef-builds.spotifycdn.com/cef_binary_91.1.21%2Bg9dd45fe%2Bchromium-91.0.4472.114_windows64_minimal.tar.bz2
|
||||
// https://cef-builds.spotifycdn.com/cef_binary_91.1.21%2Bg9dd45fe%2Bchromium-91.0.4472.114_linux64_minimal.tar.bz2
|
||||
// Here's how to get it to work:
|
||||
// 1. Copy all the contents of "Release" to the bin folder.
|
||||
// 2. Copy all the contents of "Resources" to the bin folder.
|
||||
// Supposedly, you should just need libcef.so in Release and icudtl.dat in Resources...
|
||||
// The rest might be optional.
|
||||
// Maybe. Good luck! If you get odd crashes with no info and a weird exit code, use GDB!
|
||||
// -------------------------------------------------------------------------------------
|
||||
|
||||
_app = new RobustCefApp();
|
||||
|
||||
// We pass no main arguments...
|
||||
CefRuntime.Initialize(new CefMainArgs(null), settings, _app, IntPtr.Zero);
|
||||
|
||||
// TODO CEF: After this point, debugging breaks. No, literally. My client crashes but ONLY with the debugger.
|
||||
// I have tried using the DEBUG and RELEASE versions of libcef.so, stripped or non-stripped...
|
||||
// And nothing seemed to work. Odd.
|
||||
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Needs to be called regularly for CEF to keep working.
|
||||
/// </summary>
|
||||
public void Update()
|
||||
{
|
||||
DebugTools.Assert(_initialized);
|
||||
|
||||
// Calling this makes CEF do its work, without using its own update loop.
|
||||
CefRuntime.DoMessageLoopWork();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call before program shutdown.
|
||||
/// </summary>
|
||||
public void Shutdown()
|
||||
{
|
||||
DebugTools.Assert(_initialized);
|
||||
|
||||
CefRuntime.Shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
48
Robust.Client.CEF/IBrowserControl.cs
Normal file
48
Robust.Client.CEF/IBrowserControl.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
|
||||
namespace Robust.Client.CEF
|
||||
{
|
||||
public interface IBrowserControl
|
||||
{
|
||||
/// <summary>
|
||||
/// Current URL of the browser. Set to load a new page.
|
||||
/// </summary>
|
||||
string Url { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the browser is currently loading a page.
|
||||
/// </summary>
|
||||
bool IsLoading { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Stops loading the current page.
|
||||
/// </summary>
|
||||
void StopLoad();
|
||||
|
||||
/// <summary>
|
||||
/// Reload the current page.
|
||||
/// </summary>
|
||||
void Reload();
|
||||
|
||||
/// <summary>
|
||||
/// Navigate back.
|
||||
/// </summary>
|
||||
/// <returns>Whether the browser could navigate back.</returns>
|
||||
bool GoBack();
|
||||
|
||||
/// <summary>
|
||||
/// Navigate forward.
|
||||
/// </summary>
|
||||
/// <returns>Whether the browser could navigate forward.</returns>
|
||||
bool GoForward();
|
||||
|
||||
/// <summary>
|
||||
/// Execute arbitrary JavaScript on the current page.
|
||||
/// </summary>
|
||||
/// <param name="code">JavaScript code.</param>
|
||||
void ExecuteJavaScript(string code);
|
||||
|
||||
void AddResourceRequestHandler(Action<RequestHandlerContext> handler);
|
||||
void RemoveResourceRequestHandler(Action<RequestHandlerContext> handler);
|
||||
}
|
||||
}
|
||||
9
Robust.Client.CEF/IBrowserWindow.cs
Normal file
9
Robust.Client.CEF/IBrowserWindow.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using System;
|
||||
|
||||
namespace Robust.Client.CEF
|
||||
{
|
||||
public interface IBrowserWindow : IBrowserControl, IDisposable
|
||||
{
|
||||
bool Closed { get; }
|
||||
}
|
||||
}
|
||||
42
Robust.Client.CEF/ImageBuffer.cs
Normal file
42
Robust.Client.CEF/ImageBuffer.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Maths;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using Xilium.CefGlue;
|
||||
|
||||
namespace Robust.Client.CEF
|
||||
{
|
||||
internal sealed class ImageBuffer
|
||||
{
|
||||
private readonly Control _control;
|
||||
|
||||
public ImageBuffer(Control control)
|
||||
{
|
||||
_control = control;
|
||||
}
|
||||
|
||||
public Image<Bgra32> Buffer { get; private set; } = new(1, 1);
|
||||
|
||||
public unsafe void UpdateBuffer(int width, int height, IntPtr buffer, CefRectangle dirtyRect)
|
||||
{
|
||||
if (width != Buffer.Width || height != Buffer.Height)
|
||||
UpdateSize(width, height);
|
||||
|
||||
var span = new ReadOnlySpan<Bgra32>((void*) buffer, width * height);
|
||||
|
||||
ImageSharpExt.Blit(
|
||||
span,
|
||||
width,
|
||||
UIBox2i.FromDimensions(dirtyRect.X, dirtyRect.Y, dirtyRect.Width, dirtyRect.Height),
|
||||
Buffer,
|
||||
(dirtyRect.X, dirtyRect.Y));
|
||||
}
|
||||
|
||||
private void UpdateSize(int width, int height)
|
||||
{
|
||||
Buffer = new Image<Bgra32>(width, height);
|
||||
}
|
||||
}
|
||||
}
|
||||
33
Robust.Client.CEF/Program.cs
Normal file
33
Robust.Client.CEF/Program.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using Xilium.CefGlue;
|
||||
|
||||
namespace Robust.Client.CEF
|
||||
{
|
||||
public static class Program
|
||||
{
|
||||
// This was supposed to be the main entry for the subprocess program... It doesn't work.
|
||||
public static int Main(string[] args)
|
||||
{
|
||||
// This is a workaround for this to work on UNIX.
|
||||
var argv = args;
|
||||
if (CefRuntime.Platform != CefRuntimePlatform.Windows)
|
||||
{
|
||||
argv = new string[args.Length + 1];
|
||||
Array.Copy(args, 0, argv, 1, args.Length);
|
||||
argv[0] = "-";
|
||||
}
|
||||
|
||||
var mainArgs = new CefMainArgs(argv);
|
||||
|
||||
// This will block executing until the subprocess is shut down.
|
||||
var code = CefRuntime.ExecuteProcess(mainArgs, null, IntPtr.Zero);
|
||||
|
||||
if (code != 0)
|
||||
{
|
||||
System.Console.WriteLine($"CEF Subprocess exited unsuccessfully with exit code {code}! Arguments: {string.Join(' ', argv)}");
|
||||
}
|
||||
|
||||
return code;
|
||||
}
|
||||
}
|
||||
}
|
||||
56
Robust.Client.CEF/RequestHandlerContext.cs
Normal file
56
Robust.Client.CEF/RequestHandlerContext.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using Xilium.CefGlue;
|
||||
|
||||
namespace Robust.Client.CEF
|
||||
{
|
||||
public sealed class RequestHandlerContext
|
||||
{
|
||||
internal readonly CefRequest CefRequest;
|
||||
|
||||
public bool IsNavigation { get; }
|
||||
public bool IsDownload { get; }
|
||||
public string RequestInitiator { get; }
|
||||
|
||||
public string Url => CefRequest.Url;
|
||||
public string Method => CefRequest.Method;
|
||||
|
||||
public bool IsHandled { get; private set; }
|
||||
|
||||
public bool IsCancelled { get; private set; }
|
||||
|
||||
internal IRequestResult? Result { get; private set; }
|
||||
|
||||
internal RequestHandlerContext(
|
||||
bool isNavigation,
|
||||
bool isDownload,
|
||||
string requestInitiator,
|
||||
CefRequest cefRequest)
|
||||
{
|
||||
CefRequest = cefRequest;
|
||||
IsNavigation = isNavigation;
|
||||
IsDownload = isDownload;
|
||||
RequestInitiator = requestInitiator;
|
||||
}
|
||||
|
||||
public void DoCancel()
|
||||
{
|
||||
CheckNotHandled();
|
||||
|
||||
IsHandled = true;
|
||||
IsCancelled = true;
|
||||
}
|
||||
|
||||
public void DoRespondStream(Stream stream, string contentType, HttpStatusCode code = HttpStatusCode.OK)
|
||||
{
|
||||
Result = new RequestResultStream(stream, contentType, code);
|
||||
}
|
||||
|
||||
private void CheckNotHandled()
|
||||
{
|
||||
if (IsHandled)
|
||||
throw new InvalidOperationException("Request has already been handled");
|
||||
}
|
||||
}
|
||||
}
|
||||
93
Robust.Client.CEF/RequestResults.cs
Normal file
93
Robust.Client.CEF/RequestResults.cs
Normal file
@@ -0,0 +1,93 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using Xilium.CefGlue;
|
||||
|
||||
namespace Robust.Client.CEF
|
||||
{
|
||||
internal interface IRequestResult
|
||||
{
|
||||
CefResourceHandler MakeHandler();
|
||||
}
|
||||
|
||||
internal sealed class RequestResultStream : IRequestResult
|
||||
{
|
||||
private readonly Stream _stream;
|
||||
private readonly HttpStatusCode _code;
|
||||
private readonly string _contentType;
|
||||
|
||||
public RequestResultStream(Stream stream, string contentType, HttpStatusCode code)
|
||||
{
|
||||
_stream = stream;
|
||||
_code = code;
|
||||
_contentType = contentType;
|
||||
}
|
||||
|
||||
public CefResourceHandler MakeHandler()
|
||||
{
|
||||
return new Handler(_stream, _contentType, _code);
|
||||
}
|
||||
|
||||
private sealed class Handler : CefResourceHandler
|
||||
{
|
||||
// TODO: async
|
||||
// TODO: exception handling
|
||||
|
||||
private readonly Stream _stream;
|
||||
private readonly HttpStatusCode _code;
|
||||
private readonly string _contentType;
|
||||
|
||||
public Handler(Stream stream, string contentType, HttpStatusCode code)
|
||||
{
|
||||
_stream = stream;
|
||||
_code = code;
|
||||
_contentType = contentType;
|
||||
}
|
||||
|
||||
protected override bool Open(CefRequest request, out bool handleRequest, CefCallback callback)
|
||||
{
|
||||
handleRequest = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void GetResponseHeaders(CefResponse response, out long responseLength, out string? redirectUrl)
|
||||
{
|
||||
response.Status = (int) _code;
|
||||
response.StatusText = _code.ToString();
|
||||
response.MimeType = _contentType;
|
||||
|
||||
if (_stream.CanSeek)
|
||||
responseLength = _stream.Length;
|
||||
else
|
||||
responseLength = -1;
|
||||
|
||||
redirectUrl = default;
|
||||
}
|
||||
|
||||
protected override bool Skip(long bytesToSkip, out long bytesSkipped, CefResourceSkipCallback callback)
|
||||
{
|
||||
if (!_stream.CanSeek)
|
||||
{
|
||||
bytesSkipped = -2;
|
||||
return false;
|
||||
}
|
||||
|
||||
bytesSkipped = _stream.Seek(bytesToSkip, SeekOrigin.Begin);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override unsafe bool Read(IntPtr dataOut, int bytesToRead, out int bytesRead, CefResourceReadCallback callback)
|
||||
{
|
||||
var byteSpan = new Span<byte>((void*) dataOut, bytesToRead);
|
||||
|
||||
bytesRead = _stream.Read(byteSpan);
|
||||
|
||||
return bytesRead != 0;
|
||||
}
|
||||
|
||||
protected override void Cancel()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
25
Robust.Client.CEF/Robust.Client.CEF.csproj
Normal file
25
Robust.Client.CEF/Robust.Client.CEF.csproj
Normal file
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\MSBuild\Robust.Properties.targets" />
|
||||
<Import Project="..\MSBuild\Robust.Engine.props" />
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<OutputType>WinExe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="..\MSBuild\Robust.DefineConstants.targets" />
|
||||
<Target Name="RobustAfterBuild" AfterTargets="Build" />
|
||||
<Import Project="..\MSBuild\Robust.Engine.targets" />
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2020.3.0" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="5.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\cefglue\CefGlue\CefGlue.csproj" />
|
||||
<ProjectReference Include="..\Robust.Client\Robust.Client.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
48
Robust.Client.CEF/RobustCefApp.cs
Normal file
48
Robust.Client.CEF/RobustCefApp.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Xilium.CefGlue;
|
||||
|
||||
namespace Robust.Client.CEF
|
||||
{
|
||||
internal class RobustCefApp : CefApp
|
||||
{
|
||||
private readonly BrowserProcessHandler _browserProcessHandler = new();
|
||||
private readonly RenderProcessHandler _renderProcessHandler = new();
|
||||
|
||||
protected override CefBrowserProcessHandler GetBrowserProcessHandler()
|
||||
{
|
||||
return _browserProcessHandler;
|
||||
}
|
||||
|
||||
protected override CefRenderProcessHandler GetRenderProcessHandler()
|
||||
{
|
||||
return _renderProcessHandler;
|
||||
}
|
||||
|
||||
protected override void OnBeforeCommandLineProcessing(string processType, CefCommandLine commandLine)
|
||||
{
|
||||
// Disable zygote on Linux.
|
||||
commandLine.AppendSwitch("--no-zygote");
|
||||
|
||||
//commandLine.AppendSwitch("--disable-gpu");
|
||||
//commandLine.AppendSwitch("--disable-gpu-compositing");
|
||||
//commandLine.AppendSwitch("--in-process-gpu");
|
||||
|
||||
commandLine.AppendSwitch("disable-threaded-scrolling", "1");
|
||||
commandLine.AppendSwitch("disable-features", "TouchpadAndWheelScrollLatching,AsyncWheelEvents");
|
||||
|
||||
if(IoCManager.Instance != null)
|
||||
Logger.Debug($"{commandLine}");
|
||||
}
|
||||
|
||||
private class BrowserProcessHandler : CefBrowserProcessHandler
|
||||
{
|
||||
}
|
||||
|
||||
// TODO CEF: Research - Is this even needed?
|
||||
private class RenderProcessHandler : CefRenderProcessHandler
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
20
Robust.Client.CEF/RobustCefClient.cs
Normal file
20
Robust.Client.CEF/RobustCefClient.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using Xilium.CefGlue;
|
||||
|
||||
namespace Robust.Client.CEF
|
||||
{
|
||||
// Simple CEF client.
|
||||
internal class RobustCefClient : CefClient
|
||||
{
|
||||
private readonly CefRenderHandler _renderHandler;
|
||||
private readonly CefRequestHandler _requestHandler;
|
||||
|
||||
internal RobustCefClient(CefRenderHandler handler, CefRequestHandler requestHandler)
|
||||
{
|
||||
_renderHandler = handler;
|
||||
_requestHandler = requestHandler;
|
||||
}
|
||||
|
||||
protected override CefRenderHandler GetRenderHandler() => _renderHandler;
|
||||
protected override CefRequestHandler GetRequestHandler() => _requestHandler;
|
||||
}
|
||||
}
|
||||
94
Robust.Client.CEF/RobustRequestHandler.cs
Normal file
94
Robust.Client.CEF/RobustRequestHandler.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.Log;
|
||||
using Xilium.CefGlue;
|
||||
|
||||
namespace Robust.Client.CEF
|
||||
{
|
||||
internal sealed class RobustRequestHandler : CefRequestHandler
|
||||
{
|
||||
private readonly ISawmill _sawmill;
|
||||
private readonly List<Action<RequestHandlerContext>> _handlers = new();
|
||||
|
||||
public RobustRequestHandler(ISawmill sawmill)
|
||||
{
|
||||
_sawmill = sawmill;
|
||||
}
|
||||
|
||||
public void AddHandler(Action<RequestHandlerContext> handler)
|
||||
{
|
||||
lock (_handlers)
|
||||
{
|
||||
_handlers.Add(handler);
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveHandler(Action<RequestHandlerContext> handler)
|
||||
{
|
||||
lock (_handlers)
|
||||
{
|
||||
_handlers.Remove(handler);
|
||||
}
|
||||
}
|
||||
|
||||
protected override CefResourceRequestHandler? GetResourceRequestHandler(
|
||||
CefBrowser browser,
|
||||
CefFrame frame,
|
||||
CefRequest request,
|
||||
bool isNavigation,
|
||||
bool isDownload,
|
||||
string requestInitiator,
|
||||
ref bool disableDefaultHandling)
|
||||
{
|
||||
lock (_handlers)
|
||||
{
|
||||
_sawmill.Debug($"HANDLING REQUEST: {request.Url}");
|
||||
|
||||
var context = new RequestHandlerContext(isNavigation, isDownload, requestInitiator, request);
|
||||
|
||||
foreach (var handler in _handlers)
|
||||
{
|
||||
handler(context);
|
||||
|
||||
if (context.IsHandled)
|
||||
disableDefaultHandling = true;
|
||||
|
||||
if (context.IsCancelled)
|
||||
return null;
|
||||
|
||||
if (context.Result != null)
|
||||
return new WrapReaderResourceHandler(context.Result.MakeHandler());
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
private sealed class WrapReaderResourceHandler : CefResourceRequestHandler
|
||||
{
|
||||
private readonly CefResourceHandler _handler;
|
||||
|
||||
public WrapReaderResourceHandler(CefResourceHandler handler)
|
||||
{
|
||||
_handler = handler;
|
||||
}
|
||||
|
||||
protected override CefCookieAccessFilter? GetCookieAccessFilter(
|
||||
CefBrowser browser,
|
||||
CefFrame frame,
|
||||
CefRequest request)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
protected override CefResourceHandler GetResourceHandler(
|
||||
CefBrowser browser,
|
||||
CefFrame frame,
|
||||
CefRequest request)
|
||||
{
|
||||
return _handler;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Linq;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using XamlX;
|
||||
using XamlX.Ast;
|
||||
using XamlX.Emit;
|
||||
using XamlX.IL;
|
||||
@@ -11,11 +13,14 @@ namespace Robust.Build.Tasks
|
||||
/// Emitters & Transformers based on:
|
||||
/// - https://github.com/AvaloniaUI/Avalonia/blob/c85fa2b9977d251a31886c2534613b4730fbaeaf/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlRootObjectScopeTransformer.cs
|
||||
/// - https://github.com/AvaloniaUI/Avalonia/blob/c85fa2b9977d251a31886c2534613b4730fbaeaf/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AddNameScopeRegistration.cs
|
||||
/// - https://github.com/AvaloniaUI/Avalonia/blob/afb8ae6f3c517dae912729511483995b16cb31af/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/IgnoredDirectivesTransformer.cs
|
||||
/// </summary>
|
||||
public class RobustXamlILCompiler : XamlILCompiler
|
||||
{
|
||||
public RobustXamlILCompiler(TransformerConfiguration configuration, XamlLanguageEmitMappings<IXamlILEmitter, XamlILNodeEmitResult> emitMappings, bool fillWithDefaults) : base(configuration, emitMappings, fillWithDefaults)
|
||||
{
|
||||
Transformers.Insert(0, new IgnoredDirectivesTransformer());
|
||||
|
||||
Transformers.Add(new AddNameScopeRegistration());
|
||||
Transformers.Add(new RobustMarkRootObjectScopeNode());
|
||||
|
||||
@@ -197,5 +202,24 @@ namespace Robust.Build.Tasks
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class IgnoredDirectivesTransformer : IXamlAstTransformer
|
||||
{
|
||||
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
|
||||
{
|
||||
if (node is XamlAstObjectNode astNode)
|
||||
{
|
||||
astNode.Children.RemoveAll(n =>
|
||||
n is XamlAstXmlDirective dir &&
|
||||
dir.Namespace == XamlNamespaces.Xaml2006 &&
|
||||
(dir.Name == "Class" ||
|
||||
dir.Name == "Precompile" ||
|
||||
dir.Name == "FieldModifier" ||
|
||||
dir.Name == "ClassModifier"));
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,13 +460,18 @@ namespace Robust.Client.Console.Commands
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var root = IoCManager.Resolve<IUserInterfaceManager>().RootControl;
|
||||
var uiMgr = IoCManager.Resolve<IUserInterfaceManager>();
|
||||
var res = IoCManager.Resolve<IResourceManager>();
|
||||
|
||||
using (var stream = res.UserData.Create(new ResourcePath("/guidump.txt")))
|
||||
using (var writer = new StreamWriter(stream, EncodingHelpers.UTF8))
|
||||
{
|
||||
_writeNode(root, 0, writer);
|
||||
foreach (var root in uiMgr.AllRoots)
|
||||
{
|
||||
writer.WriteLine($"ROOT: {root}");
|
||||
_writeNode(root, 0, writer);
|
||||
writer.WriteLine("---------------");
|
||||
}
|
||||
}
|
||||
|
||||
shell.WriteLine("Saved guidump");
|
||||
@@ -476,7 +481,7 @@ namespace Robust.Client.Console.Commands
|
||||
{
|
||||
var indentation = new string(' ', indents * 2);
|
||||
writer.WriteLine("{0}{1}", indentation, control);
|
||||
foreach (var (key, value) in _propertyValuesFor(control))
|
||||
foreach (var (key, value) in PropertyValuesFor(control))
|
||||
{
|
||||
writer.WriteLine("{2} * {0}: {1}", key, value, indentation);
|
||||
}
|
||||
@@ -487,7 +492,7 @@ namespace Robust.Client.Console.Commands
|
||||
}
|
||||
}
|
||||
|
||||
private static List<(string, string)> _propertyValuesFor(Control control)
|
||||
internal static List<(string, string)> PropertyValuesFor(Control control)
|
||||
{
|
||||
var members = new List<(string, string)>();
|
||||
var type = control.GetType();
|
||||
|
||||
@@ -255,6 +255,15 @@ namespace Robust.Client
|
||||
_configurationManager.OverrideConVars(_commandLineArgs.CVars);
|
||||
}
|
||||
|
||||
{
|
||||
// Handle GameControllerOptions implicit CVar overrides.
|
||||
_configurationManager.OverrideConVars(new []
|
||||
{
|
||||
(CVars.DisplayWindowIconSet.Name, options.WindowIconSet.ToString()),
|
||||
(CVars.DisplaySplashLogo.Name, options.SplashLogo.ToString())
|
||||
});
|
||||
}
|
||||
|
||||
ProfileOptSetup.Setup(_configurationManager);
|
||||
|
||||
_resourceCache.Initialize(Options.LoadConfigAndUserData ? userDataDir : null);
|
||||
@@ -472,6 +481,8 @@ namespace Robust.Client
|
||||
|
||||
internal void Cleanup()
|
||||
{
|
||||
_modLoader.Shutdown();
|
||||
|
||||
_networkManager.Shutdown("Client shutting down");
|
||||
_midiManager.Shutdown();
|
||||
IoCManager.Resolve<IEntityLookup>().Shutdown();
|
||||
|
||||
@@ -52,6 +52,16 @@ namespace Robust.Client
|
||||
/// </summary>
|
||||
public ResourcePath PrototypeDirectory { get; init; } = new(@"/Prototypes/");
|
||||
|
||||
/// <summary>
|
||||
/// Directory resource path containing window icons to load.
|
||||
/// </summary>
|
||||
public ResourcePath WindowIconSet { get; init; } = new("/Textures/Logo/icon");
|
||||
|
||||
/// <summary>
|
||||
/// Resource path for splash image to show when the game starts up.
|
||||
/// </summary>
|
||||
public ResourcePath SplashLogo { get; init; } = new("/Textures/Logo/logo.png");
|
||||
|
||||
/// <summary>
|
||||
/// Whether to disable mounting the "Resources/" folder on FULL_RELEASE.
|
||||
/// </summary>
|
||||
|
||||
@@ -9,6 +9,7 @@ using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
using OpenToolkit.Graphics.OpenGL4;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Enums;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde
|
||||
@@ -392,7 +393,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
private void DrawSplash(IRenderHandle handle)
|
||||
{
|
||||
var texture = _resourceCache.GetResource<TextureResource>("/Textures/Logo/logo.png").Texture;
|
||||
var splashTex = _cfg.GetCVar(CVars.DisplaySplashLogo);
|
||||
var texture = _resourceCache.GetResource<TextureResource>(splashTex).Texture;
|
||||
|
||||
handle.DrawingHandleScreen.DrawTexture(texture, (ScreenSize - texture.Size) / 2);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using OpenToolkit.Graphics.OpenGL4;
|
||||
using Robust.Client.Utility;
|
||||
@@ -153,6 +155,10 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
isActuallySrgb = loadParams.Srgb;
|
||||
}
|
||||
else if (pixelType == typeof(Bgra32))
|
||||
{
|
||||
isActuallySrgb = loadParams.Srgb;
|
||||
}
|
||||
else if (pixelType == typeof(A8))
|
||||
{
|
||||
DebugTools.Assert(_hasGLTextureSwizzle);
|
||||
@@ -258,6 +264,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
// Note that if _hasGLSrgb is off, we import an sRGB texture as non-sRGB.
|
||||
// Shaders are expected to compensate for this
|
||||
Rgba32 => (srgb && _hasGLSrgb ? PIF.Srgb8Alpha8 : PIF.Rgba8, PF.Rgba, PT.UnsignedByte),
|
||||
Bgra32 => (srgb && _hasGLSrgb ? PIF.Srgb8Alpha8 : PIF.Rgba8, PF.Bgra, PT.UnsignedByte),
|
||||
A8 or L8 => (PIF.R8, PF.Red, PT.UnsignedByte),
|
||||
_ => throw new NotSupportedException("Unsupported pixel type."),
|
||||
};
|
||||
@@ -315,27 +322,80 @@ namespace Robust.Client.Graphics.Clyde
|
||||
private unsafe void SetSubImage<T>(
|
||||
ClydeTexture texture,
|
||||
Vector2i dstTl,
|
||||
Image<T> srcImage,
|
||||
Image<T> img,
|
||||
in UIBox2i srcBox)
|
||||
where T : unmanaged, IPixel<T>
|
||||
{
|
||||
if (!_hasGLTextureSwizzle)
|
||||
if (srcBox.Left < 0 ||
|
||||
srcBox.Top < 0 ||
|
||||
srcBox.Right > srcBox.Width ||
|
||||
srcBox.Bottom > srcBox.Height)
|
||||
{
|
||||
if (typeof(T) == typeof(A8))
|
||||
{
|
||||
SetSubImage(texture, dstTl, ApplyA8Swizzle((Image<A8>) (object) srcImage), srcBox);
|
||||
return;
|
||||
}
|
||||
throw new ArgumentOutOfRangeException(nameof(srcBox), "Source rectangle out of bounds.");
|
||||
}
|
||||
|
||||
if (typeof(T) == typeof(L8))
|
||||
{
|
||||
SetSubImage(texture, dstTl, ApplyL8Swizzle((Image<L8>) (object) srcImage), srcBox);
|
||||
return;
|
||||
}
|
||||
var size = srcBox.Width * srcBox.Height;
|
||||
|
||||
T[]? pooled = null;
|
||||
// C# won't let me use an if due to the stackalloc.
|
||||
var copyBuffer = size < 16 * 16
|
||||
? stackalloc T[size]
|
||||
: (pooled = ArrayPool<T>.Shared.Rent(size)).AsSpan(0, size);
|
||||
|
||||
var srcSpan = img.GetPixelSpan();
|
||||
var w = img.Width;
|
||||
FlipCopySubRegion(srcBox, w, srcSpan, copyBuffer);
|
||||
|
||||
SetSubImageImpl<T>(texture, dstTl, (srcBox.Width, srcBox.Height), copyBuffer);
|
||||
|
||||
if (pooled != null)
|
||||
ArrayPool<T>.Shared.Return(pooled);
|
||||
}
|
||||
|
||||
private unsafe void SetSubImage<T>(
|
||||
ClydeTexture texture,
|
||||
Vector2i dstTl,
|
||||
Vector2i size,
|
||||
ReadOnlySpan<T> buf)
|
||||
where T : unmanaged, IPixel<T>
|
||||
{
|
||||
T[]? pooled = null;
|
||||
// C# won't let me use an if due to the stackalloc.
|
||||
var copyBuffer = buf.Length < 16 * 16
|
||||
? stackalloc T[buf.Length]
|
||||
: (pooled = ArrayPool<T>.Shared.Rent(buf.Length)).AsSpan(0, buf.Length);
|
||||
|
||||
FlipCopy(buf, copyBuffer, size.X, size.Y);
|
||||
|
||||
SetSubImageImpl<T>(texture, dstTl, size, copyBuffer);
|
||||
|
||||
if (pooled != null)
|
||||
ArrayPool<T>.Shared.Return(pooled);
|
||||
}
|
||||
|
||||
private unsafe void SetSubImageImpl<T>(
|
||||
ClydeTexture texture,
|
||||
Vector2i dstTl,
|
||||
Vector2i size,
|
||||
ReadOnlySpan<T> buf)
|
||||
where T : unmanaged, IPixel<T>
|
||||
{
|
||||
if (!_hasGLTextureSwizzle && typeof(T) == typeof(A8) && typeof(T) == typeof(L8))
|
||||
{
|
||||
var swizzleBuf = ArrayPool<Rgba32>.Shared.Rent(buf.Length);
|
||||
|
||||
var destSpan = swizzleBuf.AsSpan(0, buf.Length);
|
||||
if (typeof(T) == typeof(A8))
|
||||
ApplyA8Swizzle(MemoryMarshal.Cast<T, A8>(buf), destSpan);
|
||||
else if (typeof(T) == typeof(L8))
|
||||
ApplyL8Swizzle(MemoryMarshal.Cast<T, L8>(buf), destSpan);
|
||||
|
||||
SetSubImageImpl<Rgba32>(texture, dstTl, size, destSpan);
|
||||
ArrayPool<Rgba32>.Shared.Return(swizzleBuf);
|
||||
return;
|
||||
}
|
||||
|
||||
var loaded = _loadedTextures[texture.TextureId];
|
||||
|
||||
var pixType = GetTexturePixelType<T>();
|
||||
|
||||
if (pixType != loaded.TexturePixelType)
|
||||
@@ -346,11 +406,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
throw new InvalidOperationException("Mismatching pixel type for texture.");
|
||||
}
|
||||
|
||||
if (loaded.Width < dstTl.X + srcBox.Width || loaded.Height < dstTl.Y + srcBox.Height)
|
||||
throw new ArgumentOutOfRangeException(nameof(srcBox), "Destination rectangle out of bounds.");
|
||||
|
||||
if (srcBox.Left < 0 || srcBox.Top < 0 || srcBox.Right > srcImage.Width || srcBox.Bottom > srcImage.Height)
|
||||
throw new ArgumentOutOfRangeException(nameof(srcBox), "Source rectangle out of bounds.");
|
||||
if (loaded.Width < dstTl.X + size.X || loaded.Height < dstTl.Y + size.Y)
|
||||
throw new ArgumentOutOfRangeException(nameof(size), "Destination rectangle out of bounds.");
|
||||
|
||||
if (sizeof(T) != 4)
|
||||
{
|
||||
@@ -364,24 +421,14 @@ namespace Robust.Client.Graphics.Clyde
|
||||
GL.BindTexture(TextureTarget.Texture2D, loaded.OpenGLObject.Handle);
|
||||
CheckGlError();
|
||||
|
||||
var size = srcBox.Width * srcBox.Height;
|
||||
|
||||
var copyBuffer = size < 16 * 16 ? stackalloc T[size] : new T[size];
|
||||
|
||||
for (var y = 0; y < srcBox.Height; y++)
|
||||
for (var x = 0; x < srcBox.Width; x++)
|
||||
fixed (T* aPtr = buf)
|
||||
{
|
||||
copyBuffer[(srcBox.Height - y - 1) * srcBox.Width + x] = srcImage[x + srcBox.Left, srcBox.Top + y];
|
||||
}
|
||||
|
||||
fixed (T* aPtr = copyBuffer)
|
||||
{
|
||||
var dstY = loaded.Height - dstTl.Y - srcBox.Height;
|
||||
var dstY = loaded.Height - dstTl.Y - size.Y;
|
||||
GL.TexSubImage2D(
|
||||
TextureTarget.Texture2D,
|
||||
0,
|
||||
dstTl.X, dstY,
|
||||
srcBox.Width, srcBox.Height,
|
||||
size.X, size.Y,
|
||||
pf, pt,
|
||||
(IntPtr) aPtr);
|
||||
CheckGlError();
|
||||
@@ -398,7 +445,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
return default(T) switch
|
||||
{
|
||||
Rgba32 => TexturePixelType.Rgba32,
|
||||
Rgba32 or Bgra32 => TexturePixelType.Rgba32,
|
||||
L8 => TexturePixelType.L8,
|
||||
A8 => TexturePixelType.A8,
|
||||
_ => throw new NotSupportedException("Unsupported pixel type."),
|
||||
@@ -452,17 +499,35 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
}
|
||||
|
||||
private static void FlipCopySubRegion<T>(
|
||||
UIBox2i srcBox,
|
||||
int w,
|
||||
ReadOnlySpan<T> srcSpan,
|
||||
Span<T> copyBuffer)
|
||||
where T : unmanaged, IPixel<T>
|
||||
{
|
||||
var subH = srcBox.Height;
|
||||
var subW = srcBox.Width;
|
||||
|
||||
var dr = subH - 1;
|
||||
for (var r = 0; r < subH; r++, dr--)
|
||||
{
|
||||
var si = r * w + srcBox.Left;
|
||||
var di = dr * subW;
|
||||
var srcRow = srcSpan[si..(si + subW)];
|
||||
var dstRow = copyBuffer[di..(di + subW)];
|
||||
|
||||
srcRow.CopyTo(dstRow);
|
||||
}
|
||||
}
|
||||
|
||||
private static Image<Rgba32> ApplyA8Swizzle(Image<A8> source)
|
||||
{
|
||||
var newImage = new Image<Rgba32>(source.Width, source.Height);
|
||||
var sourceSpan = source.GetPixelSpan();
|
||||
var destSpan = newImage.GetPixelSpan();
|
||||
|
||||
for (var i = 0; i < sourceSpan.Length; i++)
|
||||
{
|
||||
var px = sourceSpan[i].PackedValue;
|
||||
destSpan[i] = new Rgba32(255, 255, 255, px);
|
||||
}
|
||||
ApplyA8Swizzle(sourceSpan, destSpan);
|
||||
|
||||
return newImage;
|
||||
}
|
||||
@@ -473,15 +538,28 @@ namespace Robust.Client.Graphics.Clyde
|
||||
var sourceSpan = source.GetPixelSpan();
|
||||
var destSpan = newImage.GetPixelSpan();
|
||||
|
||||
for (var i = 0; i < sourceSpan.Length; i++)
|
||||
{
|
||||
var px = sourceSpan[i].PackedValue;
|
||||
destSpan[i] = new Rgba32(px, px, px, 255);
|
||||
}
|
||||
ApplyL8Swizzle(sourceSpan, destSpan);
|
||||
|
||||
return newImage;
|
||||
}
|
||||
|
||||
private static void ApplyL8Swizzle(ReadOnlySpan<L8> src, Span<Rgba32> dst)
|
||||
{
|
||||
for (var i = 0; i < src.Length; i++)
|
||||
{
|
||||
var px = src[i].PackedValue;
|
||||
dst[i] = new Rgba32(px, px, px, 255);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyA8Swizzle(ReadOnlySpan<A8> src, Span<Rgba32> dst)
|
||||
{
|
||||
for (var i = 0; i < src.Length; i++)
|
||||
{
|
||||
var px = src[i].PackedValue;
|
||||
dst[i] = new Rgba32(255, 255, 255, px);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class LoadedTexture
|
||||
{
|
||||
@@ -525,6 +603,11 @@ namespace Robust.Client.Graphics.Clyde
|
||||
_clyde.SetSubImage(this, topLeft, sourceImage, sourceRegion);
|
||||
}
|
||||
|
||||
public override void SetSubImage<T>(Vector2i topLeft, Vector2i size, ReadOnlySpan<T> buffer)
|
||||
{
|
||||
_clyde.SetSubImage(this, topLeft, size, buffer);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
@@ -26,6 +25,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
private IWindowingImpl? _windowing;
|
||||
private Renderer _chosenRenderer;
|
||||
|
||||
private ResourcePath? _windowIconPath;
|
||||
private Thread? _windowingThread;
|
||||
private bool _vSync;
|
||||
private WindowMode _windowMode;
|
||||
@@ -87,6 +87,10 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
private bool InitWindowing()
|
||||
{
|
||||
var iconPath = _cfg.GetCVar(CVars.DisplayWindowIconSet);
|
||||
if (!string.IsNullOrWhiteSpace(iconPath))
|
||||
_windowIconPath = new ResourcePath(iconPath);
|
||||
|
||||
_windowingThread = Thread.CurrentThread;
|
||||
|
||||
_windowing = new GlfwWindowingImpl(this);
|
||||
@@ -164,13 +168,13 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
private IEnumerable<Image<Rgba32>> LoadWindowIcons()
|
||||
{
|
||||
if (OperatingSystem.IsMacOS())
|
||||
if (OperatingSystem.IsMacOS() || _windowIconPath == null)
|
||||
{
|
||||
// Does nothing on macOS so don't bother.
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (var file in _resourceCache.ContentFindFiles("/Textures/Logo/icon"))
|
||||
foreach (var file in _resourceCache.ContentFindFiles(_windowIconPath))
|
||||
{
|
||||
if (file.Extension != "png")
|
||||
{
|
||||
@@ -399,7 +403,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
public Action<WindowClosedEventArgs>? Closed;
|
||||
}
|
||||
|
||||
private sealed class WindowHandle : IClydeWindow
|
||||
private sealed class WindowHandle : IClydeWindowInternal
|
||||
{
|
||||
// So funny story
|
||||
// When this class was a record, the C# compiler on .NET 5 stack overflowed
|
||||
@@ -466,6 +470,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
add => _reg.Closed += value;
|
||||
remove => _reg.Closed -= value;
|
||||
}
|
||||
|
||||
public nint? WindowsHWnd => _clyde._windowing!.WindowGetWin32Window(_reg);
|
||||
}
|
||||
|
||||
private sealed class MonitorHandle : IClydeMonitor
|
||||
|
||||
@@ -396,6 +396,11 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
// Just do nothing on mutate.
|
||||
}
|
||||
|
||||
public override void SetSubImage<T>(Vector2i topLeft, Vector2i size, ReadOnlySpan<T> buffer)
|
||||
{
|
||||
// Just do nothing on mutate.
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class DummyShaderInstance : ShaderInstance
|
||||
|
||||
@@ -131,10 +131,10 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
private void ProcessEventKey(EventKey ev)
|
||||
{
|
||||
EmitKeyEvent(ConvertGlfwKey(ev.Key), ev.Action, ev.Mods);
|
||||
EmitKeyEvent(ConvertGlfwKey(ev.Key), ev.Action, ev.Mods, ev.ScanCode);
|
||||
}
|
||||
|
||||
private void EmitKeyEvent(Keyboard.Key key, InputAction action, KeyModifiers mods)
|
||||
private void EmitKeyEvent(Keyboard.Key key, InputAction action, KeyModifiers mods, int scanCode)
|
||||
{
|
||||
var shift = (mods & KeyModifiers.Shift) != 0;
|
||||
var alt = (mods & KeyModifiers.Alt) != 0;
|
||||
@@ -144,7 +144,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
var ev = new KeyEventArgs(
|
||||
key,
|
||||
action == InputAction.Repeat,
|
||||
alt, control, shift, system);
|
||||
alt, control, shift, system,
|
||||
scanCode);
|
||||
|
||||
switch (action)
|
||||
{
|
||||
@@ -162,7 +163,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
private void ProcessEventMouseButton(EventMouseButton ev)
|
||||
{
|
||||
EmitKeyEvent(Mouse.MouseButtonToKey(ConvertGlfwButton(ev.Button)), ev.Action, ev.Mods);
|
||||
EmitKeyEvent(Mouse.MouseButtonToKey(ConvertGlfwButton(ev.Button)), ev.Action, ev.Mods, default);
|
||||
}
|
||||
|
||||
private void ProcessEventScroll(EventScroll ev)
|
||||
|
||||
@@ -403,6 +403,22 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
}
|
||||
|
||||
public nint? WindowGetWin32Window(WindowReg window)
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
return null;
|
||||
|
||||
var reg = (GlfwWindowReg) window;
|
||||
try
|
||||
{
|
||||
return GLFW.GetWin32Window(reg.GlfwWindow);
|
||||
}
|
||||
catch (EntryPointNotFoundException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void WindowDestroy(WindowReg window)
|
||||
{
|
||||
var reg = (GlfwWindowReg) window;
|
||||
|
||||
@@ -38,6 +38,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
void WindowRequestAttention(WindowReg window);
|
||||
void WindowSwapBuffers(WindowReg window);
|
||||
uint? WindowGetX11Id(WindowReg window);
|
||||
nint? WindowGetWin32Window(WindowReg window);
|
||||
WindowHandle WindowCreate(WindowCreateParameters parameters);
|
||||
void WindowDestroy(WindowReg reg);
|
||||
|
||||
|
||||
@@ -30,4 +30,9 @@ namespace Robust.Client.Graphics
|
||||
/// </summary>
|
||||
event Action<WindowClosedEventArgs> Closed;
|
||||
}
|
||||
|
||||
public interface IClydeWindowInternal : IClydeWindow
|
||||
{
|
||||
nint? WindowsHWnd { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,9 @@ namespace Robust.Client.Graphics
|
||||
SetSubImage(topLeft, sourceImage, UIBox2i.FromDimensions(0, 0, sourceImage.Width, sourceImage.Height));
|
||||
}
|
||||
|
||||
public abstract void SetSubImage<T>(Vector2i topLeft, Vector2i size, ReadOnlySpan<T> buffer)
|
||||
where T : unmanaged, IPixel<T>;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
|
||||
@@ -76,11 +76,18 @@ namespace Robust.Client.Input
|
||||
/// </summary>
|
||||
public bool IsRepeat { get; }
|
||||
|
||||
public KeyEventArgs(Keyboard.Key key, bool repeat, bool alt, bool control, bool shift, bool system)
|
||||
public int ScanCode { get; }
|
||||
|
||||
public KeyEventArgs(
|
||||
Keyboard.Key key,
|
||||
bool repeat,
|
||||
bool alt, bool control, bool shift, bool system,
|
||||
int scanCode)
|
||||
: base(alt, control, shift, system)
|
||||
{
|
||||
Key = key;
|
||||
IsRepeat = repeat;
|
||||
ScanCode = scanCode;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -129,5 +129,7 @@ namespace Robust.Client.Input
|
||||
void ResetAllBindings();
|
||||
|
||||
bool IsKeyFunctionModified(BoundKeyFunction function);
|
||||
|
||||
bool IsKeyDown(Keyboard.Key key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Reflection;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
@@ -41,7 +42,7 @@ namespace Robust.Client.Input
|
||||
|
||||
[Dependency] private readonly IResourceManager _resourceMan = default!;
|
||||
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
|
||||
[Dependency] private readonly IUserInterfaceManagerInternal _userInterfaceManagerInternal = default!;
|
||||
[Dependency] private readonly IUserInterfaceManagerInternal _uiMgr = default!;
|
||||
|
||||
private bool _currentlyFindingViewport;
|
||||
|
||||
@@ -248,7 +249,15 @@ namespace Robust.Client.Input
|
||||
var uiOnly = false;
|
||||
if (hasCanFocus)
|
||||
{
|
||||
uiOnly = _userInterfaceManagerInternal.HandleCanFocusDown(MouseScreenPosition);
|
||||
uiOnly = _uiMgr.HandleCanFocusDown(MouseScreenPosition, out _);
|
||||
}
|
||||
|
||||
if (_uiMgr.KeyboardFocused is IRawInputControl rawInput)
|
||||
{
|
||||
var block = RaiseRawKeyInput(args, rawInput, args.IsRepeat ? RawKeyAction.Repeat : RawKeyAction.Down);
|
||||
|
||||
if (block)
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var binding in bindsDown)
|
||||
@@ -260,6 +269,21 @@ namespace Robust.Client.Input
|
||||
}
|
||||
}
|
||||
|
||||
private bool RaiseRawKeyInput(KeyEventArgs args, IRawInputControl rawInput, RawKeyAction action)
|
||||
{
|
||||
DebugTools.AssertNotNull(_uiMgr.KeyboardFocused);
|
||||
|
||||
var mousePos = _uiMgr.CalcRelativeMousePositionFor(_uiMgr.KeyboardFocused!, _uiMgr.MousePositionScaled);
|
||||
var keyEvent = new GuiRawKeyEvent(
|
||||
args.Key,
|
||||
args.ScanCode,
|
||||
action,
|
||||
(Vector2i) (mousePos ?? Vector2.Zero));
|
||||
|
||||
var block = rawInput.RawKeyEvent(keyEvent);
|
||||
return block;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void KeyUp(KeyEventArgs args)
|
||||
{
|
||||
@@ -289,8 +313,11 @@ namespace Robust.Client.Input
|
||||
|
||||
if (hasCanFocus)
|
||||
{
|
||||
_userInterfaceManagerInternal.HandleCanFocusUp();
|
||||
_uiMgr.HandleCanFocusUp();
|
||||
}
|
||||
|
||||
if (_uiMgr.KeyboardFocused is IRawInputControl rawInput)
|
||||
RaiseRawKeyInput(args, rawInput, RawKeyAction.Up);
|
||||
}
|
||||
|
||||
private bool DownBind(KeyBinding binding, bool uiOnly, bool isRepeat)
|
||||
@@ -607,6 +634,11 @@ namespace Robust.Client.Input
|
||||
return _modifiedKeyFunctions.Contains(function);
|
||||
}
|
||||
|
||||
public bool IsKeyDown(Key key)
|
||||
{
|
||||
return _keysPressed[(int) key];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetKeyBinding(BoundKeyFunction function, [NotNullWhen(true)] out IKeyBinding? binding)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Robust.UnitTesting")]
|
||||
[assembly: InternalsVisibleTo("Robust.Client.CEF")]
|
||||
[assembly: InternalsVisibleTo("Robust.Lite")]
|
||||
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
|
||||
[assembly: InternalsVisibleTo("Robust.Benchmarks")]
|
||||
|
||||
@@ -19,4 +19,5 @@
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=graphics_005Coverlays/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=graphics_005Crsi/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=graphics_005Cshaders/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=resourcemanagement_005Cresourcetypes/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=resourcemanagement_005Cresourcetypes/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=userinterface_005Cdevwindow/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
@@ -102,8 +102,9 @@ namespace Robust.Client.UserInterface
|
||||
bool alt,
|
||||
bool control,
|
||||
bool shift,
|
||||
bool system)
|
||||
: base(key, repeat, alt, control, shift, system)
|
||||
bool system,
|
||||
int scanCode)
|
||||
: base(key, repeat, alt, control, shift, system, scanCode)
|
||||
{
|
||||
SourceControl = sourceControl;
|
||||
}
|
||||
|
||||
@@ -608,7 +608,9 @@ namespace Robust.Client.UserInterface
|
||||
measured.Y = Math.Clamp(measured.Y, MinHeight, MaxHeight);
|
||||
|
||||
measured = _margin.Inflate(measured);
|
||||
return Vector2.ComponentMin(measured, availableSize);
|
||||
measured = Vector2.ComponentMin(measured, availableSize);
|
||||
measured = Vector2.ComponentMax(measured, Vector2.Zero);
|
||||
return measured;
|
||||
}
|
||||
|
||||
protected virtual Vector2 MeasureOverride(Vector2 availableSize)
|
||||
@@ -750,7 +752,7 @@ namespace Robust.Client.UserInterface
|
||||
maxH = MathHelper.Clamp(maxConstraint, minH, maxH);
|
||||
|
||||
minConstraint = float.IsNaN(setH) ? 0 : setH;
|
||||
minH = MathHelper.Clamp(minW, minConstraint, minH);
|
||||
minH = MathHelper.Clamp(maxH, minConstraint, minH);
|
||||
|
||||
return (
|
||||
Math.Clamp(avail.X, minW, maxW),
|
||||
|
||||
@@ -601,12 +601,15 @@ namespace Robust.Client.UserInterface
|
||||
ChildAdded(child);
|
||||
}
|
||||
|
||||
public event Action<Control>? OnChildAdded;
|
||||
|
||||
/// <summary>
|
||||
/// Called after a new child is added to this control.
|
||||
/// </summary>
|
||||
/// <param name="newChild">The new child.</param>
|
||||
protected virtual void ChildAdded(Control newChild)
|
||||
{
|
||||
OnChildAdded?.Invoke(newChild);
|
||||
InvalidateMeasure();
|
||||
}
|
||||
|
||||
@@ -649,12 +652,15 @@ namespace Robust.Client.UserInterface
|
||||
ChildRemoved(child);
|
||||
}
|
||||
|
||||
public event Action<Control>? OnChildRemoved;
|
||||
|
||||
/// <summary>
|
||||
/// Called when a child is removed from this child.
|
||||
/// </summary>
|
||||
/// <param name="child">The former child.</param>
|
||||
protected virtual void ChildRemoved(Control child)
|
||||
{
|
||||
OnChildRemoved?.Invoke(child);
|
||||
InvalidateMeasure();
|
||||
}
|
||||
|
||||
@@ -665,6 +671,8 @@ namespace Robust.Client.UserInterface
|
||||
{
|
||||
}
|
||||
|
||||
public event Action<ControlChildMovedEventArgs>? OnChildMoved;
|
||||
|
||||
/// <summary>
|
||||
/// Called when the order index of a child changes.
|
||||
/// </summary>
|
||||
@@ -673,6 +681,7 @@ namespace Robust.Client.UserInterface
|
||||
/// <param name="newIndex">The new index of the child.</param>
|
||||
protected virtual void ChildMoved(Control child, int oldIndex, int newIndex)
|
||||
{
|
||||
OnChildMoved?.Invoke(new ControlChildMovedEventArgs(child, oldIndex, newIndex));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -824,10 +833,15 @@ namespace Robust.Client.UserInterface
|
||||
UserInterfaceManager.ReleaseKeyboardFocus(this);
|
||||
}
|
||||
|
||||
public event Action? OnResized;
|
||||
|
||||
/// <summary>
|
||||
/// Called when the size of the control changes.
|
||||
/// </summary>
|
||||
protected virtual void Resized() { }
|
||||
protected virtual void Resized()
|
||||
{
|
||||
OnResized?.Invoke();
|
||||
}
|
||||
|
||||
internal void DoFrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
@@ -967,4 +981,18 @@ namespace Robust.Client.UserInterface
|
||||
}
|
||||
|
||||
public delegate Control? TooltipSupplier(Control sender);
|
||||
|
||||
public readonly struct ControlChildMovedEventArgs
|
||||
{
|
||||
public ControlChildMovedEventArgs(Control control, int oldIndex, int newIndex)
|
||||
{
|
||||
Control = control;
|
||||
OldIndex = oldIndex;
|
||||
NewIndex = newIndex;
|
||||
}
|
||||
|
||||
public readonly Control Control;
|
||||
public readonly int OldIndex;
|
||||
public readonly int NewIndex;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Robust.Client.UserInterface.Controls
|
||||
{
|
||||
@@ -10,16 +11,19 @@ namespace Robust.Client.UserInterface.Controls
|
||||
/// Defines how user-initiated moving of the split should work. See documentation
|
||||
/// for each enum value to see how the different options work.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public SplitResizeMode ResizeMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Width of the split in virtual pixels
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float SplitWidth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Virtual pixel offset from the edge beyond which the split cannot be moved.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float SplitEdgeSeparation { get; set; }
|
||||
|
||||
private float _splitCenter;
|
||||
@@ -36,6 +40,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
|
||||
private bool Vertical => Orientation == SplitOrientation.Vertical;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public SplitOrientation Orientation
|
||||
{
|
||||
get => _orientation;
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<Control xmlns="https://spacestation14.io">
|
||||
<DebugConsole />
|
||||
</Control>
|
||||
8
Robust.Client/UserInterface/DevWindow/DevWindow.xaml
Normal file
8
Robust.Client/UserInterface/DevWindow/DevWindow.xaml
Normal file
@@ -0,0 +1,8 @@
|
||||
<Control xmlns="https://spacestation14.io"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="Robust.Client.UserInterface.DevWindow">
|
||||
<TabContainer>
|
||||
<DebugConsole Name="DebugConsole" />
|
||||
<DevWindowTabUI Name="UI" />
|
||||
</TabContainer>
|
||||
</Control>
|
||||
@@ -1,12 +1,31 @@
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Robust.Client.UserInterface
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class DevWindow : Control
|
||||
{
|
||||
public DevWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
TabContainer.SetTabTitle(DebugConsole, "Debug Console");
|
||||
TabContainer.SetTabTitle(UI, "User Interface");
|
||||
}
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
internal sealed class TestWindowCommand : IConsoleCommand
|
||||
{
|
||||
@@ -27,7 +46,7 @@ namespace Robust.Client.UserInterface
|
||||
var window = clyde.CreateWindow(new WindowCreateParameters
|
||||
{
|
||||
Maximized = true,
|
||||
Title = "SS14 Debug Window",
|
||||
Title = "Robust Debug Window",
|
||||
Monitor = monitor,
|
||||
});
|
||||
var root = IoCManager.Resolve<IUserInterfaceManager>().CreateWindowRoot(window);
|
||||
@@ -38,12 +57,4 @@ namespace Robust.Client.UserInterface
|
||||
root.AddChild(control);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class DevWindow : Control
|
||||
{
|
||||
public DevWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
27
Robust.Client/UserInterface/DevWindow/DevWindowTabUI.xaml
Normal file
27
Robust.Client/UserInterface/DevWindow/DevWindowTabUI.xaml
Normal file
@@ -0,0 +1,27 @@
|
||||
<Control xmlns="https://spacestation14.io"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="Robust.Client.UserInterface.DevWindowTabUI">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<SplitContainer VerticalExpand="True" Orientation="Horizontal">
|
||||
<BoxContainer Orientation="Vertical" MinWidth="250">
|
||||
<!--
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Button Name="RefreshButton" Text="{Loc 'dev-window-ui-refresh'}" />
|
||||
</BoxContainer>
|
||||
-->
|
||||
<ScrollContainer VerticalExpand="True">
|
||||
<BoxContainer Name="ControlTreeRoot" Orientation="Vertical" MouseFilter="Stop" />
|
||||
</ScrollContainer>
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Button Name="RefreshPropertiesButton" Text="Refresh" />
|
||||
</BoxContainer>
|
||||
|
||||
<ScrollContainer VerticalExpand="True" HScrollEnabled="False">
|
||||
<GridContainer Name="ControlProperties" Columns="2" />
|
||||
</ScrollContainer>
|
||||
</BoxContainer>
|
||||
</SplitContainer>
|
||||
</BoxContainer>
|
||||
</Control>
|
||||
106
Robust.Client/UserInterface/DevWindow/DevWindowTabUI.xaml.cs
Normal file
106
Robust.Client/UserInterface/DevWindow/DevWindowTabUI.xaml.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
using System;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Console.Commands;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.UserInterface
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class DevWindowTabUI : Control
|
||||
{
|
||||
public Control? SelectedControl { get; private set; }
|
||||
|
||||
public event Action? SelectedControlChanged;
|
||||
|
||||
public DevWindowTabUI()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
ControlTreeRoot.OnKeyBindDown += ControlTreeRootOnOnKeyBindDown;
|
||||
RefreshPropertiesButton.OnPressed += _ => Refresh();
|
||||
}
|
||||
|
||||
private void ControlTreeRootOnOnKeyBindDown(GUIBoundKeyEventArgs obj)
|
||||
{
|
||||
if (obj.Function != EngineKeyFunctions.UIClick)
|
||||
return;
|
||||
|
||||
obj.Handle();
|
||||
SelectControl(null);
|
||||
}
|
||||
|
||||
protected override void EnteredTree()
|
||||
{
|
||||
base.EnteredTree();
|
||||
|
||||
// Load tree roots.
|
||||
foreach (var root in UserInterfaceManager.AllRoots)
|
||||
{
|
||||
var entry = new DevWindowUITreeEntry(this, root);
|
||||
|
||||
ControlTreeRoot.AddChild(entry);
|
||||
}
|
||||
|
||||
UserInterfaceManager.OnPostDrawUIRoot += OnPostDrawUIRoot;
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
base.ExitedTree();
|
||||
|
||||
// Clear tree children.
|
||||
ControlTreeRoot.RemoveAllChildren();
|
||||
UserInterfaceManager.OnPostDrawUIRoot -= OnPostDrawUIRoot;
|
||||
}
|
||||
|
||||
private void OnPostDrawUIRoot(PostDrawUIRootEventArgs eventArgs)
|
||||
{
|
||||
if (SelectedControl == null || eventArgs.Root != SelectedControl.Root)
|
||||
return;
|
||||
|
||||
var rect = UIBox2i.FromDimensions(SelectedControl.GlobalPixelPosition, SelectedControl.PixelSize);
|
||||
eventArgs.DrawingHandle.DrawRect(rect, Color.Cyan.WithAlpha(0.35f));
|
||||
}
|
||||
|
||||
public void EntryRemoved(DevWindowUITreeEntry entry)
|
||||
{
|
||||
if (SelectedControl == entry.VisControl)
|
||||
SelectControl(null);
|
||||
}
|
||||
|
||||
public void SelectControl(Control? control)
|
||||
{
|
||||
SelectedControl = control;
|
||||
|
||||
SelectedControlChanged?.Invoke();
|
||||
|
||||
Refresh();
|
||||
}
|
||||
|
||||
|
||||
private void Refresh()
|
||||
{
|
||||
ControlProperties.RemoveAllChildren();
|
||||
|
||||
if (SelectedControl == null)
|
||||
return;
|
||||
|
||||
var props = GuiDumpCommand.PropertyValuesFor(SelectedControl);
|
||||
foreach (var (prop, value) in props)
|
||||
{
|
||||
ControlProperties.AddChild(new Label { Text = prop });
|
||||
ControlProperties.AddChild(new Label { Text = value });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<Control xmlns="https://spacestation14.io"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="Robust.Client.UserInterface.DevWindowUITreeEntry">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<!-- Main button for this entry -->
|
||||
<PanelContainer Name="Header" MouseFilter="Stop">
|
||||
<BoxContainer Orientation="Horizontal" >
|
||||
<Button Name="ExpandButton" ToggleMode="True" Text=">" />
|
||||
<Label Name="ControlName" HorizontalExpand="True" ClipText="True" />
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
<BoxContainer Orientation="Vertical" Name="ChildEntryContainer" Margin="8 0 0 0" />
|
||||
</BoxContainer>
|
||||
</Control>
|
||||
@@ -0,0 +1,120 @@
|
||||
using System.Linq;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.UserInterface
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class DevWindowUITreeEntry : Control
|
||||
{
|
||||
private readonly DevWindowTabUI _tab;
|
||||
public readonly Control VisControl;
|
||||
|
||||
public DevWindowUITreeEntry(DevWindowTabUI tab, Control visControl)
|
||||
{
|
||||
_tab = tab;
|
||||
VisControl = visControl;
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
var typeName = visControl.GetType().Name;
|
||||
ControlName.Text = visControl.Name == null ? typeName : $"{visControl.Name} ({typeName})";
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
ExpandButton.OnToggled += ExpandButtonOnOnToggled;
|
||||
Header.OnKeyBindDown += HeaderOnOnKeyBindDown;
|
||||
}
|
||||
|
||||
private void HeaderOnOnKeyBindDown(GUIBoundKeyEventArgs obj)
|
||||
{
|
||||
if (obj.Function != EngineKeyFunctions.UIClick)
|
||||
return;
|
||||
|
||||
obj.Handle();
|
||||
_tab.SelectControl(VisControl);
|
||||
}
|
||||
|
||||
protected override void EnteredTree()
|
||||
{
|
||||
base.EnteredTree();
|
||||
|
||||
VisControl.OnChildAdded += VisControlOnChildAdded;
|
||||
VisControl.OnChildRemoved += VisControlOnChildRemoved;
|
||||
VisControl.OnChildMoved += VisControlOnChildMoved;
|
||||
_tab.SelectedControlChanged += TabOnSelectedControlChanged;
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
base.ExitedTree();
|
||||
|
||||
VisControl.OnChildAdded -= VisControlOnChildAdded;
|
||||
VisControl.OnChildRemoved -= VisControlOnChildRemoved;
|
||||
VisControl.OnChildMoved -= VisControlOnChildMoved;
|
||||
_tab.SelectedControlChanged -= TabOnSelectedControlChanged;
|
||||
|
||||
_tab.EntryRemoved(this);
|
||||
}
|
||||
|
||||
private void TabOnSelectedControlChanged()
|
||||
{
|
||||
var isThis = VisControl == _tab.SelectedControl;
|
||||
|
||||
Header.PanelOverride = isThis ? new StyleBoxFlat { BackgroundColor = Color.Blue } : null;
|
||||
}
|
||||
|
||||
private void VisControlOnChildAdded(Control child)
|
||||
{
|
||||
if (!ExpandButton.Pressed)
|
||||
return;
|
||||
|
||||
ChildEntryContainer.AddChild(new DevWindowUITreeEntry(_tab, child));
|
||||
}
|
||||
|
||||
private void VisControlOnChildRemoved(Control child)
|
||||
{
|
||||
if (!ExpandButton.Pressed)
|
||||
return;
|
||||
|
||||
var entry = ChildEntryContainer.Children.OfType<DevWindowUITreeEntry>().Single(c => c.VisControl == child);
|
||||
ChildEntryContainer.RemoveChild(entry);
|
||||
}
|
||||
|
||||
private void VisControlOnChildMoved(ControlChildMovedEventArgs eventArgs)
|
||||
{
|
||||
if (!ExpandButton.Pressed)
|
||||
return;
|
||||
|
||||
var entry = ChildEntryContainer.Children
|
||||
.OfType<DevWindowUITreeEntry>()
|
||||
.Single(c => c.VisControl == eventArgs.Control);
|
||||
entry.SetPositionInParent(eventArgs.NewIndex);
|
||||
}
|
||||
|
||||
private void ExpandButtonOnOnToggled(BaseButton.ButtonToggledEventArgs obj)
|
||||
{
|
||||
if (obj.Pressed)
|
||||
{
|
||||
DebugTools.Assert(ChildEntryContainer.ChildCount == 0);
|
||||
|
||||
foreach (var child in VisControl.Children)
|
||||
{
|
||||
ChildEntryContainer.AddChild(new DevWindowUITreeEntry(_tab, child));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ChildEntryContainer.RemoveAllChildren();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
45
Robust.Client/UserInterface/IRawInputControl.cs
Normal file
45
Robust.Client/UserInterface/IRawInputControl.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using System.Text;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.UserInterface
|
||||
{
|
||||
internal interface IRawInputControl
|
||||
{
|
||||
bool RawKeyEvent(in GuiRawKeyEvent guiRawEvent) => false;
|
||||
// bool RawCharEvent(in GuiRawCharEvent guiRawCharEvent) => false;
|
||||
}
|
||||
|
||||
/*
|
||||
internal struct GuiRawCharEvent
|
||||
{
|
||||
// public readonly
|
||||
public readonly RawKeyAction Action;
|
||||
public readonly Vector2i MouseRelative;
|
||||
public readonly Rune Char;
|
||||
}
|
||||
*/
|
||||
|
||||
internal readonly struct GuiRawKeyEvent
|
||||
{
|
||||
public readonly Keyboard.Key Key;
|
||||
public readonly int ScanCode;
|
||||
public readonly RawKeyAction Action;
|
||||
public readonly Vector2i MouseRelative;
|
||||
|
||||
public GuiRawKeyEvent(Keyboard.Key key, int scanCode, RawKeyAction action, Vector2i mouseRelative)
|
||||
{
|
||||
Key = key;
|
||||
ScanCode = scanCode;
|
||||
Action = action;
|
||||
MouseRelative = mouseRelative;
|
||||
}
|
||||
}
|
||||
|
||||
public enum RawKeyAction : byte
|
||||
{
|
||||
Down,
|
||||
Repeat,
|
||||
Up
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.UserInterface
|
||||
{
|
||||
@@ -103,5 +103,21 @@ namespace Robust.Client.UserInterface
|
||||
void PushModal(Control modal);
|
||||
WindowRoot CreateWindowRoot(IClydeWindow window);
|
||||
void DestroyWindowRoot(IClydeWindow window);
|
||||
|
||||
IEnumerable<UIRoot> AllRoots { get; }
|
||||
|
||||
event Action<PostDrawUIRootEventArgs> OnPostDrawUIRoot;
|
||||
}
|
||||
|
||||
public readonly struct PostDrawUIRootEventArgs
|
||||
{
|
||||
public readonly UIRoot Root;
|
||||
public readonly DrawingHandleScreen DrawingHandle;
|
||||
|
||||
public PostDrawUIRootEventArgs(UIRoot root, DrawingHandleScreen drawingHandle)
|
||||
{
|
||||
Root = root;
|
||||
DrawingHandle = drawingHandle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Robust.Client.Graphics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Map;
|
||||
@@ -15,7 +16,9 @@ namespace Robust.Client.UserInterface
|
||||
void FrameUpdate(FrameEventArgs args);
|
||||
|
||||
/// <returns>True if a UI control was hit and the key event should not pass through past UI.</returns>
|
||||
bool HandleCanFocusDown(ScreenCoordinates pointerPosition);
|
||||
bool HandleCanFocusDown(
|
||||
ScreenCoordinates pointerPosition,
|
||||
[NotNullWhen(true)] out (Control control, Vector2i rel)? hitData);
|
||||
|
||||
void HandleCanFocusUp();
|
||||
|
||||
@@ -52,6 +55,8 @@ namespace Robust.Client.UserInterface
|
||||
/// was not supplied by tooltip supplier or tooltip is not showing for the control).
|
||||
/// </summary>
|
||||
Control? GetSuppliedTooltipFor(Control control);
|
||||
|
||||
Vector2? CalcRelativeMousePositionFor(Control control, ScreenCoordinates mousePos);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.Console;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.Player;
|
||||
@@ -9,7 +9,6 @@ using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.IoC;
|
||||
@@ -127,6 +126,7 @@ namespace Robust.Client.UserInterface
|
||||
private void _initializeCommon()
|
||||
{
|
||||
RootControl = CreateWindowRoot(_clyde.MainWindow);
|
||||
RootControl.Name = "MainWindowRoot";
|
||||
|
||||
RootControl.InvalidateMeasure();
|
||||
QueueMeasureUpdate(RootControl);
|
||||
@@ -217,6 +217,9 @@ namespace Robust.Client.UserInterface
|
||||
root.RemoveAllChildren();
|
||||
}
|
||||
|
||||
public IEnumerable<UIRoot> AllRoots => _roots;
|
||||
public event Action<PostDrawUIRootEventArgs>? OnPostDrawUIRoot;
|
||||
|
||||
private void WindowDestroyed(WindowDestroyedEventArgs args)
|
||||
{
|
||||
DestroyWindowRoot(args.Window);
|
||||
@@ -324,9 +327,11 @@ namespace Robust.Client.UserInterface
|
||||
}
|
||||
}
|
||||
|
||||
public bool HandleCanFocusDown(ScreenCoordinates pointerPosition)
|
||||
public bool HandleCanFocusDown(
|
||||
ScreenCoordinates pointerPosition,
|
||||
[NotNullWhen(true)] out (Control control, Vector2i rel)? hitData)
|
||||
{
|
||||
var control = MouseGetControl(pointerPosition);
|
||||
var hit = MouseGetControlAndRel(pointerPosition);
|
||||
var pos = pointerPosition.Position;
|
||||
|
||||
// If we have a modal open and the mouse down was outside it, close said modal.
|
||||
@@ -342,6 +347,7 @@ namespace Robust.Client.UserInterface
|
||||
{
|
||||
ControlFocused?.ControlFocusExited();
|
||||
ControlFocused = top;
|
||||
hitData = null;
|
||||
return false; // prevent anything besides the top modal control from receiving input
|
||||
}
|
||||
}
|
||||
@@ -353,11 +359,14 @@ namespace Robust.Client.UserInterface
|
||||
|
||||
ReleaseKeyboardFocus();
|
||||
|
||||
if (control == null)
|
||||
if (hit == null)
|
||||
{
|
||||
hitData = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
var (control, rel) = hit.Value;
|
||||
|
||||
ControlFocused?.ControlFocusExited();
|
||||
ControlFocused = control;
|
||||
|
||||
@@ -366,6 +375,7 @@ namespace Robust.Client.UserInterface
|
||||
ControlFocused.GrabKeyboardFocus();
|
||||
}
|
||||
|
||||
hitData = (control, (Vector2i) rel);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -538,11 +548,16 @@ namespace Robust.Client.UserInterface
|
||||
}
|
||||
|
||||
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);
|
||||
return MouseFindControlAtPos(root, coordinates.Position);
|
||||
}
|
||||
|
||||
public ScreenCoordinates MousePositionScaled => ScreenToUIPosition(_inputManager.MouseScreenPosition);
|
||||
@@ -665,6 +680,9 @@ namespace Robust.Client.UserInterface
|
||||
void DoRender(WindowRoot root)
|
||||
{
|
||||
_render(renderHandle, root, Vector2i.Zero, Color.White, null);
|
||||
var drawingHandle = renderHandle.DrawingHandleScreen;
|
||||
drawingHandle.SetTransform(Vector2.Zero, Angle.Zero, Vector2.One);
|
||||
OnPostDrawUIRoot?.Invoke(new PostDrawUIRootEventArgs(root, drawingHandle));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -758,7 +776,7 @@ namespace Robust.Client.UserInterface
|
||||
}
|
||||
}
|
||||
|
||||
private Control? _mouseFindControlAtPos(Control control, Vector2 position)
|
||||
private static (Control control, Vector2 rel)? MouseFindControlAtPos(Control control, Vector2 position)
|
||||
{
|
||||
for (var i = control.ChildCount - 1; i >= 0; i--)
|
||||
{
|
||||
@@ -768,7 +786,7 @@ namespace Robust.Client.UserInterface
|
||||
continue;
|
||||
}
|
||||
|
||||
var maybeFoundOnChild = _mouseFindControlAtPos(child, position - child.PixelPosition);
|
||||
var maybeFoundOnChild = MouseFindControlAtPos(child, position - child.PixelPosition);
|
||||
if (maybeFoundOnChild != null)
|
||||
{
|
||||
return maybeFoundOnChild;
|
||||
@@ -777,7 +795,7 @@ namespace Robust.Client.UserInterface
|
||||
|
||||
if (control.MouseFilter != Control.MouseFilterMode.Ignore && control.HasPoint(position / control.UIScale))
|
||||
{
|
||||
return control;
|
||||
return (control, position);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -857,6 +875,17 @@ namespace Robust.Client.UserInterface
|
||||
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;
|
||||
}
|
||||
|
||||
private void _resetTooltipTimer()
|
||||
{
|
||||
_tooltipTimer = 0;
|
||||
|
||||
@@ -537,6 +537,7 @@ namespace Robust.Server
|
||||
// called right before main loop returns, do all saving/cleanup in here
|
||||
private void Cleanup()
|
||||
{
|
||||
_modLoader.Shutdown();
|
||||
IoCManager.Resolve<INetConfigurationManager>().FlushMessages();
|
||||
|
||||
// shut down networking, kicking all players.
|
||||
|
||||
@@ -330,6 +330,24 @@ namespace Robust.Shared
|
||||
public static readonly CVarDef<bool> DisplayWin32Experience =
|
||||
CVarDef.Create("display.win32_experience", false, CVar.CLIENTONLY);
|
||||
|
||||
/// <summary>
|
||||
/// The window icon set to use. Overriden by <c>GameControllerOptions</c> on startup.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Dynamically changing this does nothing.
|
||||
/// </remarks>
|
||||
public static readonly CVarDef<string> DisplayWindowIconSet =
|
||||
CVarDef.Create("display.window_icon_set", "", CVar.CLIENTONLY);
|
||||
|
||||
/// <summary>
|
||||
/// The splash logo to use. Overriden by <c>GameControllerOptions</c> on startup.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Dynamically changing this does nothing.
|
||||
/// </remarks>
|
||||
public static readonly CVarDef<string> DisplaySplashLogo =
|
||||
CVarDef.Create("display.splash_logo", "", CVar.CLIENTONLY);
|
||||
|
||||
/*
|
||||
* AUDIO
|
||||
*/
|
||||
|
||||
@@ -89,7 +89,7 @@ namespace Robust.Shared.Configuration
|
||||
{
|
||||
//or add another unregistered CVar
|
||||
//Note: the defaultValue is arbitrarily 0, it will get overwritten when the cvar is registered.
|
||||
cfgVar = new ConfigVar(tablePath, 0, CVar.NONE) { Value = tomlValue };
|
||||
cfgVar = new ConfigVar(tablePath, 0, CVar.NONE) {Value = tomlValue};
|
||||
_configVars.Add(tablePath, cfgVar);
|
||||
}
|
||||
|
||||
@@ -120,7 +120,8 @@ namespace Robust.Shared.Configuration
|
||||
|
||||
if (value == null)
|
||||
{
|
||||
Logger.ErrorS("cfg", $"CVar {name} has no value or default value, was the default value registered as null?");
|
||||
Logger.ErrorS("cfg",
|
||||
$"CVar {name} has no value or default value, was the default value registered as null?");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -144,14 +145,16 @@ namespace Robust.Shared.Configuration
|
||||
{
|
||||
tblObject = table.Add(curTblName, new Dictionary<string, TomlObject>()).Added;
|
||||
}
|
||||
table = tblObject as TomlTable ?? throw new InvalidConfigurationException($"[CFG] Object {curTblName} is being used like a table, but it is a {tblObject}. Are your CVar names formed properly?");
|
||||
|
||||
table = tblObject as TomlTable ?? throw new InvalidConfigurationException(
|
||||
$"[CFG] Object {curTblName} is being used like a table, but it is a {tblObject}. Are your CVar names formed properly?");
|
||||
}
|
||||
|
||||
//runtime unboxing, either this or generic hell... ¯\_(ツ)_/¯
|
||||
switch (value)
|
||||
{
|
||||
case Enum val:
|
||||
table.Add(keyName, (int)(object)val); // asserts Enum value != (ulong || long)
|
||||
table.Add(keyName, (int) (object) val); // asserts Enum value != (ulong || long)
|
||||
break;
|
||||
case int val:
|
||||
table.Add(keyName, val);
|
||||
@@ -186,7 +189,8 @@ namespace Robust.Shared.Configuration
|
||||
}
|
||||
}
|
||||
|
||||
public void RegisterCVar<T>(string name, T defaultValue, CVar flags = CVar.NONE, Action<T>? onValueChanged = null)
|
||||
public void RegisterCVar<T>(string name, T defaultValue, CVar flags = CVar.NONE,
|
||||
Action<T>? onValueChanged = null)
|
||||
where T : notnull
|
||||
{
|
||||
RegisterCVar(name, typeof(T), defaultValue, flags);
|
||||
@@ -284,14 +288,16 @@ namespace Robust.Shared.Configuration
|
||||
|
||||
if (!defField.IsInitOnly)
|
||||
{
|
||||
throw new InvalidOperationException($"Found CVarDef '{defField.Name}' on '{defField.DeclaringType?.FullName}' that is not readonly. Please mark it as readonly.");
|
||||
throw new InvalidOperationException(
|
||||
$"Found CVarDef '{defField.Name}' on '{defField.DeclaringType?.FullName}' that is not readonly. Please mark it as readonly.");
|
||||
}
|
||||
|
||||
var def = (CVarDef?) defField.GetValue(null);
|
||||
|
||||
if (def == null)
|
||||
{
|
||||
throw new InvalidOperationException($"CVarDef '{defField.Name}' on '{defField.DeclaringType?.FullName}' is null.");
|
||||
throw new InvalidOperationException(
|
||||
$"CVarDef '{defField.Name}' on '{defField.DeclaringType?.FullName}' is null.");
|
||||
}
|
||||
|
||||
RegisterCVar(def.Name, type, def.DefaultValue, def.Flags);
|
||||
@@ -345,7 +351,7 @@ namespace Robust.Shared.Configuration
|
||||
{
|
||||
if (_configVars.TryGetValue(name, out var cVar) && cVar.Registered)
|
||||
//TODO: Make flags work, required non-derpy net system.
|
||||
return (T)(cVar.OverrideValueParsed ?? cVar.Value ?? cVar.DefaultValue)!;
|
||||
return (T) (cVar.OverrideValueParsed ?? cVar.Value ?? cVar.DefaultValue)!;
|
||||
|
||||
throw new InvalidConfigurationException($"Trying to get unregistered variable '{name}'");
|
||||
}
|
||||
@@ -373,14 +379,17 @@ namespace Robust.Shared.Configuration
|
||||
if (_configVars.TryGetValue(key, out var cfgVar))
|
||||
{
|
||||
cfgVar.OverrideValue = value;
|
||||
cfgVar.OverrideValueParsed = ParseOverrideValue(value, cfgVar.DefaultValue?.GetType());
|
||||
InvokeValueChanged(cfgVar, cfgVar.OverrideValueParsed);
|
||||
if (cfgVar.Registered)
|
||||
{
|
||||
cfgVar.OverrideValueParsed = ParseOverrideValue(value, cfgVar.DefaultValue?.GetType());
|
||||
InvokeValueChanged(cfgVar, cfgVar.OverrideValueParsed);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//or add another unregistered CVar
|
||||
//Note: the defaultValue is arbitrarily 0, it will get overwritten when the cvar is registered.
|
||||
var cVar = new ConfigVar(key, 0, CVar.NONE) { OverrideValue = value };
|
||||
var cVar = new ConfigVar(key, 0, CVar.NONE) {OverrideValue = value};
|
||||
_configVars.Add(key, cVar);
|
||||
}
|
||||
}
|
||||
@@ -511,14 +520,19 @@ namespace Robust.Shared.Configuration
|
||||
public InvalidConfigurationException()
|
||||
{
|
||||
}
|
||||
|
||||
public InvalidConfigurationException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public InvalidConfigurationException(string message, Exception inner) : base(message, inner)
|
||||
{
|
||||
}
|
||||
|
||||
protected InvalidConfigurationException(
|
||||
System.Runtime.Serialization.SerializationInfo info,
|
||||
System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
|
||||
System.Runtime.Serialization.StreamingContext context) : base(info, context)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +103,17 @@ namespace Robust.Shared.ContentPack
|
||||
_testingCallbacks.Add(testingCallbacks);
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
foreach (var module in Mods)
|
||||
{
|
||||
foreach (var entryPoint in module.EntryPoints)
|
||||
{
|
||||
entryPoint.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds info about a loaded assembly.
|
||||
/// </summary>
|
||||
|
||||
@@ -69,5 +69,7 @@ namespace Robust.Shared.ContentPack
|
||||
void SetEnableSandboxing(bool sandboxing);
|
||||
|
||||
Func<string, Stream?>? VerifierExtraLoadHandler { get; set; }
|
||||
|
||||
void Shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // Gives access to Castle(Moq)
|
||||
[assembly: InternalsVisibleTo("Content.Benchmarks")]
|
||||
[assembly: InternalsVisibleTo("Robust.Benchmarks")]
|
||||
[assembly: InternalsVisibleTo("Robust.Client.CEF")]
|
||||
|
||||
#if NET5_0
|
||||
[module: SkipLocalsInit]
|
||||
|
||||
@@ -217,7 +217,7 @@ namespace Robust.UnitTesting.Client.UserInterface
|
||||
|
||||
_userInterfaceManager.RootControl.Arrange(new UIBox2(0, 0, 50, 50));
|
||||
|
||||
_userInterfaceManager.HandleCanFocusDown(new ScreenCoordinates(30, 30, WindowId.Main));
|
||||
_userInterfaceManager.HandleCanFocusDown(new ScreenCoordinates(30, 30, WindowId.Main), out _);
|
||||
|
||||
Assert.That(_userInterfaceManager.KeyboardFocused, NUnit.Framework.Is.EqualTo(control));
|
||||
_userInterfaceManager.ReleaseKeyboardFocus();
|
||||
|
||||
1
cefglue
Submodule
1
cefglue
Submodule
Submodule cefglue added at 7810f236c5
Reference in New Issue
Block a user