diff --git a/.vscode/launch.json b/.vscode/launch.json index c326e070..7b7d88d4 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,6 +1,22 @@ { "version": "0.2.0", "configurations": [ + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/src/UI/bin/Debug/net8.0/Volte.UI.dll", + "args": [], + "cwd": "${workspaceFolder}/src/UI/bin/Debug/net8.0", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "integratedTerminal", + "stopAtEntry": false + }, { // Use IntelliSense to find out which attributes exist for C# debugging // Use hover for the description of the existing attributes @@ -11,7 +27,7 @@ "preLaunchTask": "build", // If you have changed target frameworks, make sure to update the program path. "program": "${workspaceFolder}/src/Bot/bin/Debug/net8.0/Volte.dll", - "args": ["--ui"], + "args": [], "cwd": "${workspaceFolder}/src/Bot/bin/Debug/net8.0", // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console "console": "integratedTerminal", diff --git a/Volte.sln b/Volte.sln index fc3ce409..a6d1b68f 100644 --- a/Volte.sln +++ b/Volte.sln @@ -5,7 +5,7 @@ VisualStudioVersion = 16.0.28721.148 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volte", "src\Bot\Volte.csproj", "{5D4A85B0-1326-4CA2-A26C-D646D9579342}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volte.UI", "src\UI\Volte.UI.csproj", "{70624B23-1B94-4801-9536-DD4FE86E6671}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volte.UI", "src\UI\Volte.UI.csproj", "{BCD42127-89FE-44F3-AD2F-D224ED94B03A}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -23,14 +23,14 @@ Global {5D4A85B0-1326-4CA2-A26C-D646D9579342}.Release|Any CPU.Build.0 = Release|Any CPU {5D4A85B0-1326-4CA2-A26C-D646D9579342}.Release|x64.ActiveCfg = Release|x64 {5D4A85B0-1326-4CA2-A26C-D646D9579342}.Release|x64.Build.0 = Release|x64 - {70624B23-1B94-4801-9536-DD4FE86E6671}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {70624B23-1B94-4801-9536-DD4FE86E6671}.Debug|Any CPU.Build.0 = Debug|Any CPU - {70624B23-1B94-4801-9536-DD4FE86E6671}.Debug|x64.ActiveCfg = Debug|Any CPU - {70624B23-1B94-4801-9536-DD4FE86E6671}.Debug|x64.Build.0 = Debug|Any CPU - {70624B23-1B94-4801-9536-DD4FE86E6671}.Release|Any CPU.ActiveCfg = Release|Any CPU - {70624B23-1B94-4801-9536-DD4FE86E6671}.Release|Any CPU.Build.0 = Release|Any CPU - {70624B23-1B94-4801-9536-DD4FE86E6671}.Release|x64.ActiveCfg = Release|Any CPU - {70624B23-1B94-4801-9536-DD4FE86E6671}.Release|x64.Build.0 = Release|Any CPU + {BCD42127-89FE-44F3-AD2F-D224ED94B03A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BCD42127-89FE-44F3-AD2F-D224ED94B03A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BCD42127-89FE-44F3-AD2F-D224ED94B03A}.Debug|x64.ActiveCfg = Debug|Any CPU + {BCD42127-89FE-44F3-AD2F-D224ED94B03A}.Debug|x64.Build.0 = Debug|Any CPU + {BCD42127-89FE-44F3-AD2F-D224ED94B03A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BCD42127-89FE-44F3-AD2F-D224ED94B03A}.Release|Any CPU.Build.0 = Release|Any CPU + {BCD42127-89FE-44F3-AD2F-D224ED94B03A}.Release|x64.ActiveCfg = Release|Any CPU + {BCD42127-89FE-44F3-AD2F-D224ED94B03A}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Bot/Commands/Modules/BotOwner/UICommand.cs b/src/Bot/Commands/Modules/BotOwner/UICommand.cs deleted file mode 100644 index 2a390578..00000000 --- a/src/Bot/Commands/Modules/BotOwner/UICommand.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Volte.UI; - -namespace Volte.Commands.Text.Modules; - -public sealed partial class BotOwnerModule -{ - [Command("CreateUi", "Cui")] - [Description("Create the ImGui UI on the machine running Volte.")] - public Task UiAsync( - [Description("Desired font size of the UI.")] int fontSize = 17) - { - var uiParams = VolteBot.GetUiParams(fontSize); - if (!UiManager.TryCreateUi(uiParams, out var err)) - return BadRequest($"Could not create UI thread: {err?.Message}"); - - UiManager.AddView(new VolteUiView()); - UiManager.StartThread("Volte UI Thread"); - return None(() => Context.Message.AddReactionAsync(Emojis.BallotBoxWithCheck)); - - } -} \ No newline at end of file diff --git a/src/Bot/Core/Entities/EventArgs/VolteLogEventArgs.cs b/src/Bot/Core/Entities/EventArgs/VolteLogEventArgs.cs new file mode 100644 index 00000000..a176a892 --- /dev/null +++ b/src/Bot/Core/Entities/EventArgs/VolteLogEventArgs.cs @@ -0,0 +1,12 @@ +namespace Volte.Entities; + +#nullable enable + +public class VolteLogEventArgs +{ + public required LogSeverity Severity; + public required LogSource Source; + public required string Message; + public required string[] PrintedLines; + public required Exception? Error; +} \ No newline at end of file diff --git a/src/Bot/Core/VolteBot.cs b/src/Bot/Core/VolteBot.cs index e8cd72c6..bec2ed0c 100644 --- a/src/Bot/Core/VolteBot.cs +++ b/src/Bot/Core/VolteBot.cs @@ -1,13 +1,4 @@ -using System.IO; -using ImGuiNET; -using Silk.NET.Maths; -using Silk.NET.OpenGL.Extensions.ImGui; -using Silk.NET.Windowing; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; using Volte.Commands.Text.Modules; -using Volte.UI; -using Image = SixLabors.ImageSharp.Image; namespace Volte; @@ -15,7 +6,7 @@ public class VolteBot { public static Task StartAsync() { - Console.Title = DefaultWindowOptions.Title; + Console.Title = $"Volte {Version.InformationVersion}"; Console.CursorVisible = false; return new VolteBot().LoginAsync(); } @@ -39,9 +30,6 @@ public class VolteBot ServiceProvider = new ServiceCollection().AddAllServices().BuildServiceProvider(); - if (Program.CommandLineArguments.TryGetValue("ui", out var sizeStr)) - CreateUi(sizeStr); - _client = ServiceProvider.Get(); _cts = ServiceProvider.Get(); @@ -96,64 +84,4 @@ public class VolteBot Environment.Exit(0); } - - // WindowOptions.Default with custom title and larger base window - public static readonly WindowOptions DefaultWindowOptions = new( - isVisible: true, - position: new Vector2D(50, 50), - size: new Vector2D(1600, 900), - framesPerSecond: 0, - updatesPerSecond: 0.0, - api: GraphicsAPI.Default, - title: $"Volte {Version.InformationVersion}", - windowState: WindowState.Normal, - windowBorder: WindowBorder.Resizable, - isVSync: true, - shouldSwapAutomatically: true, - videoMode: VideoMode.Default - ); - - private static void CreateUi(string sizeStr) - { - var uiParams = GetUiParams(sizeStr.TryParse(out var fsz) ? fsz : 17); - - if (UiManager.TryCreateUi(uiParams, out var uiStartError)) - { - UiManager.AddView(new VolteUiView()); - UiManager.StartThread("Volte UI Thread"); - } - else Error(LogSource.UI, $"Could not create UI: {uiStartError!.Message}"); - } - - private static readonly string[] UiFontResourceKeys = [ "Regular", "Bold", "BoldItalic", "Italic" ]; - - public static UiManager.CreateParams GetUiParams(int fontSize) - { - unsafe - { - return new UiManager.CreateParams - { - WindowIcon = loadIcon(), - WOptions = DefaultWindowOptions, - Theme = Spectrum.Dark, - OnConfigureIo = _ => - { - UiFontResourceKeys.ForEach(key => - { - using var embeddedFont = Assembly.GetExecutingAssembly().GetManifestResourceStream(key); - if (embeddedFont != null) - UiManager.LoadFontFromStream(embeddedFont, key, fontSize); - }); - } - }; - } - - Image loadIcon() - { - using var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("VolteIcon"); - return iconStream == null - ? null - : Image.Load(iconStream); - } - } } \ No newline at end of file diff --git a/src/Bot/Helpers/Event.cs b/src/Bot/Helpers/Event.cs new file mode 100644 index 00000000..52c01a3e --- /dev/null +++ b/src/Bot/Helpers/Event.cs @@ -0,0 +1,68 @@ +using System.Collections.Immutable; + +namespace Volte.Helpers; + +internal class Event + where T : class +{ + private readonly object _subLock = new(); + private ImmutableArray _subscriptions = []; + + public bool HasSubscribers + { + get + { + lock (_subLock) + return _subscriptions.Length != 0; + } + } + + public IReadOnlyList Subscriptions + { + get + { + lock (_subLock) + return _subscriptions; + } + } + + public void Add(T subscriber) + { + Guard.Require(subscriber, nameof(subscriber)); + lock (_subLock) + _subscriptions = _subscriptions.Add(subscriber); + } + + public void Remove(T subscriber) + { + Guard.Require(subscriber, nameof(subscriber)); + lock (_subLock) + _subscriptions = _subscriptions.Remove(subscriber); + } +} + +internal static class EventExtensions +{ + public static void Call(this Event eventHandler) + => eventHandler.Subscriptions.ForEach(x => x()); + + public static void Call(this Event> eventHandler, + T arg + ) => eventHandler.Subscriptions.ForEach(x => x(arg)); + + public static void Call(this Event> eventHandler, + T1 arg1, T2 arg2 + ) => eventHandler.Subscriptions.ForEach(x => x(arg1, arg2)); + + public static void Call(this Event> eventHandler, + T1 arg1, T2 arg2, T3 arg3 + ) => eventHandler.Subscriptions.ForEach(x => x(arg1, arg2, arg3)); + + public static void Call(this Event> eventHandler, + T1 arg1, T2 arg2, T3 arg3, T4 arg4 + ) => eventHandler.Subscriptions.ForEach(x => x(arg1, arg2, arg3, arg4)); + + public static void Call(this Event> eventHandler, + T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5 + ) => eventHandler.Subscriptions.ForEach(x => x(arg1, arg2, arg3, arg4, arg5)); +} \ No newline at end of file diff --git a/src/Bot/Helpers/Logger.Internal.cs b/src/Bot/Helpers/Logger.Internal.cs index a56fd3dc..6978add4 100644 --- a/src/Bot/Helpers/Logger.Internal.cs +++ b/src/Bot/Helpers/Logger.Internal.cs @@ -55,7 +55,7 @@ public static partial class Logger { if (_logFileNoticePrinted || !(Config.EnabledFeatures?.LogToFile ?? false)) return; - GetRelevantLogPath().AppendAllText($"{Side}RESTARTING{Side}\n"); + GetLogFilePath(DateTime.Now).AppendAllText($"{Side}RESTARTING{Side}\n"); _logFileNoticePrinted = true; } @@ -113,10 +113,14 @@ public static partial class Logger if (e != null) { e.SentryCapture(scope => - scope.AddBreadcrumb("This exception might not have been thrown, and may not be important; it is merely being logged.")); - Append(Environment.NewLine + (e.Message.IsNullOrEmpty() ? "No message provided" : e.Message) + - Environment.NewLine + e.StackTrace, - Color.IndianRed, ref content); + scope.AddBreadcrumb("This exception might not have been thrown, and may not be important; it is merely being logged.") + ); + + Append(errorString(), Color.IndianRed, ref content); + + string errorString() + => Environment.NewLine + (e.Message.IsNullOrEmpty() ? "No message provided" : e.Message) + + Environment.NewLine + e.StackTrace; } if (Environment.NewLine != content[^1].ToString()) @@ -126,12 +130,22 @@ public static partial class Logger } if (Config.EnabledFeatures?.LogToFile ?? false) - GetRelevantLogPath().AppendAllText(content.ToString().TrimEnd('\n').Append("\n")); + GetLogFilePath(DateTime.Now).AppendAllText(content.ToString().TrimEnd('\n').Append("\n")); + + if (!_logEventHandler.HasSubscribers) return; + + _logEventHandler.Call(new VolteLogEventArgs + { + Severity = s, + Source = src, + Message = message, + PrintedLines = content.ToString().TrimEnd('\n').Split('\n', StringSplitOptions.RemoveEmptyEntries), + Error = e + }); } - private static FilePath GetLogFilePath(DateTime date) => new FilePath("logs") / string.Intern($"{date.Month}-{date.Day}-{date.Year}.log"); - - private static FilePath GetRelevantLogPath() => GetLogFilePath(DateTime.Now); + private static FilePath GetLogFilePath(DateTime date) + => new FilePath("logs") / string.Intern($"{date.Year}-{date.Month}-{date.Day}.log"); private static void Append(string m, Color c) { diff --git a/src/Bot/Helpers/Logger.cs b/src/Bot/Helpers/Logger.cs index 8b88fd51..3a119f40 100644 --- a/src/Bot/Helpers/Logger.cs +++ b/src/Bot/Helpers/Logger.cs @@ -1,12 +1,18 @@ using System.IO; using System.Runtime.CompilerServices; -using Color = System.Drawing.Color; -using Optional = Gommon.Optional; namespace Volte.Helpers; public static partial class Logger { + public static event Action LogEvent + { + add => _logEventHandler.Add(value); + remove => _logEventHandler.Remove(value); + } + + private static readonly Event> _logEventHandler = new(); + public static bool IsDebugLoggingEnabled => Config.DebugEnabled || Version.IsDevelopment; public static void HandleLogEvent(LogEventArgs args) => @@ -71,8 +77,9 @@ public static partial class Logger /// This method calls 's CaptureException, so it is logged to Sentry. /// /// Exception to print. - public static void Error(Exception e, InvocationInfo caller) - => Execute(LogSeverity.Error, LogSource.Volte, string.Empty, e, caller); + /// Source to print the message from. + public static void Error(Exception e, InvocationInfo caller, LogSource src = LogSource.Volte) + => Execute(LogSeverity.Error, src, string.Empty, e, caller); #endregion diff --git a/src/Bot/Interactive/Paginator/ButtonPaginatorCallback.cs b/src/Bot/Interactive/Paginator/ButtonPaginatorCallback.cs index 5defd5df..fab46d38 100644 --- a/src/Bot/Interactive/Paginator/ButtonPaginatorCallback.cs +++ b/src/Bot/Interactive/Paginator/ButtonPaginatorCallback.cs @@ -1,5 +1,4 @@ using Discord.Interactions; -using Silk.NET.Input; using Volte.Interactions; using RunMode = Qmmands.RunMode; diff --git a/src/Bot/Program.cs b/src/Bot/Program.cs index 182fda7b..0600ceef 100644 --- a/src/Bot/Program.cs +++ b/src/Bot/Program.cs @@ -12,7 +12,12 @@ public static class Program if (output.Error is not InvalidOperationException) Error(output.Error); - CommandLineArguments = new ReadOnlyDictionary(output.Parsed ?? new Dictionary()); + await Main(output.Parsed); + } + + private static async Task Main(Dictionary args) + { + CommandLineArguments = new ReadOnlyDictionary(args ?? new Dictionary()); await VolteBot.StartAsync(); } } \ No newline at end of file diff --git a/src/Bot/UI/VolteView.Panels.cs b/src/Bot/UI/VolteView.Panels.cs deleted file mode 100644 index 2dfa986b..00000000 --- a/src/Bot/UI/VolteView.Panels.cs +++ /dev/null @@ -1,145 +0,0 @@ -using System.Collections.Immutable; -// needed for commented code //using System.Numerics; -using ImGuiNET; -using Silk.NET.Input; -using Color = System.Drawing.Color; - -namespace Volte.UI; - -public partial class VolteUiView -{ - private void CommandStats(double _) - { - //using var __ = PushStyle(ImGuiStyleVar.WindowMinSize, new Vector2(201, 188)); - - ImGui.SeparatorText("Total executions"); - Gui.SameLineText($"Successful: {_state.Messages.AllTimeSuccessfulCommandCalls}", Color.LawnGreen); - Gui.SameLineText("+"); - Gui.SameLineText($"Failed: {_state.Messages.AllTimeFailedCommandCalls}", Color.OrangeRed); - Gui.SameLineText("="); - ImGui.Text($"Total: {_state.Messages.AllTimeCommandCalls}"); - - ImGui.SeparatorText("This Session"); - Gui.SameLineText($"Successful: {CalledCommandsInfo.ThisSessionSuccess + _state.Messages.UnsavedSuccessfulCommandCalls}", Color.LawnGreen); - Gui.SameLineText("+"); - Gui.SameLineText($"Failed: {CalledCommandsInfo.ThisSessionFailed + _state.Messages.UnsavedFailedCommandCalls}", Color.OrangeRed); - Gui.SameLineText("="); - ImGui.Text($"Total: { - CalledCommandsInfo.ThisSessionSuccess + CalledCommandsInfo.ThisSessionFailed + - _state.Messages.UnsavedFailedCommandCalls + _state.Messages.UnsavedSuccessfulCommandCalls - }"); - } - - private void BotManagement(double _) - { - //using var __ = PushStyle(ImGuiStyleVar.WindowMinSize, new Vector2(385, 299)); - Gui.SameLineText("Discord Gateway:"); - // ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault - // default is a meaningless case here i dont fucking care rider - switch (_state.Client.ConnectionState) - { - case ConnectionState.Connected: - ColoredText("Connected", Color.LawnGreen); - break; - case ConnectionState.Connecting: - ColoredText("Connecting...", Color.Yellow); - break; - case ConnectionState.Disconnecting: - ColoredText("Disconnecting...", Color.OrangeRed); - break; - case ConnectionState.Disconnected: - ColoredText("Disconnected!", Color.Red); - break; - } - - if (_state.Client.ConnectionState == ConnectionState.Connected) - { - ImGui.Text($"Connected as: {_state.Client.CurrentUser.Username}#{_state.Client.CurrentUser.DiscriminatorValue}"); - // ToString()ing the CurrentUser has weird question marks on both sides of Volte-dev's name, - // so we do it manually in case that happens on other bot accounts too - - var currentStatus = _state.Client.Status; - - if (ImGui.BeginMenu($"Bot status: {currentStatus}")) - { - if (ImGui.MenuItem("Online", currentStatus != UserStatus.Online)) - Await(_state.Client.SetStatusAsync(UserStatus.Online)); - if (ImGui.MenuItem("Idle", currentStatus != UserStatus.Idle)) - Await(_state.Client.SetStatusAsync(UserStatus.Idle)); - if (ImGui.MenuItem("Do Not Disturb", currentStatus != UserStatus.DoNotDisturb)) - Await(_state.Client.SetStatusAsync(UserStatus.DoNotDisturb)); - if (ImGui.MenuItem("Invisible", currentStatus != UserStatus.Invisible)) - Await(_state.Client.SetStatusAsync(UserStatus.Invisible)); - - ImGui.EndMenu(); - } - } - - var process = Process.GetCurrentProcess(); - ImGui.Text($"Process memory: {process.GetMemoryUsage()} ({process.GetMemoryUsage(MemoryType.Kilobytes)})"); - - if (ImGui.Button("Reload Config")) - Config.Reload(); - } - - #region Guild Manager - - private void GuildManager(double _) - { - //using var __ = PushStyle(ImGuiStyleVar.WindowMinSize, new Vector2(418, 300)); - - if (_state.SelectedGuildId != 0) - { - var selectedGuild = _state.Client.GetGuild(_state.SelectedGuildId); - var selectedGuildMembers = selectedGuild.Users.ToImmutableArray(); - var botMembers = selectedGuildMembers.Count(sgu => sgu.IsBot); - - ImGui.SeparatorText(selectedGuild.Name); - ImGui.Text($"Owner: @{selectedGuild.Owner}"); - ImGui.Text($"Text Channels: {selectedGuild.TextChannels.Count}"); - ImGui.Text($"Voice Channels: {selectedGuild.VoiceChannels.Count}"); - - Gui.SameLineText($"{selectedGuildMembers.Length} members |"); - Gui.SameLineText($"{selectedGuildMembers.Length - botMembers} users", Color.LawnGreen); - Gui.SameLineText("|"); - ColoredText($"{botMembers} bots", Color.OrangeRed); - - ImGui.Separator(); - - var destructiveMenuEnabled = AllKeysPressed(Key.ShiftLeft, Key.ControlLeft); - - if (ImGui.BeginMenu("Destructive Actions (Shift + Ctrl)", destructiveMenuEnabled)) - { - if (ImGui.MenuItem("Leave Guild", destructiveMenuEnabled)) - { - Await(selectedGuild.LeaveAsync()); - _state.SelectedGuildId = 0; //resets this pane back to just the "select a guild" button - } - - if (ImGui.MenuItem("Reset Configuration", destructiveMenuEnabled)) - _state.Database.Save(GuildData.CreateFrom(selectedGuild)); - - ImGui.EndMenu(); - } - - ImGui.Separator(); - } - - GuildSelect(); - } - - private void GuildSelect() - { - if (!ImGui.BeginMenu("Select a Guild")) return; - - _state.Client.Guilds.ForEach(guild => - { - if (ImGui.MenuItem(guild.Name, guild.Id != _state.SelectedGuildId)) - _state.SelectedGuildId = guild.Id; - }); - - ImGui.EndMenu(); - } - - #endregion Guild Manager -} \ No newline at end of file diff --git a/src/Bot/UI/VolteView.cs b/src/Bot/UI/VolteView.cs deleted file mode 100644 index 5d096ff9..00000000 --- a/src/Bot/UI/VolteView.cs +++ /dev/null @@ -1,85 +0,0 @@ -using ImGuiNET; -using Color = System.Drawing.Color; -// ReSharper disable InvertIf - -namespace Volte.UI; - -public sealed class VolteUiState -{ - public bool SelectedTheme = true; - public bool ShowStyleEditor; - - public VolteUiState(IServiceProvider provider) - { - Cts = provider.Get(); - Client = provider.Get(); - Messages = provider.Get(); - Database = provider.Get(); - } - - public readonly CancellationTokenSource Cts; - public readonly DiscordSocketClient Client; - public readonly MessageService Messages; - public readonly DatabaseService Database; - - public ulong SelectedGuildId = 0; -} - -public partial class VolteUiView : UiView -{ - private readonly VolteUiState _state; - - public VolteUiView() - { - _state = new VolteUiState(VolteBot.ServiceProvider); - - MainMenuBar = MenuBar; - - Panel("Command Stats", CommandStats); - Panel("Bot Management", BotManagement); - Panel("Guild Manager", GuildManager); - Panel(_ => - { - if (_state.ShowStyleEditor) - { - ImGui.Begin("Style Editor"); - ImGui.ShowStyleEditor(ImGui.GetStyle()); - ImGui.End(); - } - }); - } - - private void MenuBar(double delta) - { - if (ImGui.BeginMenu("File")) - { - if (ImGui.Button("Shutdown")) - _state.Cts.Cancel(); - - ImGui.EndMenu(); - } - - if (ImGui.BeginMenu("Theming")) - { - if (ImGui.MenuItem(_state.SelectedTheme ? "Swap to Light" : "Swap to Dark")) - { - _state.SelectedTheme = !_state.SelectedTheme; - unsafe - { - UiManager.SetColors(_state.SelectedTheme ? Spectrum.Dark : Spectrum.Light); - } - - } - - if (ImGui.RadioButton("Show Style Editor", _state.ShowStyleEditor)) - _state.ShowStyleEditor = !_state.ShowStyleEditor; - - ImGui.EndMenu(); - } - - ImGui.BeginMenu($"{Io.Framerate:###} FPS", false); - } - - private static void ColoredText(string fmt, Color color) => - ImGui.TextColored(color.AsVec4(), fmt); -} \ No newline at end of file diff --git a/src/Bot/Volte.csproj b/src/Bot/Volte.csproj index 5d2c30f0..2a837d31 100644 --- a/src/Bot/Volte.csproj +++ b/src/Bot/Volte.csproj @@ -31,10 +31,10 @@ - + @@ -43,9 +43,6 @@ - - - diff --git a/src/UI/Avalonia/App.axaml b/src/UI/Avalonia/App.axaml new file mode 100644 index 00000000..8c38b2a1 --- /dev/null +++ b/src/UI/Avalonia/App.axaml @@ -0,0 +1,26 @@ + + + + + + #FFACACAC + Transparent + #FFF2F2F2 + #FFE6E6E6 + + + #FF737373 + Transparent + #FF2B2B2B + #FF1F1F1F + + + + + + + + \ No newline at end of file diff --git a/src/UI/Avalonia/App.axaml.cs b/src/UI/Avalonia/App.axaml.cs new file mode 100644 index 00000000..749f3c37 --- /dev/null +++ b/src/UI/Avalonia/App.axaml.cs @@ -0,0 +1,23 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; + +namespace Volte.UI; + +public class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow = new UIShellView(); + } + + base.OnFrameworkInitializationCompleted(); + } +} \ No newline at end of file diff --git a/src/UI/Avalonia/UIShellView.axaml b/src/UI/Avalonia/UIShellView.axaml new file mode 100644 index 00000000..8dafc5dd --- /dev/null +++ b/src/UI/Avalonia/UIShellView.axaml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/UI/Avalonia/UIShellView.axaml.cs b/src/UI/Avalonia/UIShellView.axaml.cs new file mode 100644 index 00000000..e6eee063 --- /dev/null +++ b/src/UI/Avalonia/UIShellView.axaml.cs @@ -0,0 +1,37 @@ +using Avalonia; +using Avalonia.Input; +using Avalonia.Markup.Xaml; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using FluentAvalonia.UI.Windowing; +using Gommon; + +namespace Volte.UI; + +public partial class UIShellView : AppWindow +{ + public UIShellView() + { + InitializeComponent(); + TitleBar.ExtendsContentIntoTitleBar = true; + TitleBar.TitleBarHitTestType = TitleBarHitTestType.Complex; + + using var bitmap = new Bitmap(AssetLoader.Open(new Uri("avares://Volte.UI/Assets/icon.ico"))); + Icon = bitmap.CreateScaledBitmap(new PixelSize(48, 48)); + + DataContext = new UIShellViewModel + { + OpenDevTools = new KeyGesture(Key.F4, KeyModifiers.Control), + Icon = Icon + }; + +#if DEBUG + this.AttachDevTools(DataContext.Cast().OpenDevTools); +#endif + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} \ No newline at end of file diff --git a/src/UI/Avalonia/ViewModels/UIShellViewModel.cs b/src/UI/Avalonia/ViewModels/UIShellViewModel.cs new file mode 100644 index 00000000..b344bfb0 --- /dev/null +++ b/src/UI/Avalonia/ViewModels/UIShellViewModel.cs @@ -0,0 +1,20 @@ +using Avalonia.Input; +using Avalonia.Media; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace Volte.UI; + +public partial class UIShellViewModel : ObservableObject +{ + public KeyGesture OpenDevTools { get; init; } + + public IImage Icon { get; init; } + + [ObservableProperty] + private string _title = "Volte"; + + public UIShellViewModel() + { + + } +} \ No newline at end of file diff --git a/src/UI/Extensions.cs b/src/UI/Extensions.cs deleted file mode 100644 index b0eda667..00000000 --- a/src/UI/Extensions.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.IO; -using System.Numerics; -using System.Text; -using Gommon; -using Silk.NET.SDL; - -namespace Volte.UI; - -public static class Extensions -{ - #region Color - public static System.Drawing.Color AsColor(this Vector3 vec3) - => System.Drawing.Color.FromArgb(255, - (int)(vec3.X.CoerceAtMost(1) * 255), - (int)(vec3.Y.CoerceAtMost(1) * 255), - (int)(vec3.Z.CoerceAtMost(1) * 255)); - - public static Vector3 AsVec3(this System.Drawing.Color color) - => new(color.R / 255f, color.G / 255f, color.B / 255f); - - public static Vector4 AsVec4(this System.Drawing.Color color) - => new(color.R / 255f, color.G / 255f, color.B / 255f, color.A / 255f); - - public static Vector3 AsVec3(this Color color) - => new(color.R / 255f, color.G / 255f, color.B / 255f); - - public static Vector4 AsVec4(this Color color) - => new(color.R / 255f, color.G / 255f, color.B / 255f, color.A / 255f); - - #endregion Color -} - -public static class Buffers -{ - public static void CopyBytesTo(this string str, Span span, Encoding encoding = null) - { - encoding ??= Encoding.UTF8; - var bytes = encoding.GetBytes(str); - - bytes.CopyTo(span); - span[bytes.Length] = 0; //null terminator - } - - public static unsafe void CopyBytesTo(this string str, byte* buffer, int bufferSize) - => CopyBytesTo(str, new Span(buffer, bufferSize)); - - /** - * Reads the into a of bytes. - * Will throw by default if the read byte count is less than the 's length. - * The stream to read from. - * Whether to throw if the read byte count is less than the stream's length. - * Whether to seek to the beginning of the stream before reading. - */ - public static Span ToSpan(this Stream stream, bool throwOnUnderRead = true, bool fromStart = true) - { - if (fromStart) - stream.Seek(0, SeekOrigin.Begin); - - var data = new byte[stream.Length]; - - // no using is a deliberate choice here; the user is responsible for disposing the stream, - // plus that might not be a desired side effect of calling this. - var read = new BinaryReader(stream).Read(data, 0, data.Length); - if (throwOnUnderRead && read != data.Length) - throw new InvalidDataException("Could not read all bytes from the stream."); - - return data; - } - - public static unsafe void CopyBytesFromString(byte* buffer, int bufferSize, string str) - => str.CopyBytesTo(buffer, bufferSize); -} \ No newline at end of file diff --git a/src/UI/Helpers/PageManager.cs b/src/UI/Helpers/PageManager.cs new file mode 100644 index 00000000..a9c19b09 --- /dev/null +++ b/src/UI/Helpers/PageManager.cs @@ -0,0 +1,73 @@ +using System.Collections.ObjectModel; +using Avalonia.Controls; +using CommunityToolkit.Mvvm.ComponentModel; +using FluentAvalonia.UI.Controls; + +namespace Volte.UI.Helpers; + +public partial class PageManager : ObservableObject +{ + // ReSharper disable once InconsistentNaming + private static readonly Lazy _shared = new(() => new PageManager()); + public static PageManager Shared => _shared.Value; + + [ObservableProperty] + private PageData? _current = null; + + private readonly Dictionary _lookup = []; + public ObservableCollection Pages { get; } = []; + public ObservableCollection FooterPages { get; } = []; + + public PageData this[Page page] { + get { + var (index, isFooter) = _lookup[page]; + return (isFooter ? FooterPages : Pages)[index]; + } + } + + public void Register(Page page, string title, object? content, Symbol icon, string? description = null, bool isDefault = false, bool isFooter = false) + { + var source = isFooter ? FooterPages : Pages; + _lookup[page] = (source.Count, isFooter); + + source.Add(new PageData { + Title = title, + Content = content, + Description = description, + Icon = icon + }); + + if (isDefault) { + Focus(page); + } + } + + public void Focus(Page page) + { + Current = this[page]; + } + + public T Get(Page page) where T : ObservableObject + { + var (index, isFooter) = _lookup[page]; + if ((isFooter ? FooterPages : Pages)[index].Content is UserControl { DataContext: T value }) { + return value; + } + + throw new InvalidOperationException( + $"Invalid ViewModel type for '{page}'"); + } +} + +public enum Page +{ + Home +} + +public class PageData +{ + public required string Title { get; set; } + public object? Content { get; set; } + public required Symbol Icon { get; set; } + public string? Description { get; set; } +} \ No newline at end of file diff --git a/src/UI/ImGuiExt.cs b/src/UI/ImGuiExt.cs deleted file mode 100644 index 4ecf685d..00000000 --- a/src/UI/ImGuiExt.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using System.Numerics; -using ImGuiNET; -using Silk.NET.SDL; - -namespace Volte.UI; - -/** - * A class that contains all helper extension methods for ImGui. - * Named Gui instead of ImGui to avoid conflicts. - */ -public static class Gui -{ - public static void SameLineText(ReadOnlySpan text) - { - ImGui.Text(text); - ImGui.SameLine(); - } - - public static void SameLineText(ReadOnlySpan text, Color color) - { - ImGui.TextColored(color.AsVec4(), text); - ImGui.SameLine(); - } - - public static void SameLineText(ReadOnlySpan text, System.Drawing.Color color) - { - ImGui.TextColored(color.AsVec4(), text); - ImGui.SameLine(); - } - - public static void Text(ReadOnlySpan text, Color color) - => ImGui.TextColored(color.AsVec4(), text); - - public static void Text(ReadOnlySpan text, System.Drawing.Color color) - => ImGui.TextColored(color.AsVec4(), text); - - public static IDisposable PushValue(this ImGuiStyleVar styleVar, Vector2 value) => new ScopedStyleVar(styleVar, value); - public static IDisposable PushValue(this ImGuiStyleVar styleVar, float value) => new ScopedStyleVar(styleVar, value); - - public static IDisposable PushValue(this ImGuiCol colorVar, Vector4 value) => new ScopedStyleColor(colorVar, value); - public static IDisposable PushValue(this ImGuiCol colorVar, Vector3 value) => new ScopedStyleColor(colorVar, value); - public static IDisposable PushValue(this ImGuiCol colorVar, Color value) => new ScopedStyleColor(colorVar, value.AsVec4()); - - public static IDisposable PushValue(this ImGuiCol colorVar, System.Drawing.Color value) => - new ScopedStyleColor(colorVar, value.AsVec4()); - - public static IDisposable PushValue(this ImGuiCol colorVar, uint value) => new ScopedStyleColor(colorVar, value); -} - -public struct ScopedStyleVar : IDisposable -{ - internal ScopedStyleVar(ImGuiStyleVar styleVar, Vector2 value) - => ImGui.PushStyleVar(styleVar, value); - - internal ScopedStyleVar(ImGuiStyleVar styleVar, float value) - => ImGui.PushStyleVar(styleVar, value); - - void IDisposable.Dispose() => ImGui.PopStyleVar(); -} - -public struct ScopedStyleColor : IDisposable -{ - internal ScopedStyleColor(ImGuiCol colorVar, Vector4 value) - => ImGui.PushStyleColor(colorVar, value); - - internal ScopedStyleColor(ImGuiCol colorVar, Vector3 value) - => ImGui.PushStyleColor(colorVar, new Vector4(value, 1f)); - - internal ScopedStyleColor(ImGuiCol colorVar, uint value) => - ImGui.PushStyleColor(colorVar, value); - - void IDisposable.Dispose() => ImGui.PopStyleColor(); -} \ No newline at end of file diff --git a/src/UI/Manager.Init.cs b/src/UI/Manager.Init.cs deleted file mode 100644 index 93c49b54..00000000 --- a/src/UI/Manager.Init.cs +++ /dev/null @@ -1,152 +0,0 @@ -using System; -using System.Linq; -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using Gommon; -using ImGuiNET; -using Silk.NET.Core; -using Silk.NET.Input; -using Silk.NET.OpenGL; -using Silk.NET.OpenGL.Extensions.ImGui; -using Silk.NET.Windowing; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.PixelFormats; -using Color = Silk.NET.SDL.Color; - -namespace Volte.UI; - -#nullable enable - -public sealed partial class UiManager : IDisposable -{ - private bool _isActive; - - private readonly IWindow _window; - private readonly Action _onConfigureIo; - private Image? _windowIcon; - - private ImGuiController? _controller; - private GL? _gl; - private IInputContext? _inputContext; - - private void OnWindowLoad() - { - _gl = GL.GetApi(_window); - _inputContext = _window.CreateInput(); - _controller = new ImGuiController(_gl, _window, _inputContext, onConfigureIO: () => - { - var io = ImGui.GetIO(); - io.ConfigFlags |= ImGuiConfigFlags.DockingEnable; - io.ConfigDockingWithShift = false; - - unsafe - { - if (_theme != null) - SetColors(_theme); - else - ImGui.StyleColorsDark(); - } - - _onConfigureIo(io); - } - ); - - SetWindowIcon(); - } - - private void SetWindowIcon() - { - // shoutout https://github.com/dotnet/Silk.NET/blob/b079b28cd51ce447183cfedde0a85412b9b226ee/src/Lab/Experiments/BlankWindow/Program.cs#L82-L95 - - if (_windowIcon == null) return; - - Span span = new byte[_windowIcon.GetPixelMemoryGroup().TotalLength * Unsafe.SizeOf()]; - var block = MemoryMarshal.Cast(span); - - foreach (var memory in _windowIcon.GetPixelMemoryGroup()) - { - memory.Span.CopyTo(block); - block = block[memory.Length..]; - } - - var rawIcon = new RawImage(_windowIcon.Width, _windowIcon.Height, span.ToArray()); - - _windowIcon.Dispose(); - _windowIcon = null; - - _window.SetWindowIcon(ref rawIcon); - } - - public static unsafe void SetColors(ThemedColors* theme) - { - var style = ImGui.GetStyle(); - - style.GrabRounding = 4f; - style.FrameRounding = 6f; - style.WindowMenuButtonPosition = ImGuiDir.None; - style.FrameBorderSize = 1f; - style.TabBorderSize = 1f; - style.WindowTitleAlign = new Vector2(0.5f); - style.SeparatorTextBorderSize = 9f; - - set(ImGuiCol.Text, theme->Gray800); - set(ImGuiCol.TextDisabled, theme->Gray500); - set(ImGuiCol.WindowBg, theme->Gray100); - set(ImGuiCol.ChildBg, Spectrum.Static.None); - set(ImGuiCol.PopupBg, theme->Gray50); - set(ImGuiCol.Border, theme->Gray300); - set(ImGuiCol.BorderShadow, Spectrum.Static.None); - set(ImGuiCol.FrameBg, theme->Gray75); - set(ImGuiCol.FrameBgHovered, theme->Gray50); - set(ImGuiCol.FrameBgActive, theme->Gray200); - set(ImGuiCol.TitleBg, theme->Gray300); - set(ImGuiCol.TitleBgActive, theme->Gray200); - set(ImGuiCol.TitleBgCollapsed, theme->Gray400); - set(ImGuiCol.TabUnfocusedActive, theme->Blue400); - set(ImGuiCol.MenuBarBg, theme->Gray100); - set(ImGuiCol.ScrollbarBg, theme->Gray100); - set(ImGuiCol.ScrollbarGrab, theme->Gray400); - set(ImGuiCol.ScrollbarGrabHovered, theme->Gray600); - set(ImGuiCol.ScrollbarGrabActive, theme->Gray700); - set(ImGuiCol.CheckMark, theme->Blue500); - set(ImGuiCol.SliderGrab, theme->Gray700); - set(ImGuiCol.SliderGrabActive, theme->Gray800); - set(ImGuiCol.Button, theme->Gray75); - set(ImGuiCol.ButtonHovered, theme->Gray50); - set(ImGuiCol.ButtonActive, theme->Gray200); - set(ImGuiCol.Header, theme->Blue400); - set(ImGuiCol.HeaderHovered, theme->Blue500); - set(ImGuiCol.HeaderActive, theme->Blue600); - set(ImGuiCol.Separator, theme->Gray400); - set(ImGuiCol.SeparatorHovered, theme->Gray600); - set(ImGuiCol.SeparatorActive, theme->Gray700); - set(ImGuiCol.ResizeGrip, theme->Gray400); - set(ImGuiCol.ResizeGripHovered, theme->Gray600); - set(ImGuiCol.ResizeGripActive, theme->Gray700); - set(ImGuiCol.PlotLines, theme->Blue400); - set(ImGuiCol.PlotLinesHovered, theme->Blue600); - set(ImGuiCol.PlotHistogram, theme->Blue400); - set(ImGuiCol.PlotHistogramHovered, theme->Blue600); - - setVec(ImGuiCol.TextSelectedBg, ImGui.ColorConvertU32ToFloat4((colorValue(theme->Blue400) & 0x00FFFFFF) | 0x33000000)); - setVec(ImGuiCol.DragDropTarget, new Vector4(1.00f, 1.00f, 0.00f, 0.90f)); - setVec(ImGuiCol.NavHighlight, ImGui.ColorConvertU32ToFloat4((colorValue(theme->Gray900) & 0x00FFFFFF) | 0x0A000000)); - setVec(ImGuiCol.NavWindowingHighlight, new Vector4(1.00f, 1.00f, 1.00f, 0.70f)); - setVec(ImGuiCol.NavWindowingDimBg, new Vector4(0.80f, 0.80f, 0.80f, 0.20f)); - setVec(ImGuiCol.ModalWindowDimBg, new Vector4(0.20f, 0.20f, 0.20f, 0.35f)); - - return; - - void set(ImGuiCol colorVar, Color color) - => setVec(colorVar, new Vector4(color.R / 255f, color.G / 255f, color.B / 255f, 1f)); - - void setVec(ImGuiCol colorVar, Vector4 colorVec) - => style.Colors[(int)colorVar] = colorVec; - - uint colorValue(Color color) => ((uint)color.R << 16) - | ((uint)color.G << 8) - | color.B; - } -} \ No newline at end of file diff --git a/src/UI/Manager.cs b/src/UI/Manager.cs deleted file mode 100644 index 24bd6454..00000000 --- a/src/UI/Manager.cs +++ /dev/null @@ -1,187 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Numerics; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Gommon; -using ImGuiNET; -using Silk.NET.Windowing; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; - -#nullable enable - -namespace Volte.UI; - -// adapted from https://github.com/dotnet/Silk.NET/blob/main/examples/CSharp/OpenGL%20Demos/ImGui/Program.cs -public sealed partial class UiManager -{ - public readonly ConcurrentQueue TaskQueue = new(); - - private readonly List _views = []; - - private readonly unsafe ThemedColors* _theme; - - private int CurrentViewIdx { get; set; } - - private void SetView(int viewIndex) => - CurrentViewIdx = viewIndex.CoerceAtLeast(0).CoerceAtMost(_views.Count - 1); - - public UiView CurrentView => _views[CurrentViewIdx]; - - private unsafe UiManager(CreateParams @params) - { - _theme = @params.Theme; - - _window = Window.Create(@params.WOptions); - - _onConfigureIo = @params.OnConfigureIo; - _windowIcon = @params.WindowIcon; - - _window.Load += OnWindowLoad; - _window.Render += OnWindowRender; - _window.FramebufferResize += sz => _gl?.Viewport(sz); - - _window.Closing += () => - { - _isActive = false; - _controller?.Dispose(); - _inputContext?.Dispose(); - _gl?.Dispose(); - }; - } - - public void Run() - { - _isActive = true; - - Lambda - .Repeat(async () => - { - await Task.Delay( - 125); // tasks dont get added too often; so there's no need to run the polling below it as fast as possible. - if (TaskQueue.TryDequeue(out var task)) - await task.ConfigureAwait(false); - }) - .While(() => _isActive) - .Finally(() => TaskQueue.Clear()) - .Async(); - - _window.Run(); - } - - private const ImGuiWindowFlags DockSpaceFlags = - ImGuiWindowFlags.NoDocking | ImGuiWindowFlags.NoTitleBar | - ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove | - ImGuiWindowFlags.NoBringToFrontOnFocus | ImGuiWindowFlags.NoNavFocus; - - private void OnWindowRender(double delta) - { - if (_window.WindowState == WindowState.Minimized) return; - - _controller?.Update((float)delta); - - // shoutout https://gist.github.com/moebiussurfing/8dbc7fef5964adcd29428943b78e45d2 - // for showing me how to properly setup dock space - - var currView = CurrentView; - - var viewport = ImGui.GetMainViewport(); - - ImGui.SetNextWindowPos(viewport.WorkPos); - ImGui.SetNextWindowSize(viewport.WorkSize); - ImGui.SetNextWindowViewport(viewport.ID); - - using (ImGuiStyleVar.WindowRounding.PushValue(0f)) - using (ImGuiStyleVar.WindowBorderSize.PushValue(0f)) - ImGui.Begin("Dock Space", - currView.MainMenuBar != null - ? DockSpaceFlags | ImGuiWindowFlags.MenuBar - : DockSpaceFlags - ); - - ImGui.DockSpace(ImGui.GetID("DockSpace"), Vector2.Zero); - - if (currView.MainMenuBar is { } menuBar) - if (ImGui.BeginMenuBar()) - { - menuBar(delta); - ImGui.EndMenuBar(); - } - - currView.RenderInternal(delta); - - ImGui.End(); - - _controller?.Render(); - } - - public void Dispose() => _window.Dispose(); - - public static UiManager? Instance { get; private set; } - - public readonly struct CreateParams - { - public WindowOptions WOptions { get; init; } - public Action OnConfigureIo { get; init; } - public Image? WindowIcon { get; init; } - public unsafe ThemedColors* Theme { get; init; } - } - - public static bool TryCreateUi(CreateParams createParams, out Exception? error) - { - if (Instance is not null) - { - error = new InvalidOperationException("UI is already open."); - return false; - } - - try - { - Instance = new UiManager(createParams); - } - catch (Exception e) - { - error = e; - return false; - } - - error = null; - - return true; - } - - public static void StartThread(string threadName) - { - // declared as illegal code by the Silk God (Main thread isn't the controller of the Window) - new Thread(() => - { - Instance?.Run(); //returns when UI is closed - Instance?.Dispose(); - Instance = null; - }) { Name = threadName }.Start(); - } - - public static void AddView(UiView view) => Instance!._views.Add(view); - - public static unsafe void LoadFontFromStream(Stream stream, string fontName, float fontSize) - { - var fontData = stream.ToSpan(); - - fixed (byte* fontDataPtr = fontData) - { - var fontConfig = ImGuiNative.ImFontConfig_ImFontConfig(); - fontConfig->FontData = fontDataPtr; - fontConfig->FontDataSize = fontData.Length; - fontConfig->SizePixels = fontSize; - - Buffers.CopyBytesFromString(fontConfig->Name, 40, fontName); //40 is the size of the name buffer in ImFontConfig - - ImGuiNative.ImFontAtlas_AddFont(ImGui.GetIO().Fonts, fontConfig); - } - } -} \ No newline at end of file diff --git a/src/UI/Program.cs b/src/UI/Program.cs new file mode 100644 index 00000000..ecdcb085 --- /dev/null +++ b/src/UI/Program.cs @@ -0,0 +1,27 @@ +using Avalonia; +using Volte.Helpers; + +namespace Volte.UI; + +public class Program +{ + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static int Main(string[] args) + { + if (!UnixHelper.TryParseNamedArguments(args, out var output) && output.Error is not InvalidOperationException) + Logger.Error(output.Error); + + return BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + } + + // Avalonia configuration, don't remove; also used by visual designer. + private static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .WithInterFont() + .LogToTrace(Avalonia.Logging.LogEventLevel.Debug, "TabView"); +} \ No newline at end of file diff --git a/src/UI/README.md b/src/UI/README.md new file mode 100644 index 00000000..c010bc24 --- /dev/null +++ b/src/UI/README.md @@ -0,0 +1,4 @@ +# Volte's Avalonia UI + +This UI built with Avalonia and FluentAvalonia, and is a direct modification of the UI of the [Tears of the Kingdom Mod Merger](https://github.com/TKMM-Team/Tkmm/).
+Obviously Volte has no mod merging functionality, so what it's using is basically just TKMM's styling & instrumentation code. \ No newline at end of file diff --git a/src/UI/Spectrum.cs b/src/UI/Spectrum.cs deleted file mode 100644 index 6f1b8308..00000000 --- a/src/UI/Spectrum.cs +++ /dev/null @@ -1,254 +0,0 @@ -using Silk.NET.SDL; - -namespace Volte.UI; - -// https://github.com/adobe/imgui/blob/master/imgui_spectrum.h -public static class Spectrum -{ - public static unsafe ThemedColors* Dark - { - get - { - fixed (DarkThemedColors* ptr = &DarkThemedColors.Instance) - return (ThemedColors*)ptr; - } - } - - public static unsafe ThemedColors* Light - { - get - { - fixed (LightThemedColors* ptr = &LightThemedColors.Instance) - return (ThemedColors*)ptr; - } - } - - public class DarkThemedColors : ThemedColors - { - internal static DarkThemedColors Instance = new(); - internal override Color Gray50 => Color(0x252525); - internal override Color Gray75 => Color(0x2F2F2F); - internal override Color Gray100 => Color(0x323232); - internal override Color Gray200 => Color(0x393939); - internal override Color Gray300 => Color(0x3E3E3E); - internal override Color Gray400 => Color(0x4D4D4D); - internal override Color Gray500 => Color(0x5C5C5C); - internal override Color Gray600 => Color(0x7B7B7B); - internal override Color Gray700 => Color(0x999999); - internal override Color Gray800 => Color(0xCDCDCD); - internal override Color Gray900 => Color(0xFFFFFF); - internal override Color Blue400 => Color(0x2680EB); - internal override Color Blue500 => Color(0x378EF0); - internal override Color Blue600 => Color(0x4B9CF5); - internal override Color Blue700 => Color(0x5AA9FA); - internal override Color Red400 => Color(0xE34850); - internal override Color Red500 => Color(0xEC5B62); - internal override Color Red600 => Color(0xF76D74); - internal override Color Red700 => Color(0xFF7B82); - internal override Color Orange400 => Color(0xE68619); - internal override Color Orange500 => Color(0xF29423); - internal override Color Orange600 => Color(0xF9A43F); - internal override Color Orange700 => Color(0xFFB55B); - internal override Color Green400 => Color(0x2D9D78); - internal override Color Green500 => Color(0x33AB84); - internal override Color Green600 => Color(0x39B990); - internal override Color Green700 => Color(0x3FC89C); - internal override Color Indigo400 => Color(0x6767EC); - internal override Color Indigo500 => Color(0x7575F1); - internal override Color Indigo600 => Color(0x8282F6); - internal override Color Indigo700 => Color(0x9090FA); - internal override Color Celery400 => Color(0x44B556); - internal override Color Celery500 => Color(0x4BC35F); - internal override Color Celery600 => Color(0x51D267); - internal override Color Celery700 => Color(0x58E06F); - internal override Color Magenta400 => Color(0xD83790); - internal override Color Magenta500 => Color(0xE2499D); - internal override Color Magenta600 => Color(0xEC5AAA); - internal override Color Magenta700 => Color(0xF56BB7); - internal override Color Yellow400 => Color(0xDFBF00); - internal override Color Yellow500 => Color(0xEDCC00); - internal override Color Yellow600 => Color(0xFAD900); - internal override Color Yellow700 => Color(0xFFE22E); - internal override Color Fuchsia400 => Color(0xC038CC); - internal override Color Fuchsia500 => Color(0xCF3EDC); - internal override Color Fuchsia600 => Color(0xD951E5); - internal override Color Fuchsia700 => Color(0xE366EF); - internal override Color Seafoam400 => Color(0x1B959A); - internal override Color Seafoam500 => Color(0x20A3A8); - internal override Color Seafoam600 => Color(0x23B2B8); - internal override Color Seafoam700 => Color(0x26C0C7); - internal override Color Chartreuse400 => Color(0x85D044); - internal override Color Chartreuse500 => Color(0x8EDE49); - internal override Color Chartreuse600 => Color(0x9BEC54); - internal override Color Chartreuse700 => Color(0xA3F858); - internal override Color Purple400 => Color(0x9256D9); - internal override Color Purple500 => Color(0x9D64E1); - internal override Color Purple600 => Color(0xA873E9); - internal override Color Purple700 => Color(0xB483F0); - } - - public class LightThemedColors : ThemedColors - { - internal static LightThemedColors Instance = new(); - internal override Color Gray50 => Color(0xFFFFFF); - internal override Color Gray75 => Color(0xFAFAFA); - internal override Color Gray100 => Color(0xF5F5F5); - internal override Color Gray200 => Color(0xEAEAEA); - internal override Color Gray300 => Color(0xE1E1E1); - internal override Color Gray400 => Color(0xCACACA); - internal override Color Gray500 => Color(0xB3B3B3); - internal override Color Gray600 => Color(0x8E8E8E); - internal override Color Gray700 => Color(0x707070); - internal override Color Gray800 => Color(0x4B4B4B); - internal override Color Gray900 => Color(0x2C2C2C); - internal override Color Blue400 => Color(0x2680EB); - internal override Color Blue500 => Color(0x1473E6); - internal override Color Blue600 => Color(0x0D66D0); - internal override Color Blue700 => Color(0x095ABA); - internal override Color Red400 => Color(0xE34850); - internal override Color Red500 => Color(0xD7373F); - internal override Color Red600 => Color(0xC9252D); - internal override Color Red700 => Color(0xBB121A); - internal override Color Orange400 => Color(0xE68619); - internal override Color Orange500 => Color(0xDA7B11); - internal override Color Orange600 => Color(0xCB6F10); - internal override Color Orange700 => Color(0xBD640D); - internal override Color Green400 => Color(0x2D9D78); - internal override Color Green500 => Color(0x268E6C); - internal override Color Green600 => Color(0x12805C); - internal override Color Green700 => Color(0x107154); - internal override Color Indigo400 => Color(0x6767EC); - internal override Color Indigo500 => Color(0x5C5CE0); - internal override Color Indigo600 => Color(0x5151D3); - internal override Color Indigo700 => Color(0x4646C6); - internal override Color Celery400 => Color(0x44B556); - internal override Color Celery500 => Color(0x3DA74E); - internal override Color Celery600 => Color(0x379947); - internal override Color Celery700 => Color(0x318B40); - internal override Color Magenta400 => Color(0xD83790); - internal override Color Magenta500 => Color(0xCE2783); - internal override Color Magenta600 => Color(0xBC1C74); - internal override Color Magenta700 => Color(0xAE0E66); - internal override Color Yellow400 => Color(0xDFBF00); - internal override Color Yellow500 => Color(0xD2B200); - internal override Color Yellow600 => Color(0xC4A600); - internal override Color Yellow700 => Color(0xB79900); - internal override Color Fuchsia400 => Color(0xC038CC); - internal override Color Fuchsia500 => Color(0xB130BD); - internal override Color Fuchsia600 => Color(0xA228AD); - internal override Color Fuchsia700 => Color(0x93219E); - internal override Color Seafoam400 => Color(0x1B959A); - internal override Color Seafoam500 => Color(0x16878C); - internal override Color Seafoam600 => Color(0x0F797D); - internal override Color Seafoam700 => Color(0x096C6F); - internal override Color Chartreuse400 => Color(0x85D044); - internal override Color Chartreuse500 => Color(0x7CC33F); - internal override Color Chartreuse600 => Color(0x73B53A); - internal override Color Chartreuse700 => Color(0x6AA834); - internal override Color Purple400 => Color(0x9256D9); - internal override Color Purple500 => Color(0x864CCC); - internal override Color Purple600 => Color(0x7A42BF); - internal override Color Purple700 => Color(0x6F38B1); - } - - - public static class Static - { - internal static Color None = Color(0x000000); - internal static Color Gray200 = Color(0xF4F4F4); - internal static Color Gray300 = Color(0xEAEAEA); - internal static Color Gray400 = Color(0xD3D3D3); - internal static Color Gray500 = Color(0xBCBCBC); - internal static Color Gray600 = Color(0x959595); - internal static Color Gray700 = Color(0x767676); - internal static Color Gray800 = Color(0x505050); - internal static Color Gray900 = Color(0x323232); - internal static Color Blue400 = Color(0x378EF0); - internal static Color Blue500 = Color(0x2680EB); - internal static Color Blue600 = Color(0x1473E6); - internal static Color Blue700 = Color(0x0D66D0); - internal static Color Red400 = Color(0xEC5B62); - internal static Color Red500 = Color(0xE34850); - internal static Color Red600 = Color(0xD7373F); - internal static Color Red700 = Color(0xC9252D); - internal static Color Orange400 = Color(0xF29423); - internal static Color Orange500 = Color(0xE68619); - internal static Color Orange600 = Color(0xDA7B11); - internal static Color Orange700 = Color(0xCB6F10); - internal static Color Green400 = Color(0x33AB84); - internal static Color Green500 = Color(0x2D9D78); - internal static Color Green600 = Color(0x268E6C); - internal static Color Green700 = Color(0x12805C); - } - - private static Color Color(uint raw) - { - var r = (raw >> 16) & 0xFF; - var g = (raw >> 8) & 0xFF; - var b = (raw >> 0) & 0xFF; - return new Color((byte)r, (byte)g, (byte)b); - } -} - -public abstract class ThemedColors -{ - internal abstract Color Gray50 { get; } - internal abstract Color Gray75 { get; } - internal abstract Color Gray100 { get; } - internal abstract Color Gray200 { get; } - internal abstract Color Gray300 { get; } - internal abstract Color Gray400 { get; } - internal abstract Color Gray500 { get; } - internal abstract Color Gray600 { get; } - internal abstract Color Gray700 { get; } - internal abstract Color Gray800 { get; } - internal abstract Color Gray900 { get; } - internal abstract Color Blue400 { get; } - internal abstract Color Blue500 { get; } - internal abstract Color Blue600 { get; } - internal abstract Color Blue700 { get; } - internal abstract Color Red400 { get; } - internal abstract Color Red500 { get; } - internal abstract Color Red600 { get; } - internal abstract Color Red700 { get; } - internal abstract Color Orange400 { get; } - internal abstract Color Orange500 { get; } - internal abstract Color Orange600 { get; } - internal abstract Color Orange700 { get; } - internal abstract Color Green400 { get; } - internal abstract Color Green500 { get; } - internal abstract Color Green600 { get; } - internal abstract Color Green700 { get; } - internal abstract Color Indigo400 { get; } - internal abstract Color Indigo500 { get; } - internal abstract Color Indigo600 { get; } - internal abstract Color Indigo700 { get; } - internal abstract Color Celery400 { get; } - internal abstract Color Celery500 { get; } - internal abstract Color Celery600 { get; } - internal abstract Color Celery700 { get; } - internal abstract Color Magenta400 { get; } - internal abstract Color Magenta500 { get; } - internal abstract Color Magenta600 { get; } - internal abstract Color Magenta700 { get; } - internal abstract Color Yellow400 { get; } - internal abstract Color Yellow500 { get; } - internal abstract Color Yellow600 { get; } - internal abstract Color Yellow700 { get; } - internal abstract Color Fuchsia400 { get; } - internal abstract Color Fuchsia500 { get; } - internal abstract Color Fuchsia600 { get; } - internal abstract Color Fuchsia700 { get; } - internal abstract Color Seafoam400 { get; } - internal abstract Color Seafoam500 { get; } - internal abstract Color Seafoam600 { get; } - internal abstract Color Seafoam700 { get; } - internal abstract Color Chartreuse400 { get; } - internal abstract Color Chartreuse500 { get; } - internal abstract Color Chartreuse600 { get; } - internal abstract Color Chartreuse700 { get; } - internal abstract Color Purple400 { get; } - internal abstract Color Purple500 { get; } - internal abstract Color Purple600 { get; } - internal abstract Color Purple700 { get; } -} \ No newline at end of file diff --git a/src/UI/View.cs b/src/UI/View.cs deleted file mode 100644 index 6d19d5c2..00000000 --- a/src/UI/View.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Numerics; -using System.Threading.Tasks; -using ImGuiNET; -using Silk.NET.Input; -using Silk.NET.SDL; - -namespace Volte.UI; - -[SuppressMessage("ReSharper", "VirtualMemberNeverOverridden.Global")] //this is an API -public abstract class UiView -{ - private readonly List> _panels = []; - - public Action MainMenuBar { get; protected init; } - - protected static ImGuiIOPtr Io => ImGui.GetIO(); - - public static bool IsKeyPressed(Key key) - => Io.KeysDown[(int)key]; - - public static bool IsMouseButtonPressed(MouseButton mb) - => Io.MouseDown[(int)mb]; - - public static bool AllKeysPressed(params Key[] keys) - { - if (keys.Length == 1) - return IsKeyPressed(keys[0]); - - var io = Io; - return keys.Select(key => io.KeysDown[(int)key]) - .All(x => x); - } - - public static bool AllMouseButtonsPressed(params MouseButton[] mouseButtons) - { - if (mouseButtons.Length == 1) - return Io.MouseDown[(int)mouseButtons[0]]; - - var io = Io; - return mouseButtons.Select(mb => io.MouseDown[(int)mb]) - .All(x => x); - } - - /** - * Override this function for custom one-off rendering. - * The return value determines whether your 's defined panels will be rendered. - * (false = no render, true = render) - */ - protected virtual bool Render(double _) => true; - - internal void RenderInternal(double delta) - { - if (!Render(delta)) return; - - foreach (var renderPanel in _panels) - renderPanel(delta); - } - - protected static void Await(Func task) => Await(task()); - protected static void Await(Task task) => UiManager.Instance!.TaskQueue.Enqueue(task); - - protected void Panel(string label, Action render) => _panels.Add(delta => - { - if (ImGui.Begin(label)) - { - render(delta); - ImGui.End(); - } - }); - - protected void Panel(Action render) => _panels.Add(render); -} \ No newline at end of file diff --git a/src/UI/Volte.UI.csproj b/src/UI/Volte.UI.csproj index 02924f1d..75c15ff8 100644 --- a/src/UI/Volte.UI.csproj +++ b/src/UI/Volte.UI.csproj @@ -1,28 +1,33 @@  - GreemDev - Polyhaze - https://github.com/Polyhaze/Volte - git + Exe net8.0 - true - 1.0.1 - true - $(NoWarn);CS8500 - The mini abstraction used for the UI of Volte. https://github.com/Polyhaze/Volte - - Themes are no longer passed by ref, and instead are passed by pointer. - Layers are now added AFTER UiManager is initialized and the instance is set. - TryCreateUi no longer automatically starts the UI thread, for you to add layers & do any custom ImGui setup before starting the thread. - - Silk.NET;Silk;Silk.NET.Windowing;Silk.NET.OpenGL;Silk.NET.OpenGL.Extensions.ImGui;Silk.NET.Input;ImGui;UI;GUI + latest + enable + enable - - - - - - + + + + + + + + + + + + + + + + App.axaml + Code + + + MainWindow.axaml + Code + diff --git a/src/UI/nupkg.sh b/src/UI/nupkg.sh deleted file mode 100644 index d6cbfdfc..00000000 --- a/src/UI/nupkg.sh +++ /dev/null @@ -1 +0,0 @@ -dotnet publish -r Release \ No newline at end of file