Files
FlaxEngine/Source/Engine/Scripting/Scripting.cs
Luke Schneider ad28a3fdbf Better light theme (Style) support, and a Default light theme (as a secondary option)
1) Added ForegroundViewport as a new color.  It is used in the main game viewport (ViewportWidgetButton), and the viewport for rendering of particles and materials.  It is needed because the default foreground in a Light theme is black, but black does not work well in a viewport.  A new color seemed appropriate.

2) Fixed the profiler window to use the Foreground color in multiple text elements, instead of Color.White (or no default TitleColor).  This includes  the Row class, Asset class, SingleChart class, Timeline Class, and more.

3) Added a second theme/Style (DefaultLight) to include with the engine.  It uses RGB float values because those were easier to transfer from the saved values that I had created (and they're easier for me to edit if necessary).  I tried to emulate how the Default theme is created/loaded/etc as closely as possible.
2023-09-27 21:54:34 -05:00

396 lines
14 KiB
C#

// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.Marshalling;
using System.Threading;
using System.Threading.Tasks;
using FlaxEngine.GUI;
using FlaxEngine.Interop;
namespace FlaxEngine
{
/// <summary>
/// The async tasks scheduler that runs the tasks on a main thread during game update.
/// </summary>
/// <seealso cref="System.Threading.Tasks.TaskScheduler" />
/// <seealso cref="System.IDisposable" />
internal class MainThreadTaskScheduler : TaskScheduler, IDisposable
{
private readonly BlockingCollection<Task> _tasks = new BlockingCollection<Task>();
internal void Execute()
{
while (_tasks.TryTake(out var task))
{
TryExecuteTask(task);
}
}
private void Dispose(bool disposing)
{
if (!disposing)
return;
_tasks.CompleteAdding();
_tasks.Dispose();
}
/// <inheritdoc />
protected override void QueueTask(Task task)
{
_tasks.Add(task);
}
/// <inheritdoc />
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
return false;
}
/// <inheritdoc />
protected override IEnumerable<Task> GetScheduledTasks()
{
return _tasks.ToArray();
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
/// <summary>
/// C# scripting service.
/// </summary>
public static partial class Scripting
{
private static readonly List<Action> UpdateActions = new List<Action>();
private static readonly MainThreadTaskScheduler MainThreadTaskScheduler = new MainThreadTaskScheduler();
/// <summary>
/// Occurs on scripting update.
/// </summary>
public static event Action Update;
/// <summary>
/// Occurs on scripting late update.
/// </summary>
public static event Action LateUpdate;
/// <summary>
/// Occurs on scripting fixed update.
/// </summary>
public static event Action FixedUpdate;
/// <summary>
/// Occurs on scripting late fixed update.
/// </summary>
public static event Action LateFixedUpdate;
/// <summary>
/// Occurs on scripting draw update. Called during frame rendering and can be used to invoke custom rendering with GPUDevice.
/// </summary>
public static event Action Draw;
/// <summary>
/// Occurs when scripting engine is disposing. Engine is during closing and some services may be unavailable (eg. loading scenes). This may be called after the engine fatal error event.
/// </summary>
public static event Action Exit;
/// <summary>
/// Gets the async tasks scheduler that runs the tasks on a main thread during game update.
/// </summary>
public static TaskScheduler MainThreadScheduler => MainThreadTaskScheduler;
/// <summary>
/// Calls the given action on the next scripting update.
/// </summary>
/// <param name="action">The action to invoke.</param>
public static void InvokeOnUpdate(Action action)
{
lock (UpdateActions)
{
UpdateActions.Add(action);
}
}
/// <summary>
/// Queues the specified work to run on the main thread and returns a <see cref="T:System.Threading.Tasks.Task" /> object that represents that work.
/// </summary>
/// <param name="action">The work to execute asynchronously</param>
/// <returns>A task that represents the work queued to execute in the pool.</returns>
public static Task RunOnUpdate(Action action)
{
return Task.Factory.StartNew(action, CancellationToken.None, TaskCreationOptions.None, MainThreadTaskScheduler);
}
private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
{
if (e.ExceptionObject is Exception exception)
{
Debug.LogError("Unhandled Exception: " + exception.Message);
Debug.LogException(exception);
}
}
private static void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
{
if (e.Exception != null)
{
foreach (var ex in e.Exception.InnerExceptions)
{
Debug.LogError("Unhandled Task Exception: " + ex.Message);
Debug.LogException(ex);
}
}
}
/// <summary>
/// Initializes Flax API. Called before everything else from native code.
/// </summary>
internal static void Init()
{
#if BUILD_DEBUG
Debug.Logger.LogHandler.LogWrite(LogType.Info, "Using FlaxAPI in Debug");
#elif BUILD_DEVELOPMENT
Debug.Logger.LogHandler.LogWrite(LogType.Info, "Using FlaxAPI in Development");
#elif BUILD_RELEASE
Debug.Logger.LogHandler.LogWrite(LogType.Info, "Using FlaxAPI in Release");
#endif
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
Localization.LocalizationChanged += OnLocalizationChanged;
OnLocalizationChanged();
if (!Engine.IsEditor)
{
CreateGuiStyle();
}
}
private static void OnLocalizationChanged()
{
var currentThread = Thread.CurrentThread;
var language = Localization.CurrentLanguage;
if (language != null)
currentThread.CurrentUICulture = language;
var culture = Localization.CurrentCulture;
if (culture != null)
currentThread.CurrentCulture = culture;
}
/// <summary>
/// Sets the managed window as a main game window. Called after creating game window by the native code.
/// </summary>
/// <param name="window">The window.</param>
internal static void SetWindow(Window window)
{
// Link it as a GUI root control
window.GUI.BackgroundColor = Color.Transparent;
RootControl.GameRoot = window.GUI;
}
internal static Type MakeGenericType(Type genericType, Type[] genericArgs)
{
return genericType.MakeGenericType(genericArgs);
}
internal static void AddDictionaryItem(IDictionary dictionary, object key, object value)
{
// TODO: more generic approach to properly add value that is of custom boxed type? (eg. via NativeInterop.MarshalToManaged)
if (value is ManagedArray managedArray)
{
var managedArrayHandle = ManagedHandle.Alloc(managedArray, GCHandleType.Normal);
value = NativeInterop.MarshalToManaged((IntPtr)managedArrayHandle, managedArray.ArrayType);
managedArrayHandle.Free();
}
dictionary.Add(key, value);
}
internal static object[] GetDictionaryKeys(IDictionary dictionary)
{
if (dictionary == null)
return null;
var keys = dictionary.Keys;
var result = new object[keys.Count];
keys.CopyTo(result, 0);
return result;
}
internal static ManagedHandle VersionToManaged(int major, int minor, int build, int revision)
{
Version version = new Version(major, minor, Math.Max(build, 0), Math.Max(revision, 0));
return ManagedHandle.Alloc(version);
}
internal static ManagedHandle CultureInfoToManaged(int lcid)
{
return ManagedHandle.Alloc(new CultureInfo(lcid));
}
[StructLayout(LayoutKind.Sequential)]
internal struct VersionNative
{
public int Major;
public int Minor;
public int Build;
public int Revision;
}
internal static void VersionToNative(ManagedHandle versionHandle, ref int major, ref int minor, ref int build, ref int revision)
{
Version version = Unsafe.As<Version>(versionHandle.Target);
if (version != null)
{
major = version.Major;
minor = version.Minor;
build = version.Build;
revision = version.Revision;
}
}
private static void CreateGuiStyle()
{
var style = new Style
{
Background = Color.FromBgra(0xFF1C1C1C),
LightBackground = Color.FromBgra(0xFF2D2D30),
Foreground = Color.FromBgra(0xFFFFFFFF),
ForegroundGrey = Color.FromBgra(0xFFA9A9B3),
ForegroundDisabled = Color.FromBgra(0xFF787883),
ForegroundViewport = Color.FromBgra(0xFFFFFFFF),
BackgroundHighlighted = Color.FromBgra(0xFF54545C),
BorderHighlighted = Color.FromBgra(0xFF6A6A75),
BackgroundSelected = Color.FromBgra(0xFF007ACC),
BorderSelected = Color.FromBgra(0xFF1C97EA),
BackgroundNormal = Color.FromBgra(0xFF3F3F46),
BorderNormal = Color.FromBgra(0xFF54545C),
TextBoxBackground = Color.FromBgra(0xFF333337),
ProgressNormal = Color.FromBgra(0xFF0ad328),
TextBoxBackgroundSelected = Color.FromBgra(0xFF3F3F46),
CollectionBackgroundColor = Color.FromBgra(0x14CCCCCC),
SharedTooltip = new Tooltip(),
};
style.DragWindow = style.BackgroundSelected * 0.7f;
Style.Current = style;
}
internal static void Internal_Update()
{
Update?.Invoke();
lock (UpdateActions)
{
for (int i = 0; i < UpdateActions.Count; i++)
{
try
{
UpdateActions[i]();
}
catch (Exception ex)
{
Debug.LogException(ex);
}
}
UpdateActions.Clear();
}
MainThreadTaskScheduler.Execute();
}
internal static void Internal_LateUpdate()
{
LateUpdate?.Invoke();
}
internal static void Internal_FixedUpdate()
{
FixedUpdate?.Invoke();
}
internal static void Internal_LateFixedUpdate()
{
LateFixedUpdate?.Invoke();
}
internal static void Internal_Draw()
{
Draw?.Invoke();
}
internal static void Internal_Exit()
{
Exit?.Invoke();
MainThreadTaskScheduler.Dispose();
Json.JsonSerializer.Dispose();
}
/// <summary>
/// Returns true if game scripts assembly has been loaded.
/// </summary>
/// <returns>True if game scripts assembly is loaded, otherwise false.</returns>
[LibraryImport("FlaxEngine", EntryPoint = "ScriptingInternal_HasGameModulesLoaded", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))]
[return: MarshalAs(UnmanagedType.U1)]
public static partial bool HasGameModulesLoaded();
/// <summary>
/// Returns true if given type is from one of the game scripts assemblies.
/// </summary>
/// <returns>True if the type is from game assembly, otherwise false.</returns>
[LibraryImport("FlaxEngine", EntryPoint = "ScriptingInternal_IsTypeFromGameScripts", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))]
[return: MarshalAs(UnmanagedType.U1)]
public static partial bool IsTypeFromGameScripts([MarshalUsing(typeof(SystemTypeMarshaller))] Type type);
/// <summary>
/// Flushes the removed objects (disposed objects using Object.Destroy).
/// </summary>
[LibraryImport("FlaxEngine", EntryPoint = "ScriptingInternal_FlushRemovedObjects", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))]
public static partial void FlushRemovedObjects();
}
/// <summary>
/// Provides C# scripts profiling methods.
/// </summary>
/// <remarks>
/// Profiler is available in the editor and Debug/Development builds. Release builds don't have profiling tools.
/// </remarks>
public static partial class Profiler
{
/// <summary>
/// Begins profiling a piece of code with a custom label.
/// </summary>
/// <param name="name">The name of the event.</param>
[LibraryImport("FlaxEngine", EntryPoint = "ProfilerInternal_BeginEvent", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))]
public static partial void BeginEvent(string name);
/// <summary>
/// Ends profiling an event.
/// </summary>
[LibraryImport("FlaxEngine", EntryPoint = "ProfilerInternal_EndEvent", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))]
public static partial void EndEvent();
/// <summary>
/// Begins GPU profiling a piece of code with a custom label.
/// </summary>
/// <param name="name">The name of the event.</param>
[LibraryImport("FlaxEngine", EntryPoint = "ProfilerInternal_BeginEventGPU", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))]
public static partial void BeginEventGPU(string name);
/// <summary>
/// Ends GPU profiling an event.
/// </summary>
[LibraryImport("FlaxEngine", EntryPoint = "ProfilerInternal_EndEventGPU", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))]
public static partial void EndEventGPU();
}
}