From b3eb17f61e3f91cd12a139ec18ee8c8156e6f435 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 7 Nov 2021 20:46:56 +0100 Subject: [PATCH] Add `Platform.Users` to handle users per-platform --- .../Platform/Android/AndroidPlatform.cpp | 6 -- .../Engine/Platform/Android/AndroidPlatform.h | 1 - Source/Engine/Platform/Base/PlatformBase.cpp | 25 ++++++ Source/Engine/Platform/Base/PlatformBase.h | 21 ++++- Source/Engine/Platform/Base/PlatformUtils.h | 20 +++++ Source/Engine/Platform/Base/UserBase.h | 29 +++++++ Source/Engine/Platform/GDK/GDKPlatform.cpp | 80 ++++++------------- Source/Engine/Platform/GDK/GDKPlatform.h | 1 - Source/Engine/Platform/GDK/GDKUser.h | 38 +++++++++ .../Engine/Platform/Linux/LinuxPlatform.cpp | 16 ++-- Source/Engine/Platform/Linux/LinuxPlatform.h | 1 - Source/Engine/Platform/Types.h | 18 +++++ Source/Engine/Platform/UWP/UWPPlatform.cpp | 10 +-- Source/Engine/Platform/UWP/UWPPlatform.h | 1 - Source/Engine/Platform/User.h | 15 ++++ .../Platform/Windows/WindowsPlatform.cpp | 14 ++-- .../Engine/Platform/Windows/WindowsPlatform.h | 1 - 17 files changed, 203 insertions(+), 94 deletions(-) create mode 100644 Source/Engine/Platform/Base/PlatformUtils.h create mode 100644 Source/Engine/Platform/Base/UserBase.h create mode 100644 Source/Engine/Platform/GDK/GDKUser.h create mode 100644 Source/Engine/Platform/User.h diff --git a/Source/Engine/Platform/Android/AndroidPlatform.cpp b/Source/Engine/Platform/Android/AndroidPlatform.cpp index 48499938a..4dd8a7174 100644 --- a/Source/Engine/Platform/Android/AndroidPlatform.cpp +++ b/Source/Engine/Platform/Android/AndroidPlatform.cpp @@ -956,12 +956,6 @@ String AndroidPlatform::GetComputerName() return DeviceModel; } -String AndroidPlatform::GetUserName() -{ - // TODO: add support for username on Android - return String::Empty; -} - bool AndroidPlatform::GetHasFocus() { return HasFocus; diff --git a/Source/Engine/Platform/Android/AndroidPlatform.h b/Source/Engine/Platform/Android/AndroidPlatform.h index 4f0228b9e..85988b019 100644 --- a/Source/Engine/Platform/Android/AndroidPlatform.h +++ b/Source/Engine/Platform/Android/AndroidPlatform.h @@ -110,7 +110,6 @@ public: static ScreenOrientationType GetScreenOrientationType(); static String GetUserLocaleName(); static String GetComputerName(); - static String GetUserName(); static bool GetHasFocus(); static bool GetIsPaused(); static bool CanOpenUrl(const StringView& url); diff --git a/Source/Engine/Platform/Base/PlatformBase.cpp b/Source/Engine/Platform/Base/PlatformBase.cpp index f641334a4..382aff1b4 100644 --- a/Source/Engine/Platform/Base/PlatformBase.cpp +++ b/Source/Engine/Platform/Base/PlatformBase.cpp @@ -5,6 +5,7 @@ #include "Engine/Platform/MemoryStats.h" #include "Engine/Platform/MessageBox.h" #include "Engine/Platform/FileSystem.h" +#include "Engine/Platform/User.h" #include "Engine/Core/Log.h" #include "Engine/Core/Types/DateTime.h" #include "Engine/Core/Types/TimeSpan.h" @@ -40,6 +41,9 @@ static_assert(sizeof(float) == 4, "Invalid float type size."); static_assert(sizeof(double) == 8, "Invalid double type size."); float PlatformBase::CustomDpiScale = 1.0f; +Array> PlatformBase::Users; +Delegate PlatformBase::UserAdded; +Delegate PlatformBase::UserRemoved; const Char* ToString(NetworkConnectionType value) { @@ -102,6 +106,22 @@ const Char* ToString(ThreadPriority value) } } +UserBase::UserBase(const String& name) + : UserBase(SpawnParams(Guid::New(), TypeInitializer), name) +{ +} + +UserBase::UserBase(const SpawnParams& params, const String& name) + : PersistentScriptingObject(params) + , _name(name) +{ +} + +String UserBase::GetName() const +{ + return _name; +} + bool PlatformBase::Init() { #if BUILD_DEBUG @@ -462,6 +482,11 @@ ScreenOrientationType PlatformBase::GetScreenOrientationType() return ScreenOrientationType::Unknown; } +String PlatformBase::GetUserName() +{ + return Users.Count() != 0 ? Users[0]->GetName() : String::Empty; +} + bool PlatformBase::GetIsPaused() { return false; diff --git a/Source/Engine/Platform/Base/PlatformBase.h b/Source/Engine/Platform/Base/PlatformBase.h index 2b67f3720..e4a5c2806 100644 --- a/Source/Engine/Platform/Base/PlatformBase.h +++ b/Source/Engine/Platform/Base/PlatformBase.h @@ -595,7 +595,7 @@ public: API_PROPERTY() static ScreenOrientationType GetScreenOrientationType(); /// - /// Gets the current locale culture (eg. "pl-PL" or "en-US"). + /// Gets the current locale culture (eg. "pl-PL" or "en-US"). /// API_PROPERTY() static String GetUserLocaleName() = delete; @@ -607,7 +607,7 @@ public: /// /// Gets the user name. /// - API_PROPERTY() static String GetUserName() = delete; + API_PROPERTY() static String GetUserName(); /// /// Returns true if app has user focus. @@ -625,6 +625,23 @@ public: /// The result. static void CreateGuid(Guid& result); +public: + + /// + /// The list of users. + /// + API_FIELD(ReadOnly) static Array> Users; + + /// + /// Event called when user gets added (eg. logged in). + /// + API_EVENT() static Delegate UserAdded; + + /// + /// Event called when user gets removed (eg. logged out). + /// + API_EVENT() static Delegate UserRemoved; + public: /// diff --git a/Source/Engine/Platform/Base/PlatformUtils.h b/Source/Engine/Platform/Base/PlatformUtils.h new file mode 100644 index 000000000..003993461 --- /dev/null +++ b/Source/Engine/Platform/Base/PlatformUtils.h @@ -0,0 +1,20 @@ +// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved. + +#pragma once + +#include "Engine/Platform/Platform.h" +#include "Engine/Platform/User.h" +#include "Engine/Core/Collections/Array.h" + +inline void OnPlatformUserAdd(User* user) +{ + Platform::Users.Add(user); + Platform::UserAdded(user); +} + +inline void OnPlatformUserRemove(User* user) +{ + Platform::Users.Remove(user); + Platform::UserRemoved(user); + Delete(user); +} diff --git a/Source/Engine/Platform/Base/UserBase.h b/Source/Engine/Platform/Base/UserBase.h new file mode 100644 index 000000000..c946c4ba1 --- /dev/null +++ b/Source/Engine/Platform/Base/UserBase.h @@ -0,0 +1,29 @@ +// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved. + +#pragma once + +#include "Engine/Core/Types/String.h" +#include "Engine/Scripting/ScriptingObject.h" + +API_INJECT_CPP_CODE("#include \"Engine/Platform/User.h\""); + +/// +/// Native platform user object. +/// +API_CLASS(NoSpawn, NoConstructor, Sealed, Name="User") class FLAXENGINE_API UserBase : public PersistentScriptingObject +{ +DECLARE_SCRIPTING_TYPE_NO_SPAWN(UserBase); +protected: + + const String _name; + +public: + + UserBase(const String& name); + UserBase(const SpawnParams& params, const String& name); + + /// + /// Gets the username. + /// + API_PROPERTY() String GetName() const; +}; diff --git a/Source/Engine/Platform/GDK/GDKPlatform.cpp b/Source/Engine/Platform/GDK/GDKPlatform.cpp index c1c0ec685..782172dcc 100644 --- a/Source/Engine/Platform/GDK/GDKPlatform.cpp +++ b/Source/Engine/Platform/GDK/GDKPlatform.cpp @@ -8,6 +8,7 @@ #include "Engine/Platform/WindowsManager.h" #include "Engine/Platform/MemoryStats.h" #include "Engine/Platform/BatteryInfo.h" +#include "Engine/Platform/Base/PlatformUtils.h" #include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Core/Log.h" #include "Engine/Core/Collections/Array.h" @@ -30,26 +31,6 @@ void* GDKPlatform::Instance = nullptr; Delegate<> GDKPlatform::OnSuspend; Delegate<> GDKPlatform::OnResume; -struct User -{ - XUserHandle UserHandle; - XUserLocalId LocalId; - Array> AssociatedDevices; - - void Set(XUserHandle userHandle, XUserLocalId localId) - { - UserHandle = userHandle; - LocalId = localId; - AssociatedDevices.Clear(); - } - - void Unset() - { - XUserCloseHandle(UserHandle); - AssociatedDevices.Clear(); - } -}; - namespace { bool IsSuspended = false; @@ -58,11 +39,24 @@ namespace PAPPSTATE_REGISTRATION Plm = {}; String UserLocale, ComputerName; XTaskQueueHandle TaskQueue = nullptr; - Array> Users; XTaskQueueRegistrationToken UserChangeEventCallbackToken; XTaskQueueRegistrationToken UserDeviceAssociationChangedCallbackToken; } +User* FindUser(const XUserLocalId& id) +{ + User* result = nullptr; + for (auto& user : Platform::Users) + { + if (user->LocalId.value == id.value) + { + result = user; + break; + } + } + return result; +} + LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch (msg) @@ -76,7 +70,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) // Complete deferral SetEvent(PlmSuspendComplete); - (void)WaitForSingleObject(PlmSignalResume, INFINITE); + WaitForSingleObject(PlmSignalResume, INFINITE); IsSuspended = false; LOG(Info, "Resuming application"); @@ -100,23 +94,20 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) return DefWindowProc(hWnd, msg, wParam, lParam); } -void CALLBACK UserChangeEventCallback(_In_opt_ void* context,_In_ XUserLocalId userLocalId, _In_ XUserChangeEvent event) +void CALLBACK UserChangeEventCallback(_In_opt_ void* context, _In_ XUserLocalId userLocalId, _In_ XUserChangeEvent event) { LOG(Info, "User event (userLocalId: {0}, event: {1})", userLocalId.value, (int32)event); + auto user = FindUser(userLocalId); switch (event) { case XUserChangeEvent::SignedInAgain: break; case XUserChangeEvent::SignedOut: - for (int32 i = 0; i < Users.Count(); i++) + if (user) { - if (Users[i].LocalId.value == userLocalId.value) - { - Users[i].Unset(); - Users.RemoveAt(i); - break; - } + // Logout + OnPlatformUserRemove(user); } break; default: ; @@ -136,20 +127,6 @@ String ToString(const APP_LOCAL_DEVICE_ID& deviceId) *reinterpret_cast(&deviceId.value[28])); } -User* FindUser(const XUserLocalId& id) -{ - User* result = nullptr; - for (auto& user : Users) - { - if (user.LocalId.value == id.value) - { - result = &user; - break; - } - } - return result; -} - void CALLBACK UserDeviceAssociationChangedCallback(_In_opt_ void* context,_In_ const XUserDeviceAssociationChange* change) { LOG(Info, "User device association event (deviceId: {0}, oldUser: {1}, newUser: {2})", ToString(change->deviceId), change->oldUser.value, change->newUser.value); @@ -185,7 +162,7 @@ void OnMainWindowCreated(HWND hWnd) PostMessage(reinterpret_cast(context), WM_USER, 0, 0); // To defer suspend, you must wait to exit this callback - (void)WaitForSingleObject(PlmSuspendComplete, INFINITE); + WaitForSingleObject(PlmSuspendComplete, INFINITE); } else { @@ -209,9 +186,9 @@ void CALLBACK AddUserComplete(_In_ XAsyncBlock* ab) if (FindUser(localId) == nullptr) { - // Add user - auto& user = Users.AddOne(); - user.Set(userHandle, userLocalId); + // Login + auto user = New(userHandle, userLocalId, String::Empty); + OnPlatformUserAdd(user); } } @@ -506,11 +483,6 @@ String GDKPlatform::GetComputerName() return ComputerName; } -String GDKPlatform::GetUserName() -{ - return String::Empty; -} - bool GDKPlatform::GetHasFocus() { return !IsSuspended; @@ -524,7 +496,7 @@ bool GDKPlatform::CanOpenUrl(const StringView& url) void GDKPlatform::OpenUrl(const StringView& url) { const StringAsANSI<> urlANSI(url.Get(), url.Length()); - XLaunchUri(Users[0].UserHandle, urlANSI.Get()); + XLaunchUri(Users[0]->UserHandle, urlANSI.Get()); } struct GetMonitorBoundsData diff --git a/Source/Engine/Platform/GDK/GDKPlatform.h b/Source/Engine/Platform/GDK/GDKPlatform.h index 65d712bb9..d58127e28 100644 --- a/Source/Engine/Platform/GDK/GDKPlatform.h +++ b/Source/Engine/Platform/GDK/GDKPlatform.h @@ -60,7 +60,6 @@ public: static int32 GetDpi(); static String GetUserLocaleName(); static String GetComputerName(); - static String GetUserName(); static bool GetHasFocus(); static bool CanOpenUrl(const StringView& url); static void OpenUrl(const StringView& url); diff --git a/Source/Engine/Platform/GDK/GDKUser.h b/Source/Engine/Platform/GDK/GDKUser.h new file mode 100644 index 000000000..30645a6f2 --- /dev/null +++ b/Source/Engine/Platform/GDK/GDKUser.h @@ -0,0 +1,38 @@ +// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved. + +#pragma once + +#if PLATFORM_GDK + +#include "Engine/Platform/Base/UserBase.h" +#include "Engine/Core/Collections/Array.h" +#include "Engine/Platform/Win32/IncludeWindowsHeaders.h" +#include + +/// +/// Implementation of the user for GDK platform. +/// +class FLAXENGINE_API GDKUser : public UserBase +{ +public: + + GDKUser(XUserHandle userHandle, XUserLocalId localId, const String& name) + : UserBase(name) + { + UserHandle = userHandle; + LocalId = localId; + } + + ~GDKUser() + { + XUserCloseHandle(UserHandle); + } + +public: + + XUserHandle UserHandle; + XUserLocalId LocalId; + Array> AssociatedDevices; +}; + +#endif diff --git a/Source/Engine/Platform/Linux/LinuxPlatform.cpp b/Source/Engine/Platform/Linux/LinuxPlatform.cpp index 7e5b55228..9c73b5d1f 100644 --- a/Source/Engine/Platform/Linux/LinuxPlatform.cpp +++ b/Source/Engine/Platform/Linux/LinuxPlatform.cpp @@ -21,6 +21,7 @@ #include "Engine/Platform/WindowsManager.h" #include "Engine/Platform/Clipboard.h" #include "Engine/Platform/IGuiData.h" +#include "Engine/Platform/Base/PlatformUtils.h" #include "Engine/Utilities/StringConverter.h" #include "Engine/Threading/Threading.h" #include "Engine/Engine/Engine.h" @@ -51,7 +52,7 @@ CPUInfo UnixCpu; int ClockSource; Guid DeviceId; -String UserLocale, ComputerName, UserName, HomeDir; +String UserLocale, ComputerName, HomeDir; byte MacAddress[6]; #define UNIX_APP_BUFF_SIZE 256 @@ -2014,6 +2015,10 @@ bool LinuxPlatform::Init() UnixCpu.CacheLineSize = sysconf(_SC_LEVEL1_DCACHE_LINESIZE); ASSERT(UnixCpu.CacheLineSize && Math::IsPowerOfTwo(UnixCpu.CacheLineSize)); + // Get user name string + getlogin_r(buffer, UNIX_APP_BUFF_SIZE); + OnPlatformUserAdd(New(String(buffer)); + UnixGetMacAddress(MacAddress); // Generate unique device ID @@ -2054,10 +2059,6 @@ bool LinuxPlatform::Init() gethostname(buffer, UNIX_APP_BUFF_SIZE); ComputerName = String(buffer); - // Get user name string - getlogin_r(buffer, UNIX_APP_BUFF_SIZE); - UserName = String(buffer); - // Get home dir struct passwd pw; struct passwd* result = NULL; @@ -2603,11 +2604,6 @@ String LinuxPlatform::GetComputerName() return ComputerName; } -String LinuxPlatform::GetUserName() -{ - return UserName; -} - bool LinuxPlatform::GetHasFocus() { // Check if any window is focused diff --git a/Source/Engine/Platform/Linux/LinuxPlatform.h b/Source/Engine/Platform/Linux/LinuxPlatform.h index b77162900..6e1e9e356 100644 --- a/Source/Engine/Platform/Linux/LinuxPlatform.h +++ b/Source/Engine/Platform/Linux/LinuxPlatform.h @@ -113,7 +113,6 @@ public: static int32 GetDpi(); static String GetUserLocaleName(); static String GetComputerName(); - static String GetUserName(); static bool GetHasFocus(); static bool CanOpenUrl(const StringView& url); static void OpenUrl(const StringView& url); diff --git a/Source/Engine/Platform/Types.h b/Source/Engine/Platform/Types.h index 2120fb1d9..3529dcf47 100644 --- a/Source/Engine/Platform/Types.h +++ b/Source/Engine/Platform/Types.h @@ -24,6 +24,8 @@ class WindowsWindow; typedef WindowsWindow Window; class Win32Network; typedef Win32Network Network; +class UserBase; +typedef UserBase User; #elif PLATFORM_UWP @@ -47,6 +49,8 @@ class UWPWindow; typedef UWPWindow Window; class Win32Network; typedef Win32Network Network; +class UserBase; +typedef UserBase User; #elif PLATFORM_LINUX @@ -70,6 +74,8 @@ class LinuxWindow; typedef LinuxWindow Window; class UnixNetwork; typedef UnixNetwork Network; +class UserBase; +typedef UserBase User; #elif PLATFORM_PS4 @@ -93,6 +99,8 @@ class PS4Window; typedef PS4Window Window; class PS4Network; typedef PS4Network Network; +class PS4User; +typedef PS4User User; #elif PLATFORM_PS5 @@ -116,6 +124,8 @@ class PS5Window; typedef PS5Window Window; class PS5Network; typedef PS5Network Network; +class PS5User; +typedef PS5User User; #elif PLATFORM_XBOX_ONE @@ -139,6 +149,8 @@ class GDKWindow; typedef GDKWindow Window; class NetworkBase; typedef NetworkBase Network; +class GDKUser; +typedef GDKUser User; #elif PLATFORM_XBOX_SCARLETT @@ -162,6 +174,8 @@ class GDKWindow; typedef GDKWindow Window; class NetworkBase; typedef NetworkBase Network; +class GDKUser; +typedef GDKUser User; #elif PLATFORM_ANDROID @@ -185,6 +199,8 @@ class AndroidWindow; typedef AndroidWindow Window; class UnixNetwork; typedef UnixNetwork Network; +class UserBase; +typedef UserBase User; #elif PLATFORM_SWITCH @@ -208,6 +224,8 @@ class SwitchWindow; typedef SwitchWindow Window; class SwitchNetwork; typedef SwitchNetwork Network; +class UserBase; +typedef UserBase User; #else diff --git a/Source/Engine/Platform/UWP/UWPPlatform.cpp b/Source/Engine/Platform/UWP/UWPPlatform.cpp index aeb799a2c..24c39b779 100644 --- a/Source/Engine/Platform/UWP/UWPPlatform.cpp +++ b/Source/Engine/Platform/UWP/UWPPlatform.cpp @@ -13,7 +13,7 @@ namespace { - String UserLocale, ComputerName, UserName; + String UserLocale, ComputerName; Vector2 VirtualScreenSize = Vector2(0.0f); int32 SystemDpi = 96; } @@ -58,9 +58,6 @@ bool UWPPlatform::Init() ComputerName = String(buffer); } - // Cannot access user name with a direct API - UserName = String::Empty; - SystemDpi = CUWPPlatform->GetDpi(); Input::Mouse = Impl::Mouse = New(); Input::Keyboard = Impl::Keyboard = New(); @@ -145,11 +142,6 @@ String UWPPlatform::GetComputerName() return ComputerName; } -String UWPPlatform::GetUserName() -{ - return UserName; -} - bool UWPPlatform::GetHasFocus() { // TODO: impl this diff --git a/Source/Engine/Platform/UWP/UWPPlatform.h b/Source/Engine/Platform/UWP/UWPPlatform.h index 9a946ce98..a8149133a 100644 --- a/Source/Engine/Platform/UWP/UWPPlatform.h +++ b/Source/Engine/Platform/UWP/UWPPlatform.h @@ -34,7 +34,6 @@ public: static int32 GetDpi(); static String GetUserLocaleName(); static String GetComputerName(); - static String GetUserName(); static bool GetHasFocus(); static bool CanOpenUrl(const StringView& url); static void OpenUrl(const StringView& url); diff --git a/Source/Engine/Platform/User.h b/Source/Engine/Platform/User.h new file mode 100644 index 000000000..bce5e2ff2 --- /dev/null +++ b/Source/Engine/Platform/User.h @@ -0,0 +1,15 @@ +// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved. + +#pragma once + +#if PLATFORM_XBOX_ONE || PLATFORM_XBOX_SCARLETT +#include "GDK/GDKUser.h" +#elif PLATFORM_PS4 +#include "Platforms/PS4/Engine/Platform/PS4User.h" +#elif PLATFORM_PS5 +#include "Platforms/PS5/Engine/Platform/PS5User.h" +#else +#include "Base/UserBase.h" +#endif + +#include "Types.h" diff --git a/Source/Engine/Platform/Windows/WindowsPlatform.cpp b/Source/Engine/Platform/Windows/WindowsPlatform.cpp index f4b9e1295..9922caaf8 100644 --- a/Source/Engine/Platform/Windows/WindowsPlatform.cpp +++ b/Source/Engine/Platform/Windows/WindowsPlatform.cpp @@ -9,6 +9,7 @@ #include "Engine/Platform/WindowsManager.h" #include "Engine/Platform/MemoryStats.h" #include "Engine/Platform/BatteryInfo.h" +#include "Engine/Platform/Base/PlatformUtils.h" #include "Engine/Engine/Globals.h" #include "Engine/Core/Log.h" #include "Engine/Core/Collections/Dictionary.h" @@ -55,9 +56,9 @@ void DbgHelpUnlock() namespace { - String UserLocale, ComputerName, UserName, WindowsName; + String UserLocale, ComputerName, WindowsName; HANDLE EngineMutex = nullptr; - Rectangle VirtualScreenBounds = Rectangle(0.0f, 0.0f, 0.0f, 0.0f); + Rectangle VirtualScreenBounds(0.0f, 0.0f, 0.0f, 0.0f); int32 VersionMajor = 0; int32 VersionMinor = 0; int32 VersionBuild = 0; @@ -647,10 +648,12 @@ bool WindowsPlatform::Init() } // Get user name string + String userName; if (GetUserNameW(buffer, &tmp)) { - UserName = String(buffer); + userName = String(buffer); } + OnPlatformUserAdd(New(userName)); WindowsInput::Init(); @@ -794,11 +797,6 @@ String WindowsPlatform::GetComputerName() return ComputerName; } -String WindowsPlatform::GetUserName() -{ - return UserName; -} - bool WindowsPlatform::GetHasFocus() { DWORD foregroundProcess; diff --git a/Source/Engine/Platform/Windows/WindowsPlatform.h b/Source/Engine/Platform/Windows/WindowsPlatform.h index a7b37b6af..07a6fa471 100644 --- a/Source/Engine/Platform/Windows/WindowsPlatform.h +++ b/Source/Engine/Platform/Windows/WindowsPlatform.h @@ -68,7 +68,6 @@ public: static int32 GetDpi(); static String GetUserLocaleName(); static String GetComputerName(); - static String GetUserName(); static bool GetHasFocus(); static bool CanOpenUrl(const StringView& url); static void OpenUrl(const StringView& url);