diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 80368ec..a3a6b93 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,7 +38,7 @@ name: build env: - DWAppVersion: v1.7.0r + DWAppVersion: v1.7.1r permissions: contents: write diff --git a/DynamicWin/Main/App.xaml.cs b/DynamicWin/Main/App.xaml.cs index 0ba6dda..03445b7 100644 --- a/DynamicWin/Main/App.xaml.cs +++ b/DynamicWin/Main/App.xaml.cs @@ -20,7 +20,7 @@ public partial class DynamicWinMain : System.Windows.Application public static MMDevice? defaultDevice; public static MMDevice? defaultMicrophone; - public static string Version => "v1.7.0r"; + public static string Version => "v1.7.1r"; public static Channel ReleaseStream => Channel.Release; public static Architecture ProcessArchitecture => RuntimeInformation.ProcessArchitecture; diff --git a/DynamicWin/UI/Menu/Menus/SettingsMenu.cs b/DynamicWin/UI/Menu/Menus/SettingsMenu.cs index 621a078..a1a47b8 100644 --- a/DynamicWin/UI/Menu/Menus/SettingsMenu.cs +++ b/DynamicWin/UI/Menu/Menus/SettingsMenu.cs @@ -1,594 +1,449 @@ -using DynamicWin.Main; +using DynamicWin.Main; using DynamicWin.Resources; using DynamicWin.UI.Menu.Menus.SettingsMenuObjects; using DynamicWin.UI.UIElements; using DynamicWin.UI.UIElements.Custom; using DynamicWin.UI.Widgets; -using DynamicWin.UI.Widgets.Small; using DynamicWin.Utils; -using Newtonsoft.Json.Linq; using SkiaSharp; -using System; -using System.Collections.Generic; -using System.ComponentModel; using System.IO; -using System.Linq; using System.Reflection; -using System.Text; -using System.Threading.Tasks; -using System.Windows.Controls; -using System.Windows.Forms; using System.Windows.Input; -using System.Windows.Navigation; -using System.Xml.Linq; -using static DynamicWin.UI.UIElements.IslandObject; -using static System.Net.Mime.MediaTypeNames; namespace DynamicWin.UI.Menu.Menus { public class SettingsMenu : BaseMenu { - private static List _cachedCustomOptions; + private const float ContentLeft = 25f; + private const float ContentTop = 35f; + private const float ContentSpacing = 10f; + private const float TitleBottomTuck = -12f; + private const float SectionTitleBottomTuck = -6f; + private const float BottomReserve = 85f; + private const float ScrollSpeed = 0.50f; + private const float ScrollSmoothSpeed = 10f; + private const float ScrollClampSpeed = 15f; + private const float ScrollEpsilon = 0.05f; + + private static readonly Vec2 CheckboxSize = new Vec2(25, 32); + private static List? cachedCustomOptions; + private readonly Action scrollHandler; + private List contentObjects = new List(); + private readonly Dictionary layoutBottomAdjustments = new Dictionary(); + private SmallWidgetAdder? smallWidgetAdder; + private BigWidgetAdder? bigWidgetAdder; + private UIObject? bottomMask; + + private SettingsCheckbox allowBlur = null!; + private SettingsCheckbox allowAnimation = null!; + private SettingsCheckbox antiAliasing = null!; + private SettingsCheckbox runOnStartup = null!; + private SettingsCheckbox allowAutomaticUpdates = null!; + private SettingsCheckbox alwaysTopmost = null!; + private SettingsCheckbox reduceWorkingArea = null!; + private SettingsCheckbox toggleIslandShadow = null!; + private SettingsCheckbox toggleHomeMenuShadow = null!; + private SettingsCheckbox toggleHighRefreshRate = null!; + private SettingsCheckbox limitRefreshRateWhenIdle = null!; + + private DWText limitRefreshRateDisclaimer1 = null!; + private DWText limitRefreshRateDisclaimer2 = null!; + private DWMultiSelectionButton? bigMenuModeSelector; + + private bool changedTheme; + private bool layoutDirty = true; + private float yScrollOffset; + private float ySmoothScroll; + private float cachedScrollLimit; + private float lastContentSignature = float.NaN; + private float lastIslandWidth = float.NaN; + private float lastIslandHeight = float.NaN; + public SettingsMenu() { scrollHandler = OnScroll; MainForm.onScrollEvent += scrollHandler; } - private void OnScroll(MouseWheelEventArgs x) + public override List InitializeMenu(IslandObject island) { - if (!ReferenceEquals(MenuManager.Instance?.ActiveMenu, this)) return; - yScrollOffset += x.Delta * 0.50f; - } + contentObjects = new List(); + layoutBottomAdjustments.Clear(); + layoutDirty = true; + changedTheme = false; + yScrollOffset = 0f; + ySmoothScroll = 0f; + cachedScrollLimit = 0f; + lastContentSignature = float.NaN; - bool changedTheme = false; + var objects = base.InitializeMenu(island); + var customOptions = LoadCustomOptions(); - DWMultiSelectionButton bigMenuModeSelector; + foreach (var option in customOptions) + option.LoadSettings(); - void SaveAndBack() - { - Settings.AllowBlur = allowBlur.IsChecked; - Settings.AllowAnimation = allowAnimation.IsChecked; - Settings.AntiAliasing = antiAliasing.IsChecked; - Settings.ToggleHighRefreshRate = toggleHighRefreshRate.IsChecked; - Settings.LimitRefreshRateWhenIdle = limitRefreshRateWhenIdle != null && limitRefreshRateWhenIdle.IsChecked; - Settings.ToggleIslandShadow = toggleIslandShadow.IsChecked; - Settings.ToggleHomeMenuShadow = toggleHomeMenuShadow.IsChecked; - Settings.RunOnStartup = runOnStartup.IsChecked; - Settings.AllowAutomaticUpdates = allowAutomaticUpdates.IsChecked; - Settings.AlwaysTopmost = alwaysTopmost.IsChecked; - Settings.ReduceWorkingArea = reduceWorkingArea.IsChecked; - - // Save the selected default big menu mode - if (bigMenuModeSelector != null) + AddGap(objects, island, 10f); + AddTitle(objects, island, "General"); + AddSectionTitle(objects, island, "Island Mode"); + var islandMode = AddSelector(objects, island, new[] { "Island", "Notch" }); + islandMode.SelectedIndex = Settings.IslandMode == IslandObject.IslandMode.Island ? 0 : 1; + islandMode.onClick += index => { - Settings.DefaultBigMenuMode = bigMenuModeSelector.SelectedIndex switch - { - 0 => HomeMenu.BigMenuMode.Widgets, - 1 => HomeMenu.BigMenuMode.Tray, - 2 => HomeMenu.BigMenuMode.Media, - _ => HomeMenu.BigMenuMode.Widgets - }; - } + Settings.IslandMode = index == 0 + ? IslandObject.IslandMode.Island + : IslandObject.IslandMode.Notch; + }; - DynamicWinMain.UpdateStartup(); + AddBodyText(objects, island, "Renders the interface to its minimum height and width possible, also improves performance."); + AddBodyText(objects, island, "Disabling this setting may prevent the interface from being placed correctly at the top."); - if (changedTheme) - Theme.Instance.UpdateTheme(true); - else - { - Res.HomeMenu = new HomeMenu(); - MenuManager.OpenMenu(Res.HomeMenu); - } + alwaysTopmost = AddCheckbox(objects, island, "Keep interface always topmost", Settings.AlwaysTopmost); + AddBodyText(objects, island, "Prevents the interface from overlapping on top of other windows."); + + reduceWorkingArea = AddCheckbox(objects, island, "Reduce working area", Settings.ReduceWorkingArea); + allowBlur = AddCheckbox(objects, island, "Toggle blur", Settings.AllowBlur); + allowAnimation = AddCheckbox(objects, island, "Toggle animations", Settings.AllowAnimation); + antiAliasing = AddCheckbox(objects, island, "Toggle anti-aliasing", Settings.AntiAliasing); - foreach (var item in _cachedCustomOptions) + AddBodyText(objects, island, "Enables application to run at the highest refresh rate supported by your monitor."); + AddBodyText(objects, island, "This setting will cause performance degradation on some devices, proceed with caution."); + toggleHighRefreshRate = AddCheckbox(objects, island, "Toggle high-refresh-rate mode", Settings.ToggleHighRefreshRate, () => { - item.SaveSettings(); - } + bool enabled = toggleHighRefreshRate.IsChecked; + SetRefreshRateSubSettingsEnabled(enabled, immediate: false); - Settings.Save(); - } + if (!enabled) + Settings.LimitRefreshRateWhenIdle = false; + }); - DWCheckbox allowBlur; - DWCheckbox allowAnimation; - DWCheckbox antiAliasing; - DWCheckbox runOnStartup; - DWCheckbox allowAutomaticUpdates; - DWCheckbox alwaysTopmost; - DWCheckbox reduceWorkingArea; - DWCheckbox toggleIslandShadow; - DWCheckbox toggleHomeMenuShadow; - DWCheckbox toggleHighRefreshRate; - DWCheckbox limitRefreshRateWhenIdle; + limitRefreshRateDisclaimer1 = AddBodyText(objects, island, "Renders the application at 60 hertz when not hovered.", left: 65f); + limitRefreshRateDisclaimer2 = AddBodyText(objects, island, "Toggle this setting to improve some of the performance usage.", left: 65f); + limitRefreshRateWhenIdle = AddCheckbox(objects, island, "Limit refresh rate when idle", Settings.LimitRefreshRateWhenIdle, left: 65f); + SetRefreshRateSubSettingsEnabled(toggleHighRefreshRate.IsChecked, immediate: true); - DWText refreshRateDisclaimer1, refreshRateDisclaimer2, limitRefreshRateDisclaimer1, limitRefreshRateDisclaimer2, topmostDisclaimer, topmostDisclaimer2, workingAreaDisclaimer; + toggleIslandShadow = AddCheckbox(objects, island, "Toggle island shadow", Settings.ToggleIslandShadow, () => + { + bool enabled = toggleIslandShadow.IsChecked; + SetHomeMenuShadowEnabled(enabled, immediate: false); - UIObject bottomMask; + if (!enabled) + Settings.ToggleHomeMenuShadow = false; + }); - public override List InitializeMenu(IslandObject island) - { - var objects = base.InitializeMenu(island); + toggleHomeMenuShadow = AddCheckbox(objects, island, "Toggle home menu shadow when idle", Settings.ToggleHomeMenuShadow, left: 65f); + SetHomeMenuShadowEnabled(toggleIslandShadow.IsChecked, immediate: true); - LoadCustomOptions(); + runOnStartup = AddCheckbox(objects, island, "Start application on login", Settings.RunOnStartup); + allowAutomaticUpdates = AddCheckbox(objects, island, "Allow automatic updates", Settings.AllowAutomaticUpdates); - foreach (var item in _cachedCustomOptions) - { - item.LoadSettings(); - } + AddMonitorSelector(objects, island); + AddDefaultBigMenuSelector(objects, island); + AddThemeSelector(objects, island); - var generalTitle = new DWText(island, "General", new Vec2(25, 0), UIAlignment.TopLeft); - generalTitle.Font = Res.SFProBold; - generalTitle.Anchor.X = 0; - objects.Add(generalTitle); + AddGap(objects, island, 18f); + AddTitle(objects, island, "Widgets"); - { - var islandModesTitle = new DWText(island, "Island Mode", new Vec2(25, 0), UIAlignment.TopLeft); - islandModesTitle.Font = Res.SFProBold; - islandModesTitle.Color = Theme.TextMain; - islandModesTitle.TextSize = 15; - islandModesTitle.Anchor.X = 0; - objects.Add(islandModesTitle); + AddSectionTitle(objects, island, "Small widgets (right click to add/edit)"); + smallWidgetAdder = new SmallWidgetAdder(island, Vec2.zero, new Vec2(IslandSize().X - 50, 35), UIAlignment.TopCenter); + AddContent(objects, smallWidgetAdder); - var islandModes = new string[] { "Island", "Notch" }; - var islandMode = new DWMultiSelectionButton(island, islandModes, new Vec2(25, 0), new Vec2(IslandSize().X - 50, 25), UIAlignment.TopLeft); - islandMode.SelectedIndex = (Settings.IslandMode == IslandObject.IslandMode.Island) ? 0 : 1; - islandMode.Anchor.X = 0; - islandMode.onClick += (index) => - { - Settings.IslandMode = (index == 0) ? IslandObject.IslandMode.Island : IslandObject.IslandMode.Notch; - }; - objects.Add(islandMode); - } + AddSectionTitle(objects, island, "Big widgets (right click to add/edit)", topPadding: 15f); + bigWidgetAdder = new BigWidgetAdder(island, Vec2.zero, new Vec2(IslandSize().X - 50, 35), UIAlignment.TopCenter); + AddContent(objects, bigWidgetAdder); - topmostDisclaimer = new DWText(island, "Renders the interface to its minimum height and width possible, also improves performance.", new Vec2(25, 0), UIAlignment.TopLeft); - topmostDisclaimer.Font = Res.SFProRegular; - topmostDisclaimer.TextSize = 12; - topmostDisclaimer.Anchor.X = 0; - - topmostDisclaimer2 = new DWText(island, "Disabling this setting may prevent the interface from being placed correctly at the top.", new Vec2(25, 0), UIAlignment.TopLeft); - topmostDisclaimer2.Font = Res.SFProRegular; - topmostDisclaimer2.TextSize = 12; - topmostDisclaimer2.Anchor.X = 0; - - objects.Add(topmostDisclaimer); - objects.Add(topmostDisclaimer2); - - alwaysTopmost = new DWCheckbox(island, "Keep interface always topmost", new Vec2(25, 0), new Vec2(25, 25), () => { }, UIAlignment.TopLeft); - alwaysTopmost.IsChecked = Settings.AlwaysTopmost; - alwaysTopmost.Anchor.X = 0; - objects.Add(alwaysTopmost); - - workingAreaDisclaimer = new DWText(island, "Prevents the interface from overlapping on top of other windows.", new Vec2(25, 0), UIAlignment.TopLeft); - workingAreaDisclaimer.TextSize = 12; - workingAreaDisclaimer.Font = Res.SFProRegular; - workingAreaDisclaimer.Anchor.X = 0; - - reduceWorkingArea = new DWCheckbox(island, "Reduce working area", new Vec2(25, 0), new Vec2(25, 25), () => { }, UIAlignment.TopLeft); - reduceWorkingArea.IsChecked = Settings.ReduceWorkingArea; - reduceWorkingArea.Anchor.X = 0; - - objects.Add(workingAreaDisclaimer); - objects.Add(reduceWorkingArea); - - allowBlur = new DWCheckbox(island, "Toggle blur", new Vec2(25, 0), new Vec2(25, 25), () => { }, UIAlignment.TopLeft); - allowBlur.IsChecked = Settings.AllowBlur; - allowBlur.Anchor.X = 0; - objects.Add(allowBlur); - - allowAnimation = new DWCheckbox(island, "Toggle animations", new Vec2(25, 0), new Vec2(25, 25), () => { }, UIAlignment.TopLeft); - allowAnimation.IsChecked = Settings.AllowAnimation; - allowAnimation.Anchor.X = 0; - objects.Add(allowAnimation); - - antiAliasing = new DWCheckbox(island, "Toggle anti-aliasing", new Vec2(25, 0), new Vec2(25, 25), () => { }, UIAlignment.TopLeft); - antiAliasing.IsChecked = Settings.AntiAliasing; - antiAliasing.Anchor.X = 0; - objects.Add(antiAliasing); - - refreshRateDisclaimer1 = new DWText(island, "Enables application to run at the highest refresh rate supported by your monitor.", new Vec2(25, 0), UIAlignment.TopLeft); - refreshRateDisclaimer1.Font = Res.SFProRegular; - refreshRateDisclaimer1.TextSize = 12; - refreshRateDisclaimer1.Anchor.X = 0; - - refreshRateDisclaimer2 = new DWText(island, "This setting will cause performance degradation on some devices, proceed with caution.", new Vec2(25, 0), UIAlignment.TopLeft); - refreshRateDisclaimer2.Font = Res.SFProRegular; - refreshRateDisclaimer2.TextSize = 12; - refreshRateDisclaimer2.Anchor.X = 0; - - toggleHighRefreshRate = new DWCheckbox( - island, - "Toggle high-refresh-rate mode", - new Vec2(25, 0), - new Vec2(25, 25), - () => - { - bool enabled = toggleHighRefreshRate.IsChecked; + AddGap(objects, island, 18f); + AddTitle(objects, island, "Widget Settings"); + AddCustomOptions(objects, island, customOptions); - limitRefreshRateWhenIdle.IsEnabled = enabled; - limitRefreshRateDisclaimer1.IsEnabled = enabled; - limitRefreshRateDisclaimer2.IsEnabled = enabled; + AddReleaseStream(objects, island); + AddVersionInfo(objects, island); - if (!enabled) - { - limitRefreshRateWhenIdle.IsChecked = false; - Settings.LimitRefreshRateWhenIdle = false; - } - }, - UIAlignment.TopLeft - ); - toggleHighRefreshRate.IsChecked = Settings.ToggleHighRefreshRate; - toggleHighRefreshRate.Anchor.X = 0; + objects.Add(new SettingsRenderDemand(island, NeedsRealtimeLayout)); + AddSaveButton(objects, island); - objects.Add(refreshRateDisclaimer1); - objects.Add(refreshRateDisclaimer2); - objects.Add(toggleHighRefreshRate); + return objects; + } - limitRefreshRateWhenIdle = new DWCheckbox( - island, - "Limit refresh rate when idle", - new Vec2(65, 0), - new Vec2(25, 25), - () => { }, - UIAlignment.TopLeft - ); - limitRefreshRateWhenIdle.IsChecked = Settings.LimitRefreshRateWhenIdle; - limitRefreshRateWhenIdle.Anchor.X = 0; - - limitRefreshRateDisclaimer1 = new DWText( - island, - "Renders the application at 60 hertz when not hovered.", - new Vec2(65, 0), - UIAlignment.TopLeft - ) - { - Font = Res.SFProRegular, - TextSize = 12, - Anchor = new Vec2(0, 0) - }; + public override void Update() + { + base.Update(); - limitRefreshRateDisclaimer2 = new DWText( - island, - "Toggle this setting to improve some of the performance usage.", - new Vec2(65, 0), - UIAlignment.TopLeft - ) - { - Font = Res.SFProRegular, - TextSize = 12, - Anchor = new Vec2(0, 0) - }; + if (bottomMask != null) + bottomMask.blurAmount = 15; - objects.Add(limitRefreshRateDisclaimer1); - objects.Add(limitRefreshRateDisclaimer2); - objects.Add(limitRefreshRateWhenIdle); + float deltaTime = RendererMain.Instance?.DeltaTime ?? 1f / 60f; - bool enableRefreshRateSubSettings = toggleHighRefreshRate.IsChecked; + float signature = BuildContentSignature(); + if (float.IsNaN(lastContentSignature) || Math.Abs(signature - lastContentSignature) > 0.01f) + { + lastContentSignature = signature; + layoutDirty = true; + } - limitRefreshRateWhenIdle.IsEnabled = enableRefreshRateSubSettings; - limitRefreshRateDisclaimer1.IsEnabled = enableRefreshRateSubSettings; - limitRefreshRateDisclaimer2.IsEnabled = enableRefreshRateSubSettings; + var islandSize = IslandSize(); + if (float.IsNaN(lastIslandWidth) + || Math.Abs(lastIslandWidth - islandSize.X) > 0.05f + || Math.Abs(lastIslandHeight - islandSize.Y) > 0.05f) + { + lastIslandWidth = islandSize.X; + lastIslandHeight = islandSize.Y; + layoutDirty = true; + } - toggleIslandShadow = new DWCheckbox( - island, - "Toggle island shadow", - new Vec2(25, 0), - new Vec2(25, 25), - () => - { - bool enabled = toggleIslandShadow.IsChecked; + float clampedTarget = Mathf.Clamp(yScrollOffset, -cachedScrollLimit, 0f); + yScrollOffset = Smooth(yScrollOffset, clampedTarget, ScrollClampSpeed, deltaTime, ScrollEpsilon); - toggleHomeMenuShadow.IsEnabled = enabled; - if (!enabled) - { - toggleHomeMenuShadow.IsChecked = false; - Settings.ToggleHomeMenuShadow = false; - } - }, - UIAlignment.TopLeft); - toggleIslandShadow.IsChecked = Settings.ToggleIslandShadow; - toggleIslandShadow.Anchor.X = 0; - objects.Add(toggleIslandShadow); + float previousSmooth = ySmoothScroll; + ySmoothScroll = Smooth(ySmoothScroll, yScrollOffset, ScrollSmoothSpeed, deltaTime, ScrollEpsilon); + if (Math.Abs(previousSmooth - ySmoothScroll) > 0.001f) + layoutDirty = true; - toggleHomeMenuShadow = new DWCheckbox( - island, - "Toggle home menu shadow when idle", - new Vec2(65, 0), - new Vec2(25, 25), - () => { }, - UIAlignment.TopLeft); - toggleHomeMenuShadow.IsChecked = Settings.ToggleHomeMenuShadow; - toggleHomeMenuShadow.Anchor.X = 0; - objects.Add(toggleHomeMenuShadow); + if (layoutDirty) + LayoutContent(); + } - bool enableIslandShadowSubSettings = toggleIslandShadow.IsChecked; + public override void OnDispose() + { + MainForm.onScrollEvent -= scrollHandler; + base.OnDispose(); + } - toggleHomeMenuShadow.IsEnabled = enableIslandShadowSubSettings; + public override Vec2 IslandSize() + { + var vec = new Vec2(525, 425); - runOnStartup = new DWCheckbox(island, "Start application on login", new Vec2(25, 0), new Vec2(25, 25), () => { }, UIAlignment.TopLeft); - runOnStartup.IsChecked = Settings.RunOnStartup; - runOnStartup.Anchor.X = 0; - objects.Add(runOnStartup); + if (smallWidgetAdder != null) + vec.X = Math.Max(vec.X, smallWidgetAdder.Size.X + 50); - allowAutomaticUpdates = new DWCheckbox(island, "Allow automatic updates", new Vec2(25, 0), new Vec2(25, 25), () => { }, UIAlignment.TopLeft); - allowAutomaticUpdates.IsChecked = Settings.AllowAutomaticUpdates; - allowAutomaticUpdates.Anchor.X = 0; - objects.Add(allowAutomaticUpdates); + return vec; + } - { - var selectedMonitorTitle = new DWText(island, "Selected Monitor", new Vec2(25, 0), UIAlignment.TopLeft); - selectedMonitorTitle.Font = Res.SFProBold; - selectedMonitorTitle.TextSize = 15; - selectedMonitorTitle.Anchor.X = 0; - objects.Add(selectedMonitorTitle); + public override Vec2 IslandSizeBig() + { + return IslandSize() + 5; + } - // Get current monitor count and names - int monitorCount = MainForm.GetMonitorCount(); - var selectedMonitors = new string[monitorCount]; + public override Col IslandBorderColor() + { + return Settings.IslandMode == IslandObject.IslandMode.Island + ? new Col(0.5f, 0.5f, 0.5f) + : Col.Transparent; + } - for (int i = 0; i < monitorCount; i++) - { - // Get the specific monitor to show resolution/friendly name if possible - var screen = System.Windows.Forms.Screen.AllScreens[i]; - string monitorLabel = (i == 0) ? "Primary" : $"Monitor {i + 1}"; - selectedMonitors[i] = $"{monitorLabel} ({screen.Bounds.Width}x{screen.Bounds.Height})"; - } + public static List LoadCustomOptions() + { + if (cachedCustomOptions != null) + return cachedCustomOptions; - var selectedMonitor = new DWMultiSelectionButton(island, selectedMonitors, new Vec2(25, 0), new Vec2(IslandSize().X - 50, 25), UIAlignment.TopLeft); + cachedCustomOptions = new List(); - // Clamp the index to ensure it doesn't crash if a monitor was unplugged - selectedMonitor.SelectedIndex = Math.Clamp(Settings.ScreenIndex, 0, monitorCount - 1); - selectedMonitor.Anchor.X = 0; + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + AddRegisterableSettingsFromAssembly(assembly, cachedCustomOptions); - selectedMonitor.onClick += (index) => - { - Settings.ScreenIndex = index; - // Immediate preview: move the island to the selected monitor - if (MainForm.Instance != null) - { - MainForm.Instance.SetMonitor(index); - } - }; - objects.Add(selectedMonitor); + string dirPath = Path.Combine(SaveManager.SavePath, "Extensions"); + if (!Directory.Exists(dirPath)) + { + Directory.CreateDirectory(dirPath); + return cachedCustomOptions; } + foreach (string file in Directory.GetFiles(dirPath, "*.dll")) { - var bigMenuModeTitle = new DWText(island, "Default Big Menu Mode", new Vec2(25, 0), UIAlignment.TopLeft); - bigMenuModeTitle.Font = Res.SFProBold; - bigMenuModeTitle.TextSize = 15; - bigMenuModeTitle.Anchor.X = 0; - objects.Add(bigMenuModeTitle); - var bigMenuModes = new string[] { "Widgets", "Tray", "Media" }; - bigMenuModeSelector = new DWMultiSelectionButton(island, bigMenuModes, new Vec2(25, 0), new Vec2(IslandSize().X - 50, 25), UIAlignment.TopLeft); - bigMenuModeSelector.SelectedIndex = Settings.DefaultBigMenuMode switch + try { - HomeMenu.BigMenuMode.Widgets => 0, - HomeMenu.BigMenuMode.Tray => 1, - HomeMenu.BigMenuMode.Media => 2, - _ => 0 - }; - bigMenuModeSelector.Anchor.X = 0; - bigMenuModeSelector.onClick += (index) => + AddRegisterableSettingsFromAssembly(Assembly.LoadFile(file), cachedCustomOptions); + } + catch (Exception ex) { - Settings.DefaultBigMenuMode = index switch - { - 0 => HomeMenu.BigMenuMode.Widgets, - 1 => HomeMenu.BigMenuMode.Tray, - 2 => HomeMenu.BigMenuMode.Media, - _ => HomeMenu.BigMenuMode.Widgets - }; - }; - objects.Add(bigMenuModeSelector); + System.Diagnostics.Debug.WriteLine($"SettingsMenu: failed to load extension settings from {file}: {ex.Message}"); + } } - { - var themeTitle = new DWText(island, "Themes", new Vec2(25, 0), UIAlignment.TopLeft); - themeTitle.Font = Res.SFProBold; - themeTitle.TextSize = 15; - themeTitle.Anchor.X = 0; - objects.Add(themeTitle); + return cachedCustomOptions; + } + + private void OnScroll(MouseWheelEventArgs e) + { + if (!ReferenceEquals(MenuManager.Instance?.ActiveMenu, this)) + return; - var themeOptions = new string[] { "Custom", "Dark", "Light", "Candy", "Forest Dawn", "Sunset Glow" }; - var theme = new DWMultiSelectionButton(island, themeOptions, new Vec2(25, 0), new Vec2(IslandSize().X - 50, 25), UIAlignment.TopLeft); - theme.SelectedIndex = Settings.Theme + 1; - theme.Anchor.X = 0; - theme.onClick += (index) => + yScrollOffset += e.Delta * ScrollSpeed; + yScrollOffset = Mathf.Clamp(yScrollOffset, -cachedScrollLimit, 0f); + layoutDirty = true; + } + + private void SaveAndBack() + { + Settings.AllowBlur = allowBlur.IsChecked; + Settings.AllowAnimation = allowAnimation.IsChecked; + Settings.AntiAliasing = antiAliasing.IsChecked; + Settings.ToggleHighRefreshRate = toggleHighRefreshRate.IsChecked; + Settings.LimitRefreshRateWhenIdle = toggleHighRefreshRate.IsChecked && limitRefreshRateWhenIdle.IsChecked; + Settings.ToggleIslandShadow = toggleIslandShadow.IsChecked; + Settings.ToggleHomeMenuShadow = toggleIslandShadow.IsChecked && toggleHomeMenuShadow.IsChecked; + Settings.RunOnStartup = runOnStartup.IsChecked; + Settings.AllowAutomaticUpdates = allowAutomaticUpdates.IsChecked; + Settings.AlwaysTopmost = alwaysTopmost.IsChecked; + Settings.ReduceWorkingArea = reduceWorkingArea.IsChecked; + + if (bigMenuModeSelector != null) + { + Settings.DefaultBigMenuMode = bigMenuModeSelector.SelectedIndex switch { - Settings.Theme = index - 1; - changedTheme = true; + 1 => HomeMenu.BigMenuMode.Tray, + 2 => HomeMenu.BigMenuMode.Media, + _ => HomeMenu.BigMenuMode.Widgets }; - objects.Add(theme); } - objects.Add(new DWText(island, " ", new Vec2(0, 0)) - { - TextSize = 2 - }); + foreach (var item in LoadCustomOptions()) + item.SaveSettings(); - var widgetsTitle = new DWText(island, "Widgets", new Vec2(25, 0), UIAlignment.TopLeft); - widgetsTitle.Font = Res.SFProBold; - widgetsTitle.Color = Theme.TextMain; - widgetsTitle.Anchor.X = 0; - objects.Add(widgetsTitle); + DynamicWinMain.UpdateStartup(); + if (changedTheme) { - var wTitle = new DWText(island, "Small widgets (right click to add/edit)", new Vec2(25, 0), UIAlignment.TopLeft); - wTitle.Font = Res.SFProBold; - wTitle.Color = Theme.TextMain; - wTitle.TextSize = 15; - wTitle.Anchor.X = 0; - objects.Add(wTitle); - - smallWidgetAdder = new SmallWidgetAdder(island, Vec2.zero, new Vec2(IslandSize().X - 50, 35), UIAlignment.TopCenter); - objects.Add(smallWidgetAdder); + Theme.Instance.UpdateTheme(true); } - + else { - var wTitle = new DWText(island, "Big widgets (right click to add/edit)", new Vec2(25, 15), UIAlignment.TopLeft); - wTitle.Font = Res.SFProBold; - wTitle.Color = Theme.TextMain; - wTitle.TextSize = 15; - wTitle.Anchor.X = 0; - objects.Add(wTitle); - - bigWidgetAdder = new BigWidgetAdder(island, Vec2.zero, new Vec2(IslandSize().X - 50, 35), UIAlignment.TopCenter); - objects.Add(bigWidgetAdder); + Res.HomeMenu = new HomeMenu(); + MenuManager.OpenMenu(Res.HomeMenu); } - objects.Add(new DWText(island, " ", new Vec2(25, 0), UIAlignment.TopLeft) - { - Color = Theme.TextThird, - Anchor = new Vec2(0, 0.5f), - TextSize = 20 - }); + Settings.Save(); + } - var widgetOptionsTitle = new DWText(island, "Widget Settings", new Vec2(25, 0), UIAlignment.TopLeft); - widgetOptionsTitle.Font = Res.SFProBold; - widgetOptionsTitle.Color = Theme.TextMain; - widgetOptionsTitle.Anchor.X = 0; - objects.Add(widgetOptionsTitle); + private void AddMonitorSelector(List objects, IslandObject island) + { + AddSectionTitle(objects, island, "Selected Monitor"); - { - foreach (var option in _cachedCustomOptions) - { - var wTitle = new DWText(island, option.SettingTitle, new Vec2(25, 0), UIAlignment.TopLeft); - wTitle.Font = Res.SFProBold; - wTitle.TextSize = 15; - wTitle.Anchor.X = 0; - objects.Add(wTitle); + int monitorCount = Math.Max(1, MainForm.GetMonitorCount()); + var selectedMonitors = new string[monitorCount]; - foreach (var optionItem in option.SettingsObjects()) - { - optionItem.Parent = island; - - if (optionItem.alignment == UIAlignment.TopLeft) - { - optionItem.Position = new Vec2(25, 0); - optionItem.Anchor.X = 0; - } - - if (optionItem is DWText) - { - ((DWText)optionItem).Color = Theme.TextMain; - ((DWText)optionItem).Font = Res.SFProRegular; - ((DWText)optionItem).TextSize = 13; - } - else if (optionItem is DWCheckbox) - { - optionItem.Size = new Vec2(25, 25); - } - - objects.Add(optionItem); - } - } + for (int i = 0; i < monitorCount; i++) + { + var screen = System.Windows.Forms.Screen.AllScreens[Math.Min(i, System.Windows.Forms.Screen.AllScreens.Length - 1)]; + string monitorLabel = i == 0 ? "Primary" : $"Monitor {i + 1}"; + selectedMonitors[i] = $"{monitorLabel} ({screen.Bounds.Width}x{screen.Bounds.Height})"; } - var releaseStreamTitle = new DWText(island, "Release Stream", new Vec2(25, 0), UIAlignment.TopLeft); - releaseStreamTitle.Font = Res.SFProBold; - releaseStreamTitle.Color = Theme.TextMain; - releaseStreamTitle.Anchor.X = 0; - objects.Add(releaseStreamTitle); - - var releaseStreamDisclaimerPt1 = new DWText(island, "Updates will be checked after you restart the application", new Vec2(25, -15), UIAlignment.TopLeft); - releaseStreamDisclaimerPt1.Font = Res.SFProRegular; - releaseStreamDisclaimerPt1.TextSize = 12; - releaseStreamDisclaimerPt1.Color = Theme.TextSecond; - releaseStreamDisclaimerPt1.Anchor.X = 0; - objects.Add(releaseStreamDisclaimerPt1); - - var releaseStreamDisclaimerPt2 = new DWText(island, "or by pressing the 'Check for updates now' button.", new Vec2(25, -30), UIAlignment.TopLeft); - releaseStreamDisclaimerPt2.Font = Res.SFProRegular; - releaseStreamDisclaimerPt2.TextSize = 12; - releaseStreamDisclaimerPt2.Color = Theme.TextSecond; - releaseStreamDisclaimerPt2.Anchor.X = 0; - objects.Add(releaseStreamDisclaimerPt2); - { - var releaseStreams = new string[] { "Release", "Canary" }; - var releaseStream = new DWMultiSelectionButton(island, releaseStreams, new Vec2(25, -25), new Vec2(IslandSize().X - 50, 30), UIAlignment.TopLeft); - releaseStream.SelectedIndex = Settings.ReleaseStream; - releaseStream.Anchor.X = 0; - releaseStream.onClick += (index) => + var selectedMonitor = AddSelector(objects, island, selectedMonitors); + selectedMonitor.SelectedIndex = Math.Clamp(Settings.ScreenIndex, 0, monitorCount - 1); + selectedMonitor.onClick += index => + { + Settings.ScreenIndex = index; + MainForm.Instance?.SetMonitor(index); + }; + } + + private void AddDefaultBigMenuSelector(List objects, IslandObject island) + { + AddSectionTitle(objects, island, "Default Big Menu Mode"); + + bigMenuModeSelector = AddSelector(objects, island, new[] { "Widgets", "Tray", "Media" }); + bigMenuModeSelector.SelectedIndex = Settings.DefaultBigMenuMode switch + { + HomeMenu.BigMenuMode.Tray => 1, + HomeMenu.BigMenuMode.Media => 2, + _ => 0 + }; + bigMenuModeSelector.onClick += index => + { + Settings.DefaultBigMenuMode = index switch { - Settings.ReleaseStream = index; - Settings.Save(); + 1 => HomeMenu.BigMenuMode.Tray, + 2 => HomeMenu.BigMenuMode.Media, + _ => HomeMenu.BigMenuMode.Widgets }; - objects.Add(releaseStream); - } + }; + } + + private void AddThemeSelector(List objects, IslandObject island) + { + AddSectionTitle(objects, island, "Themes"); - // Trigger update logic immediately upon pressing the update button - var checkForUpdateBtn = new DWTextButton(island, "Check for updates now", new Vec2(25, -25), new Vec2(IslandSize().X - 360, 30), () => + var theme = AddSelector(objects, island, new[] { "Custom", "Dark", "Light", "Candy", "Forest Dawn", "Sunset Glow" }); + theme.SelectedIndex = Settings.Theme + 1; + theme.onClick += index => { - SaveManager.Add("settings.ReleaseStream", Settings.ReleaseStream); + Settings.Theme = index - 1; + changedTheme = true; + }; + } - // Show overlay immediately on UI thread - MenuManager.OpenOverlayMenu(new UpdaterOverlay(), 0f); + private void AddCustomOptions(List objects, IslandObject island, List customOptions) + { + foreach (var option in customOptions) + { + AddSectionTitle(objects, island, option.SettingTitle); - // Perform check in background - _ = Task.Run(async () => + foreach (var optionItem in option.SettingsObjects()) { - var updater = new Updater(); - AppVersion? update = null; - try { update = await updater.CheckForUpdate(); } catch { update = null; } + optionItem.Parent = island; - // Back to UI thread to update menus - System.Windows.Application.Current?.Dispatcher.Invoke(() => + if (optionItem.alignment == UIAlignment.TopLeft) { - MenuManager.CloseOverlay(); - MenuManager.Instance?.UnlockMenu(); - - if (update == null) - { - MenuManager.OpenMenu(Res.HomeMenu); - } - else - { - MenuManager.OpenMenu(new UpdaterMenu(update)); - } - }); - }); - }, UIAlignment.TopLeft); - checkForUpdateBtn.Anchor.X = 0; - objects.Add(checkForUpdateBtn); + optionItem.Position = new Vec2(ContentLeft, 0); + optionItem.Anchor.X = 0; + } - objects.Add(new DWText(island, $"Application version: {DynamicWinMain.Version} ({DynamicWinMain.ReleaseStream.ToFriendlyString()})", new Vec2(25, -15), UIAlignment.TopLeft) - { - Color = Theme.TextMain, - Anchor = new Vec2(0, 0), - TextSize = 15, - Font = Res.SFProBold - }); + if (optionItem is DWText text) + { + text.Color = Theme.TextSecond; + text.Font = Res.SFProRegular; + text.TextSize = 12; + text.Size = text.GetBoundsForString(text.Text); + } + else if (optionItem is DWCheckbox) + { + optionItem.Size = CheckboxSize; + } - objects.Add(new DWText(island, $"Software architecture: {DynamicWinMain.ProcessArchitecture.ToString().ToLower()}", new Vec2(25, -25), UIAlignment.TopLeft) - { - Color = Theme.TextMain, - Anchor = new Vec2(0, 0), - TextSize = 13, - Font = Res.SFProBold - }); + AddContent(objects, optionItem); + } + } + } - objects.Add(new DWText(island, "Maintained and developed by 59xa", new Vec2(25, -25), UIAlignment.TopLeft) - { - Color = Theme.TextThird, - Anchor = new Vec2(0, 0.5f), - TextSize = 13, - }); + private void AddReleaseStream(List objects, IslandObject island) + { + AddSectionTitle(objects, island, "Release Stream"); + AddBodyText(objects, island, "Updates will be checked after you restart the application", topPadding: -15f); + AddBodyText(objects, island, "or by pressing the 'Check for updates now' button.", topPadding: -15f); - objects.Add(new DWText(island, "Created by Florian Butz", new Vec2(25, -25), UIAlignment.TopLeft) + var releaseStream = AddSelector(objects, island, new[] { "Release", "Canary" }); + releaseStream.SelectedIndex = Settings.ReleaseStream; + releaseStream.onClick += index => { - Color = Theme.TextThird, - Anchor = new Vec2(0, 0.5f), - TextSize = 13 - }); + Settings.ReleaseStream = index; + Settings.Save(); + }; - objects.Add(new DWText(island, "Licenced under CC BY-SA 4.0", new Vec2(25, -25), UIAlignment.TopLeft) - { - Color = Theme.TextThird, - Anchor = new Vec2(0, 0.5f), - TextSize = 13 - }); + var checkForUpdateBtn = new DWTextButton( + island, + "Check for updates now", + new Vec2(ContentLeft, 0), + new Vec2(IslandSize().X - 360, 32), + CheckForUpdate, + UIAlignment.TopLeft); + checkForUpdateBtn.Anchor.X = 0; + AddContent(objects, checkForUpdateBtn); + } + + private void AddVersionInfo(List objects, IslandObject island) + { + AddText(objects, island, $"Application version: {DynamicWinMain.Version} ({DynamicWinMain.ReleaseStream.ToFriendlyString()})", 15, Res.SFProBold, Theme.TextMain); + AddText(objects, island, $"Software architecture: {DynamicWinMain.ProcessArchitecture.ToString().ToLower()}", 13, Res.SFProBold, Theme.TextMain, topPadding: -10f); + AddText(objects, island, "Maintained and developed by 59xa", 13, Res.SFProRegular, Theme.TextThird, topPadding: -10f); + AddText(objects, island, "Created by Florian Butz", 13, Res.SFProRegular, Theme.TextThird, topPadding: -10f); + AddText(objects, island, "Licenced under CC BY-SA 4.0", 13, Res.SFProRegular, Theme.TextThird, topPadding: -10f); + } - var backBtn = new DWTextButton(island, "Save changes", new Vec2(0, -45), new Vec2(250, 40), () => { SaveAndBack(); }, UIAlignment.BottomCenter) + private void AddSaveButton(List objects, IslandObject island) + { + var backBtn = new DWTextButton(island, "Save changes", new Vec2(0, -45), new Vec2(250, 40), SaveAndBack, UIAlignment.BottomCenter) { roundRadius = 25 }; @@ -606,142 +461,295 @@ public override List InitializeMenu(IslandObject island) objects.Add(bottomMask); objects.Add(backBtn); + } - return objects; + private void CheckForUpdate() + { + SaveManager.Add("settings.ReleaseStream", Settings.ReleaseStream); + MenuManager.OpenOverlayMenu(new UpdaterOverlay(), 0f); + + _ = Task.Run(async () => + { + var updater = new Updater(); + AppVersion? update = null; + + try + { + update = await updater.CheckForUpdate(); + } + catch + { + update = null; + } + + System.Windows.Application.Current?.Dispatcher.Invoke(() => + { + MenuManager.CloseOverlay(); + MenuManager.Instance?.UnlockMenu(); + + if (update == null) + MenuManager.OpenMenu(Res.HomeMenu); + else + MenuManager.OpenMenu(new UpdaterMenu(update)); + }); + }); + } + + private SettingsCheckbox AddCheckbox( + List objects, + IslandObject island, + string text, + bool isChecked, + Action? onChanged = null, + float left = ContentLeft) + { + var checkbox = new SettingsCheckbox(island, text, new Vec2(left, 0), CheckboxSize, onChanged, UIAlignment.TopLeft) + { + Anchor = new Vec2(0, 0.5f) + }; + checkbox.IsChecked = isChecked; + AddContent(objects, checkbox); + return checkbox; } - SmallWidgetAdder smallWidgetAdder; - BigWidgetAdder bigWidgetAdder; + private DWMultiSelectionButton AddSelector( + List objects, + IslandObject island, + string[] options, + float left = ContentLeft, + float height = 32f) + { + var selector = new DWMultiSelectionButton( + island, + options, + new Vec2(left, 0), + new Vec2(IslandSize().X - left * 2f, height), + UIAlignment.TopLeft); - float yScrollOffset = 0f; - float ySmoothScroll = 0f; - float cachedScrollLimit = 0f; - float lastLayoutScroll = float.NaN; - float lastBigWidgetAdderHeight = float.NaN; - float lastSmallWidgetAdderWidth = float.NaN; - int lastLayoutObjectCount = -1; + selector.Anchor.X = 0; + AddContent(objects, selector); + return selector; + } - public override void Update() + private DWText AddTitle(List objects, IslandObject island, string text) { - base.Update(); + return AddText(objects, island, text, 24, Res.SFProBold, Theme.TextMain, bottomAdjustment: TitleBottomTuck); + } - ySmoothScroll = Mathf.Lerp(ySmoothScroll, - yScrollOffset, 10f * RendererMain.Instance.DeltaTime); + private DWText AddSectionTitle(List objects, IslandObject island, string text, float left = ContentLeft, float topPadding = 0f) + { + return AddText(objects, island, text, 15, Res.SFProBold, Theme.TextMain, left, topPadding, SectionTitleBottomTuck); + } - bottomMask.blurAmount = 15; + private DWText AddBodyText(List objects, IslandObject island, string text, float left = ContentLeft, float topPadding = 0f) + { + return AddText(objects, island, text, 12, Res.SFProRegular, Theme.TextSecond, left, topPadding); + } - bool layoutDirty = - float.IsNaN(lastLayoutScroll) || - Math.Abs(ySmoothScroll - lastLayoutScroll) > 0.05f || - lastLayoutObjectCount != UiObjects.Count || - (bigWidgetAdder != null && Math.Abs(bigWidgetAdder.Size.Y - lastBigWidgetAdderHeight) > 0.05f) || - (smallWidgetAdder != null && Math.Abs(smallWidgetAdder.Size.X - lastSmallWidgetAdderWidth) > 0.05f); + private DWText AddText( + List objects, + IslandObject island, + string text, + float textSize, + SKTypeface font, + Col color, + float left = ContentLeft, + float topPadding = 0f, + float bottomAdjustment = 0f) + { + if (topPadding > 0.001f) + AddGap(objects, island, topPadding); - if (layoutDirty) + var label = new DWText(island, text, new Vec2(left, 0), UIAlignment.TopLeft) { - var yScrollLim = 0f; - var yPos = 35f; - var spacing = 15f; - - for (int i = 0; i < UiObjects.Count - 2; i++) - { - var uiObject = UiObjects[i]; - if (!uiObject.IsEnabled) continue; + Anchor = new Vec2(0, 0), + Color = color, + Font = font, + TextSize = textSize + }; + label.Size = label.GetBoundsForString(text); - uiObject.LocalPosition.Y = yPos + ySmoothScroll; - yPos += uiObject.Size.Y + spacing; + AddContent(objects, label); + if (Math.Abs(bottomAdjustment) > 0.001f) + layoutBottomAdjustments[label] = bottomAdjustment; - if (yPos > IslandSize().Y - 50) yScrollLim += uiObject.Size.Y + spacing; - } + return label; + } - cachedScrollLimit = yScrollLim; - lastLayoutScroll = ySmoothScroll; - lastLayoutObjectCount = UiObjects.Count; - lastBigWidgetAdderHeight = bigWidgetAdder?.Size.Y ?? 0f; - lastSmallWidgetAdderWidth = smallWidgetAdder?.Size.X ?? 0f; - } + private void AddGap(List objects, IslandObject island, float height) + { + AddContent(objects, new SettingsSpacer(island, height)); + } - yScrollOffset = Mathf.Lerp(yScrollOffset, - Mathf.Clamp(yScrollOffset, -cachedScrollLimit, 0f), 15f * RendererMain.Instance.DeltaTime); + private void AddContent(List objects, UIObject obj) + { + contentObjects.Add(obj); + objects.Add(obj); } - public override void OnDispose() + private void SetRefreshRateSubSettingsEnabled(bool enabled, bool immediate) { - MainForm.onScrollEvent -= scrollHandler; - base.OnDispose(); + SetObjectEnabled(limitRefreshRateWhenIdle, enabled, immediate); + SetObjectEnabled(limitRefreshRateDisclaimer1, enabled, immediate); + SetObjectEnabled(limitRefreshRateDisclaimer2, enabled, immediate); + + if (!enabled && limitRefreshRateWhenIdle != null) + limitRefreshRateWhenIdle.IsChecked = false; + + layoutDirty = true; } - public override Vec2 IslandSize() + private void SetHomeMenuShadowEnabled(bool enabled, bool immediate) { - var vec = new Vec2(525, 425); + SetObjectEnabled(toggleHomeMenuShadow, enabled, immediate); - if (smallWidgetAdder != null) - { - vec.X = Math.Max(vec.X, smallWidgetAdder.Size.X + 50); - } + if (!enabled && toggleHomeMenuShadow != null) + toggleHomeMenuShadow.IsChecked = false; - return vec; + layoutDirty = true; } - public override Vec2 IslandSizeBig() + private static void SetObjectEnabled(UIObject? obj, bool enabled, bool immediate) { - return IslandSize() + 5; + if (obj == null) + return; + + if (immediate) + obj.SilentSetActive(enabled); + else + obj.IsEnabled = enabled; } - public static List LoadCustomOptions() + private bool NeedsRealtimeLayout() + { + return layoutDirty + || Math.Abs(yScrollOffset - Mathf.Clamp(yScrollOffset, -cachedScrollLimit, 0f)) > ScrollEpsilon + || Math.Abs(ySmoothScroll - yScrollOffset) > ScrollEpsilon; + } + + private void LayoutContent() { - if (_cachedCustomOptions != null) return _cachedCustomOptions; + float y = ContentTop; - _cachedCustomOptions = new List(); + foreach (var uiObject in contentObjects) + { + if (uiObject == null || !uiObject.IsEnabled) + continue; - var registerableSettings = AppDomain.CurrentDomain.GetAssemblies() - .SelectMany(s => s.GetTypes()) - .Where(p => typeof(IRegisterableSetting).IsAssignableFrom(p) && p.IsClass); + uiObject.LocalPosition.Y = y + ySmoothScroll; + y += Math.Max(1f, uiObject.Size.Y) + ContentSpacing + GetLayoutBottomAdjustment(uiObject); + } - foreach (var option in registerableSettings) + float contentHeight = Math.Max(0f, y - ContentTop - ContentSpacing); + float visibleHeight = Math.Max(1f, IslandSize().Y - ContentTop - BottomReserve); + cachedScrollLimit = Math.Max(0f, contentHeight - visibleHeight); + + if (cachedScrollLimit <= 0f) { - var optionInstance = (IRegisterableSetting)Activator.CreateInstance(option); - _cachedCustomOptions.Add(optionInstance); + yScrollOffset = 0f; + ySmoothScroll = 0f; } - // Loading in custom DLLs + layoutDirty = false; + } - var dirPath = Path.Combine(SaveManager.SavePath, "Extensions"); + private float BuildContentSignature() + { + float signature = contentObjects.Count * 19f; - if (!Directory.Exists(dirPath)) + foreach (var uiObject in contentObjects) { - Directory.CreateDirectory(dirPath); + if (uiObject == null || !uiObject.IsEnabled) + continue; + + signature += uiObject.Size.X * 0.013f + uiObject.Size.Y * 0.37f; + signature += GetLayoutBottomAdjustment(uiObject) * 0.11f; } - else + + return signature; + } + + private float GetLayoutBottomAdjustment(UIObject uiObject) + { + return layoutBottomAdjustments.TryGetValue(uiObject, out float adjustment) ? adjustment : 0f; + } + + private static float Smooth(float current, float target, float speed, float deltaTime, float epsilon) + { + if (Math.Abs(current - target) <= epsilon) + return target; + + return Mathf.Lerp(current, target, speed * deltaTime); + } + + private static void AddRegisterableSettingsFromAssembly(Assembly assembly, List target) + { + foreach (var option in GetLoadableTypes(assembly)) { - foreach (var file in Directory.GetFiles(dirPath)) + if (!typeof(IRegisterableSetting).IsAssignableFrom(option) || !option.IsClass || option.IsAbstract) + continue; + + try { - if (Path.GetExtension(file).ToLower().Equals(".dll")) - { - System.Diagnostics.Debug.WriteLine(file); - var DLL = Assembly.LoadFile(Path.Combine(dirPath, file)); + if (Activator.CreateInstance(option) is IRegisterableSetting optionInstance) + target.Add(optionInstance); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"SettingsMenu: failed to create setting {option.FullName}: {ex.Message}"); + } + } + } - var dllRegisterableSettings = DLL.GetTypes() - .Where(p => typeof(IRegisterableSetting).IsAssignableFrom(p) && p.IsClass); + private static IEnumerable GetLoadableTypes(Assembly assembly) + { + try + { + return assembly.GetTypes(); + } + catch (ReflectionTypeLoadException ex) + { + return ex.Types.OfType(); + } + catch + { + return Enumerable.Empty(); + } + } - foreach (var option in dllRegisterableSettings) - { - var optionInstance = (IRegisterableSetting)Activator.CreateInstance(option); - _cachedCustomOptions.Add(optionInstance); - } - } - } + private sealed class SettingsSpacer : UIObject + { + public SettingsSpacer(UIObject? parent, float height) + : base(parent, Vec2.zero, new Vec2(1, Math.Max(1f, height)), UIAlignment.TopLeft) + { + UseGpuCaching = false; + Color = Col.Transparent; } - return _cachedCustomOptions; + public override void Draw(SKCanvas canvas) + { + } } - // Border should only be rendered if on island mode instead of notch - public override Col IslandBorderColor() + private sealed class SettingsRenderDemand : UIObject { - IslandMode mode = Settings.IslandMode; // Reads either Island or Notch as value - if (mode == IslandMode.Island) return new Col(0.5f, 0.5f, 0.5f); - else return new Col(0, 0, 0, 0); // Render transparent if island mode is Notch + private readonly Func shouldRender; + + public SettingsRenderDemand(UIObject? parent, Func shouldRender) + : base(parent, Vec2.zero, Vec2.one, UIAlignment.TopLeft) + { + this.shouldRender = shouldRender; + UseGpuCaching = false; + Color = Col.Transparent; + maskInToIsland = false; + expandInteractionRect = 0; + } + + public override bool WantsRealtimeUpdate => shouldRender(); + + public override void Draw(SKCanvas canvas) + { + } } } } diff --git a/DynamicWin/UI/Menu/Menus/SettingsMenuObjects/AddNew.cs b/DynamicWin/UI/Menu/Menus/SettingsMenuObjects/AddNew.cs index bee25e6..6714ecd 100644 --- a/DynamicWin/UI/Menu/Menus/SettingsMenuObjects/AddNew.cs +++ b/DynamicWin/UI/Menu/Menus/SettingsMenuObjects/AddNew.cs @@ -1,4 +1,4 @@ -using DynamicWin.Resources; +using DynamicWin.Resources; using DynamicWin.UI.UIElements; using DynamicWin.Utils; using SkiaSharp; @@ -7,26 +7,29 @@ namespace DynamicWin.UI.Menu.Menus.SettingsMenuObjects { internal class AddNew : UIObject { - public AddNew(UIObject? parent, Vec2 position, Vec2 size, UIAlignment alignment = UIAlignment.TopCenter) : base(parent, position, size, alignment) + private static readonly float[] DashIntervals = { 10f, 10f }; + + public AddNew(UIObject? parent, Vec2 position, Vec2 size, UIAlignment alignment = UIAlignment.TopCenter) + : base(parent, position, size, alignment) { + UseGpuCaching = false; + Color = Theme.IconColor.Override(a: 0.4f); + AddLocalObject(new DWImage(this, Res.Add, Vec2.zero, new Vec2(15, 15), UIAlignment.Center) { Color = Theme.IconColor }); - - Color = Theme.IconColor.Override(a: 0.4f); } public override void Draw(SKCanvas canvas) { - var paint = GetPaint(); + using var paint = GetPaint(); + using var dash = SKPathEffect.CreateDash(DashIntervals, 0f); var placeRect = new SKRoundRect(SKRect.Create(Position.X, Position.Y, Size.X, Size.Y), 25); placeRect.Deflate(5, 5); - float[] intervals = { 10, 10 }; - paint.PathEffect = SKPathEffect.CreateDash(intervals, 0f); - + paint.PathEffect = dash; paint.IsStroke = true; paint.StrokeCap = SKStrokeCap.Round; paint.StrokeJoin = SKStrokeJoin.Round; @@ -35,6 +38,7 @@ public override void Draw(SKCanvas canvas) canvas.DrawRoundRect(placeRect, paint); placeRect.Deflate(5f, 5f); + paint.PathEffect = null; paint.Color = Color.Override(a: 0.05f).Value(); paint.IsStroke = false; diff --git a/DynamicWin/UI/Menu/Menus/SettingsMenuObjects/BigWidgetAdder.cs b/DynamicWin/UI/Menu/Menus/SettingsMenuObjects/BigWidgetAdder.cs index 4db5107..61dc291 100644 --- a/DynamicWin/UI/Menu/Menus/SettingsMenuObjects/BigWidgetAdder.cs +++ b/DynamicWin/UI/Menu/Menus/SettingsMenuObjects/BigWidgetAdder.cs @@ -1,4 +1,4 @@ -using DynamicWin.Main; +using DynamicWin.Main; using DynamicWin.Resources; using DynamicWin.UI.Widgets; using DynamicWin.Utils; @@ -8,134 +8,209 @@ namespace DynamicWin.UI.Menu.Menus.SettingsMenuObjects { internal class BigWidgetAdder : UIObject { - AddNew addNew; - - public BigWidgetAdder(UIObject? parent, Vec2 position, Vec2 size, UIAlignment alignment = UIAlignment.TopCenter) : base(parent, position, size, alignment) + private const float RowHeight = 45f; + private const int Columns = 2; + private const float AnimationSpeed = 15f; + private const float Epsilon = 0.05f; + + private readonly AddNew addNew; + private readonly List displays = new List(); + + private float targetHeight; + private float targetAddX; + private float targetAddY; + private float targetAddWidth; + private bool layoutDirty = true; + + public BigWidgetAdder(UIObject? parent, Vec2 position, Vec2 size, UIAlignment alignment = UIAlignment.TopCenter) + : base(parent, position, size, alignment) { + UseGpuCaching = false; Color = Theme.WidgetBackground.Override(a: 0.1f); roundRadius = 20; - Anchor.Y = 0; - addNew = new AddNew(this, Vec2.zero, new Vec2(size.X, 45), UIAlignment.BottomLeft); + addNew = new AddNew(this, Vec2.zero, new Vec2(size.X, RowHeight), UIAlignment.BottomLeft); addNew.Anchor.Y = 0; AddLocalObject(addNew); UpdateWidgetDisplay(); + RecalculateLayout(); + ApplyLayoutTargets(immediate: true); + } + + public override bool WantsRealtimeUpdate + { + get + { + return Math.Abs(addNew.LocalPosition.X - targetAddX) > Epsilon + || Math.Abs(addNew.LocalPosition.Y - targetAddY) > Epsilon + || Math.Abs(addNew.Size.X - targetAddWidth) > Epsilon + || Math.Abs(Size.Y - targetHeight) > Epsilon; + } } public override void Update(float deltaTime) { base.Update(deltaTime); - int line = (int)(Math.Floor(displays.Count / maxE)); + if (layoutDirty) + RecalculateLayout(); - addNew.LocalPosition.Y = Mathf.Lerp(addNew.LocalPosition.Y, -line * 45 - 45, 15f * deltaTime); - addNew.LocalPosition.X = Mathf.Lerp(addNew.LocalPosition.X, isDisplayEven() ? Size.X / 2f : Size.X / 1.3333333f, 15f * deltaTime); - addNew.Size.X = Mathf.Lerp(addNew.Size.X, isDisplayEven() ? Size.X : Size.X / 2, 15f * deltaTime); - - var lines2 = (int)Math.Max(1, (displays.Count / maxE + 1)); - Size.Y = Mathf.Lerp(Size.Y, lines2 * 45, 15f * RendererMain.Instance.DeltaTime); + ApplyLayoutTargets(immediate: false, deltaTime); } - bool isDisplayEven() + public override ContextMenu? GetContextMenu() { - return displays.Count % 2 == 0; - } - - List displays = new List(); - float maxE = 2; + var ctx = new ContextMenu(); + bool anyWidgetsLeft = false; - void UpdateWidgetDisplay() - { - displays.ForEach((x) => DestroyLocalObject(x)); - displays.Clear(); + foreach (var availableWidget in Res.availableBigWidgets) + { + string? fullName = availableWidget.GetType().FullName; + if (string.IsNullOrEmpty(fullName) || Settings.bigWidgets.Contains(fullName)) + continue; - Dictionary bigWidgets = new Dictionary(); + anyWidgetsLeft = true; + var item = new MenuItem() { Header = $"{GetWidgetSourceName(availableWidget)}: {availableWidget.WidgetName}" }; + item.Click += (x, y) => + { + Settings.bigWidgets.Add(fullName); + UpdateWidgetDisplay(); + }; - foreach (var widget in Res.availableBigWidgets) - { - if (bigWidgets.ContainsKey(widget.GetType().FullName)) continue; - bigWidgets.Add(widget.GetType().FullName, widget); - System.Diagnostics.Debug.WriteLine(widget.GetType().FullName); + ctx.Items.Add(item); } - int c = 0; - foreach (var bigWidget in Settings.bigWidgets) + if (anyWidgetsLeft) + return ctx; + + var empty = new ContextMenu(); + empty.Items.Add(new MenuItem() { - if (!bigWidgets.ContainsKey(bigWidget)) continue; + Header = "No widgets available.", + IsEnabled = false + }); + return empty; + } - var widget = bigWidgets[bigWidget.ToString()]; + private void UpdateWidgetDisplay() + { + for (int i = displays.Count - 1; i >= 0; i--) + DestroyLocalObject(displays[i]); + displays.Clear(); + + var bigWidgets = Res.availableBigWidgets + .Where(widget => !string.IsNullOrEmpty(widget.GetType().FullName)) + .GroupBy(widget => widget.GetType().FullName!) + .ToDictionary(group => group.Key, group => group.First()); + + for (int i = 0; i < Settings.bigWidgets.Count; i++) + { + string bigWidget = Settings.bigWidgets[i]; + if (!bigWidgets.TryGetValue(bigWidget, out var widget)) + continue; + + string capturedWidget = bigWidget; var display = new BigWidgetAdderDisplay(this, widget.WidgetName, UIAlignment.BottomLeft); - display.onEditRemoveWidget += () => { - Settings.bigWidgets.Remove(bigWidget); + display.onEditRemoveWidget += () => + { + Settings.bigWidgets.Remove(capturedWidget); UpdateWidgetDisplay(); }; - display.onEditMoveWidgetRight += () => { - int index = Math.Clamp(Settings.bigWidgets.IndexOf(bigWidget) + 1, 0, Settings.bigWidgets.Count - 1); - Settings.bigWidgets.Remove(bigWidget); - - Settings.bigWidgets.Insert(index, bigWidget); + display.onEditMoveWidgetRight += () => + { + MoveWidget(capturedWidget, 1); UpdateWidgetDisplay(); }; - display.onEditMoveWidgetLeft += () => { - int index = Math.Clamp(Settings.bigWidgets.IndexOf(bigWidget) - 1, 0, Settings.bigWidgets.Count - 1); - Settings.bigWidgets.Remove(bigWidget); - - Settings.bigWidgets.Insert(index, bigWidget); + display.onEditMoveWidgetLeft += () => + { + MoveWidget(capturedWidget, -1); UpdateWidgetDisplay(); }; - int line = (int)(c / maxE); - - display.LocalPosition.X = (c % 2) * Size.X / 2; - display.LocalPosition.Y -= 45 + line * 45; - displays.Add(display); AddLocalObject(display); - - c++; } + + layoutDirty = true; } - public override ContextMenu? GetContextMenu() + private void RecalculateLayout() { - var ctx = new System.Windows.Controls.ContextMenu(); - bool anyWidgetsLeft = false; - - foreach (var availableWidget in Res.availableBigWidgets) + for (int i = 0; i < displays.Count; i++) { - if (Settings.bigWidgets.Contains(availableWidget.GetType().FullName)) continue; + int row = i / Columns; + int column = i % Columns; - anyWidgetsLeft = true; + var display = displays[i]; + display.Size = new Vec2(Size.X / Columns, RowHeight); + display.LocalPosition.X = column * Size.X / Columns; + display.LocalPosition.Y = -RowHeight - row * RowHeight; + } - var item = new MenuItem() { Header = availableWidget.GetType().Namespace.Split('.')[0] + ": " + availableWidget.WidgetName }; - item.Click += (x, y) => - { - Settings.bigWidgets.Add(availableWidget.GetType().FullName); - UpdateWidgetDisplay(); - }; + int addRow = displays.Count / Columns; + bool evenDisplayCount = displays.Count % Columns == 0; - ctx.Items.Add(item); - } + targetAddY = -RowHeight - addRow * RowHeight; + targetAddX = evenDisplayCount ? Size.X / 2f : Size.X * 0.75f; + targetAddWidth = evenDisplayCount ? Size.X : Size.X / Columns; + targetHeight = Math.Max(RowHeight, (addRow + 1) * RowHeight); - if (!anyWidgetsLeft) + layoutDirty = false; + } + + private void ApplyLayoutTargets(bool immediate, float deltaTime = 0f) + { + if (immediate) { - var ctx2 = new ContextMenu(); - ctx2.Items.Add(new MenuItem() - { - Header = "No widgets available.", - IsEnabled = false - }); - return ctx2; + addNew.LocalPosition.X = targetAddX; + addNew.LocalPosition.Y = targetAddY; + addNew.Size = new Vec2(targetAddWidth, RowHeight); + Size = new Vec2(Size.X, targetHeight); + return; } - return ctx; + addNew.LocalPosition.X = Smooth(addNew.LocalPosition.X, targetAddX, AnimationSpeed, deltaTime, Epsilon); + addNew.LocalPosition.Y = Smooth(addNew.LocalPosition.Y, targetAddY, AnimationSpeed, deltaTime, Epsilon); + addNew.Size = new Vec2(Smooth(addNew.Size.X, targetAddWidth, AnimationSpeed, deltaTime, Epsilon), RowHeight); + Size = new Vec2(Size.X, Smooth(Size.Y, targetHeight, AnimationSpeed, deltaTime, Epsilon)); + } + + private static void MoveWidget(string widget, int direction) + { + int currentIndex = Settings.bigWidgets.IndexOf(widget); + if (currentIndex < 0) + return; + + int nextIndex = Math.Clamp(currentIndex + direction, 0, Settings.bigWidgets.Count - 1); + if (nextIndex == currentIndex) + return; + + Settings.bigWidgets.RemoveAt(currentIndex); + Settings.bigWidgets.Insert(nextIndex, widget); + } + + private static string GetWidgetSourceName(IRegisterableWidget widget) + { + string? widgetNamespace = widget.GetType().Namespace; + if (string.IsNullOrEmpty(widgetNamespace)) + return "DynamicWin"; + + return widgetNamespace.Split('.')[0]; + } + + private static float Smooth(float current, float target, float speed, float deltaTime, float epsilon) + { + if (Math.Abs(current - target) <= epsilon) + return target; + + return Mathf.Lerp(current, target, speed * deltaTime); } } } diff --git a/DynamicWin/UI/Menu/Menus/SettingsMenuObjects/BigWidgetAdderDisplay.cs b/DynamicWin/UI/Menu/Menus/SettingsMenuObjects/BigWidgetAdderDisplay.cs index a3241fc..c718bf8 100644 --- a/DynamicWin/UI/Menu/Menus/SettingsMenuObjects/BigWidgetAdderDisplay.cs +++ b/DynamicWin/UI/Menu/Menus/SettingsMenuObjects/BigWidgetAdderDisplay.cs @@ -1,4 +1,4 @@ -using DynamicWin.UI.UIElements; +using DynamicWin.UI.UIElements; using DynamicWin.Utils; using SkiaSharp; using System.Windows.Controls; @@ -7,74 +7,94 @@ namespace DynamicWin.UI.Menu.Menus.SettingsMenuObjects { internal class BigWidgetAdderDisplay : UIObject { - public BigWidgetAdderDisplay(UIObject? parent, string widgetName, UIAlignment alignment = UIAlignment.TopCenter) : base(parent, Vec2.zero, Vec2.zero, alignment) + private readonly Col displayColor; + private float hoverAlpha; + private float scale = 1f; + + public Action? onEditRemoveWidget; + public Action? onEditMoveWidgetLeft; + public Action? onEditMoveWidgetRight; + + public BigWidgetAdderDisplay(UIObject? parent, string widgetName, UIAlignment alignment = UIAlignment.TopCenter) + : base(parent, Vec2.zero, Vec2.zero, alignment) { - Size.X = parent.Size.X / 2; - Size.Y = 45; + UseGpuCaching = false; + Size = new Vec2((parent?.Size.X ?? 400f) / 2f, 45f); Anchor = Vec2.zero; + roundRadius = 45f; + displayColor = Theme.Primary.Override(a: 0.15f); AddLocalObject(new DWText(this, DWText.Truncate(widgetName, 25), Vec2.zero, UIAlignment.Center) { - TextSize = 14 + TextSize = 14, + Color = Theme.TextSecond }); + } - roundRadius = 45; + public override bool WantsRealtimeUpdate + { + get + { + float targetHover = IsHovering ? 1f : 0f; + float targetScale = IsHovering ? 1.025f : 1f; - color = Theme.Primary.Override(); + return Math.Abs(hoverAlpha - targetHover) > 0.01f + || Math.Abs(scale - targetScale) > 0.002f; + } + } + + public override void Update(float deltaTime) + { + base.Update(deltaTime); + + hoverAlpha = Smooth(hoverAlpha, IsHovering ? 1f : 0f, 9f, deltaTime, 0.01f); + scale = Smooth(scale, IsHovering ? 1.025f : 1f, 15f, deltaTime, 0.002f); } public override void Draw(SKCanvas canvas) { int canvasRestore = canvas.Save(); - var p = Position + Size / 2; - canvas.Scale(this.s, this.s, p.X, p.Y); - - var paint = GetPaint(); - var rect = GetRect(); + var pivot = Position + Size / 2f; + canvas.Scale(scale, scale, pivot.X, pivot.Y); - paint.Color = color.Value(); + using var paint = GetPaint(); + paint.Color = displayColor.Override(a: 0.15f + hoverAlpha * 0.05f).Value(); + var rect = GetRect(); rect.Deflate(5, 5); canvas.DrawRoundRect(rect, paint); canvas.RestoreToCount(canvasRestore); } - Col color; - float s = 1; - - public override void Update(float deltaTime) - { - base.Update(deltaTime); - - color.a = Mathf.Lerp(color.a, IsHovering ? 0.2f : 0.15f, 7.5f * deltaTime); - s = Mathf.Lerp(s, IsHovering ? 1.025f : 1, 15f * deltaTime); - } - - public Action onEditRemoveWidget; - public Action onEditMoveWidgetLeft; - public Action onEditMoveWidgetRight; - public override ContextMenu? GetContextMenu() { - var ctx = new System.Windows.Controls.ContextMenu(); + var ctx = new ContextMenu(); - MenuItem remove = new MenuItem() { Header = "Remove", Icon = ContextMenuUtils.LoadMenuIcon("Resources/icons/context/trash.png") }; + var remove = new MenuItem() { Header = "Remove", Icon = ContextMenuUtils.LoadMenuIcon("Resources/icons/context/trash.png") }; remove.Click += (x, y) => onEditRemoveWidget?.Invoke(); - MenuItem pL = new MenuItem() { Header = "Push Left", Icon = ContextMenuUtils.LoadMenuIcon("Resources/icons/context/left.png") }; - pL.Click += (x, y) => onEditMoveWidgetLeft?.Invoke(); + var pushLeft = new MenuItem() { Header = "Push Left", Icon = ContextMenuUtils.LoadMenuIcon("Resources/icons/context/left.png") }; + pushLeft.Click += (x, y) => onEditMoveWidgetLeft?.Invoke(); - MenuItem pR = new MenuItem() { Header = "Push Right", Icon = ContextMenuUtils.LoadMenuIcon("Resources/icons/context/right.png") }; - pR.Click += (x, y) => onEditMoveWidgetRight?.Invoke(); + var pushRight = new MenuItem() { Header = "Push Right", Icon = ContextMenuUtils.LoadMenuIcon("Resources/icons/context/right.png") }; + pushRight.Click += (x, y) => onEditMoveWidgetRight?.Invoke(); ctx.Items.Add(remove); - ctx.Items.Add(pL); - ctx.Items.Add(pR); + ctx.Items.Add(pushLeft); + ctx.Items.Add(pushRight); return ctx; } + + private static float Smooth(float current, float target, float speed, float deltaTime, float epsilon) + { + if (Math.Abs(current - target) <= epsilon) + return target; + + return Mathf.Lerp(current, target, speed * deltaTime); + } } } diff --git a/DynamicWin/UI/Menu/Menus/SettingsMenuObjects/SettingsCheckbox.cs b/DynamicWin/UI/Menu/Menus/SettingsMenuObjects/SettingsCheckbox.cs new file mode 100644 index 0000000..5b65b04 --- /dev/null +++ b/DynamicWin/UI/Menu/Menus/SettingsMenuObjects/SettingsCheckbox.cs @@ -0,0 +1,19 @@ +using DynamicWin.UI.UIElements; +using DynamicWin.Utils; + +namespace DynamicWin.UI.Menu.Menus.SettingsMenuObjects +{ + internal class SettingsCheckbox : DWCheckbox + { + public SettingsCheckbox( + UIObject? parent, + string text, + Vec2 position, + Vec2 size, + Action? clickCallback, + UIAlignment alignment = UIAlignment.TopLeft) + : base(parent, text, position, size, clickCallback, alignment) + { + } + } +} diff --git a/DynamicWin/UI/Menu/Menus/SettingsMenuObjects/SmallWidgetAdder.cs b/DynamicWin/UI/Menu/Menus/SettingsMenuObjects/SmallWidgetAdder.cs index 99dd52a..21b9163 100644 --- a/DynamicWin/UI/Menu/Menus/SettingsMenuObjects/SmallWidgetAdder.cs +++ b/DynamicWin/UI/Menu/Menus/SettingsMenuObjects/SmallWidgetAdder.cs @@ -1,4 +1,4 @@ -using DynamicWin.Main; +using DynamicWin.Main; using DynamicWin.Resources; using DynamicWin.UI.Widgets; using DynamicWin.UI.Widgets.Small; @@ -9,263 +9,275 @@ namespace DynamicWin.UI.Menu.Menus.SettingsMenuObjects { internal class SmallWidgetAdder : UIObject { - public SmallWidgetAdder(UIObject? parent, Vec2 position, Vec2 size, UIAlignment alignment = UIAlignment.TopCenter) : base(parent, position, size, alignment) + private const float EdgePadding = 15f; + private const float WidgetSpacing = 30f; + private const float MiddleSpacing = 35f; + + private readonly UIObject container; + private readonly float minimumWidth; + private float lastLayoutSignature = float.NaN; + private bool layoutDirty = true; + + public readonly List smallLeftWidgets = new List(); + public readonly List smallRightWidgets = new List(); + public readonly List smallCenterWidgets = new List(); + + public SmallWidgetAdder(UIObject? parent, Vec2 position, Vec2 size, UIAlignment alignment = UIAlignment.TopCenter) + : base(parent, position, size, alignment) { + UseGpuCaching = false; + minimumWidth = size.X; Color = Theme.WidgetBackground.Override(a: 0.1f); roundRadius = 25; - container = new UIObject(this, Vec2.zero, new Vec2(size.X - 100, size.Y), UIAlignment.Center); - container.Color = Col.Transparent; + container = new UIObject(this, Vec2.zero, new Vec2(size.X - 100, size.Y), UIAlignment.Center) + { + Color = Col.Transparent, + UseGpuCaching = false + }; AddLocalObject(container); UpdateWidgetDisplay(); + LayoutWidgets(); } - UIObject container; - - public List smallLeftWidgets = new List(); - public List smallRightWidgets = new List(); - public List smallCenterWidgets = new List(); - - void UpdateWidgetDisplay() + public override void Update(float deltaTime) { - smallRightWidgets.ForEach((x) => DestroyLocalObject(x)); - smallLeftWidgets.ForEach((x) => DestroyLocalObject(x)); - smallCenterWidgets.ForEach((x) => DestroyLocalObject(x)); - - smallRightWidgets.Clear(); - smallLeftWidgets.Clear(); - smallCenterWidgets.Clear(); - - Dictionary smallWidgets = new Dictionary(); - + base.Update(deltaTime); - foreach (var widget in Res.availableSmallWidgets) + float signature = BuildLayoutSignature(); + if (float.IsNaN(lastLayoutSignature) || Math.Abs(signature - lastLayoutSignature) > 0.01f) { - smallWidgets.Add(widget.GetType().FullName, widget); - System.Diagnostics.Debug.WriteLine(widget.GetType().FullName); + lastLayoutSignature = signature; + layoutDirty = true; } - foreach (var smallWidget in Settings.smallWidgetsMiddle) - { - if (!smallWidgets.ContainsKey(smallWidget)) continue; + if (layoutDirty) + LayoutWidgets(); + } - var widget = smallWidgets[smallWidget.ToString()]; + public override ContextMenu? GetContextMenu() + { + var ctx = new ContextMenu(); + bool anyWidgetsLeft = false; - var instance = (SmallWidgetBase)widget.CreateWidgetInstance(container, Vec2.zero, UIAlignment.Center); - instance.isEditMode = true; + var left = new MenuItem() { Header = "Left", Icon = ContextMenuUtils.LoadMenuIcon("Resources/icons/context/align-left.png") }; + var middle = new MenuItem() { Header = "Middle", Icon = ContextMenuUtils.LoadMenuIcon("Resources/icons/context/align-centre.png") }; + var right = new MenuItem() { Header = "Right", Icon = ContextMenuUtils.LoadMenuIcon("Resources/icons/context/align-right.png") }; - instance.onEditRemoveWidget += () => { - Settings.smallWidgetsMiddle.Remove(smallWidget); - UpdateWidgetDisplay(); - }; + foreach (var availableWidget in Res.availableSmallWidgets) + { + string? fullName = availableWidget.GetType().FullName; + if (string.IsNullOrEmpty(fullName) || IsWidgetSelected(fullName)) + continue; - instance.onEditMoveWidgetLeft += () => { - int index = Math.Clamp(Settings.smallWidgetsMiddle.IndexOf(smallWidget) + 1, 0, Settings.smallWidgetsMiddle.Count - 1); - Settings.smallWidgetsMiddle.Remove(smallWidget); + anyWidgetsLeft = true; - Settings.smallWidgetsMiddle.Insert(index, smallWidget); + left.Items.Add(CreateAddItem(availableWidget, () => + { + Settings.smallWidgetsLeft.Add(fullName); UpdateWidgetDisplay(); - }; + })); - instance.onEditMoveWidgetRight += () => { - int index = Math.Clamp(Settings.smallWidgetsMiddle.IndexOf(smallWidget) - 1, 0, Settings.smallWidgetsMiddle.Count - 1); - Settings.smallWidgetsMiddle.Remove(smallWidget); - - Settings.smallWidgetsMiddle.Insert(index, smallWidget); + middle.Items.Add(CreateAddItem(availableWidget, () => + { + Settings.smallWidgetsMiddle.Add(fullName); UpdateWidgetDisplay(); - }; + })); - smallCenterWidgets.Add(instance); + right.Items.Add(CreateAddItem(availableWidget, () => + { + Settings.smallWidgetsRight.Add(fullName); + UpdateWidgetDisplay(); + })); } - foreach (var smallWidget in Settings.smallWidgetsLeft) - { - if (!smallWidgets.ContainsKey(smallWidget)) continue; - - var widget = smallWidgets[smallWidget.ToString()]; - - var instance = (SmallWidgetBase)widget.CreateWidgetInstance(container, Vec2.zero, UIAlignment.MiddleLeft); - instance.isEditMode = true; + ctx.Items.Add(left); + ctx.Items.Add(middle); + ctx.Items.Add(right); - instance.onEditRemoveWidget += () => { - Settings.smallWidgetsLeft.Remove(smallWidget); - UpdateWidgetDisplay(); - }; + if (anyWidgetsLeft) + return ctx; - instance.onEditMoveWidgetLeft += () => { - int index = Math.Clamp(Settings.smallWidgetsLeft.IndexOf(smallWidget) + 1, 0, Settings.smallWidgetsLeft.Count - 1); - Settings.smallWidgetsLeft.Remove(smallWidget); + var empty = new ContextMenu(); + empty.Items.Add(new MenuItem() + { + Header = "No widgets available.", + IsEnabled = false + }); + return empty; + } - Settings.smallWidgetsLeft.Insert(index, smallWidget); - UpdateWidgetDisplay(); - }; + private void UpdateWidgetDisplay() + { + ClearWidgets(smallRightWidgets); + ClearWidgets(smallLeftWidgets); + ClearWidgets(smallCenterWidgets); - instance.onEditMoveWidgetRight += () => { - int index = Math.Clamp(Settings.smallWidgetsLeft.IndexOf(smallWidget) - 1, 0, Settings.smallWidgetsLeft.Count - 1); - Settings.smallWidgetsLeft.Remove(smallWidget); + var smallWidgets = Res.availableSmallWidgets + .Where(widget => !string.IsNullOrEmpty(widget.GetType().FullName)) + .GroupBy(widget => widget.GetType().FullName!) + .ToDictionary(group => group.Key, group => group.First()); - Settings.smallWidgetsLeft.Insert(index, smallWidget); - UpdateWidgetDisplay(); - }; + AddConfiguredWidgets(Settings.smallWidgetsMiddle, smallCenterWidgets, smallWidgets, UIAlignment.Center); + AddConfiguredWidgets(Settings.smallWidgetsLeft, smallLeftWidgets, smallWidgets, UIAlignment.MiddleLeft); + AddConfiguredWidgets(Settings.smallWidgetsRight, smallRightWidgets, smallWidgets, UIAlignment.MiddleRight); - smallLeftWidgets.Add(instance); - } + layoutDirty = true; + } - foreach (var smallWidget in Settings.smallWidgetsRight) + private void AddConfiguredWidgets( + List configuredWidgets, + List destination, + Dictionary availableWidgets, + UIAlignment alignment) + { + foreach (string configuredWidget in configuredWidgets.ToArray()) { - if (!smallWidgets.ContainsKey(smallWidget)) continue; + if (!availableWidgets.TryGetValue(configuredWidget, out var widget)) + continue; - var widget = smallWidgets[smallWidget.ToString()]; - - var instance = (SmallWidgetBase)widget.CreateWidgetInstance(container, Vec2.zero, UIAlignment.MiddleRight); + string capturedWidget = configuredWidget; + var instance = (SmallWidgetBase)widget.CreateWidgetInstance(container, Vec2.zero, alignment); instance.isEditMode = true; - instance.onEditRemoveWidget += () => { - Settings.smallWidgetsRight.Remove(smallWidget); + instance.onEditRemoveWidget += () => + { + configuredWidgets.Remove(capturedWidget); UpdateWidgetDisplay(); }; - instance.onEditMoveWidgetLeft += () => { - int index = Math.Clamp(Settings.smallWidgetsRight.IndexOf(smallWidget) + 1, 0, Settings.smallWidgetsRight.Count - 1); - Settings.smallWidgetsRight.Remove(smallWidget); - - Settings.smallWidgetsRight.Insert(index, smallWidget); + instance.onEditMoveWidgetLeft += () => + { + MoveWidget(configuredWidgets, capturedWidget, 1); UpdateWidgetDisplay(); }; - instance.onEditMoveWidgetRight += () => { - int index = Math.Clamp(Settings.smallWidgetsRight.IndexOf(smallWidget) - 1, 0, Settings.smallWidgetsRight.Count - 1); - Settings.smallWidgetsRight.Remove(smallWidget); - - Settings.smallWidgetsRight.Insert(index, smallWidget); + instance.onEditMoveWidgetRight += () => + { + MoveWidget(configuredWidgets, capturedWidget, -1); UpdateWidgetDisplay(); }; - smallRightWidgets.Add(instance); + destination.Add(instance); + AddLocalObject(instance); } - - smallCenterWidgets.ForEach((x) => AddLocalObject(x)); - smallLeftWidgets.ForEach((x) => AddLocalObject(x)); - smallRightWidgets.ForEach((x) => AddLocalObject(x)); } - public float smallWidgetsSpacing = 30; - public float middleWidgetsSpacing = 35; - - public override void Update(float deltaTime) + private void LayoutWidgets() { - base.Update(deltaTime); - - { // Left Small Widgets - float leftStackedPos = 15f; - foreach (var smallLeft in smallLeftWidgets) - { - smallLeft.Anchor.X = 0; - smallLeft.LocalPosition.X = leftStackedPos; - - leftStackedPos += smallWidgetsSpacing + smallLeft.GetWidgetSize().X; - } + float leftStackedPos = EdgePadding; + foreach (var smallLeft in smallLeftWidgets) + { + smallLeft.Anchor.X = 0; + smallLeft.LocalPosition.X = leftStackedPos; + leftStackedPos += WidgetSpacing + smallLeft.GetWidgetSize().X; } - { // Right Small Widgets - float rightStackedPos = -15f; - foreach (var smallRight in smallRightWidgets) - { - smallRight.Anchor.X = 1; - smallRight.LocalPosition.X = rightStackedPos; + float rightStackedPos = -EdgePadding; + foreach (var smallRight in smallRightWidgets) + { + smallRight.Anchor.X = 1; + smallRight.LocalPosition.X = rightStackedPos; + rightStackedPos -= WidgetSpacing + smallRight.GetWidgetSize().X; + } - rightStackedPos -= smallWidgetsSpacing + smallRight.GetWidgetSize().X; - } + float centerStackPos = 0f; + foreach (var smallCenter in smallCenterWidgets) + { + smallCenter.Anchor.X = 1; + smallCenter.LocalPosition.X = centerStackPos; + centerStackPos -= WidgetSpacing + smallCenter.GetWidgetSize().X; } - { // Center Small Widgets - float centerStackPos = 0f; - foreach (var smallCenter in smallCenterWidgets) - { - smallCenter.Anchor.X = 1; - smallCenter.LocalPosition.X = centerStackPos; + foreach (var smallCenter in smallCenterWidgets) + smallCenter.LocalPosition.X -= centerStackPos / 2f + WidgetSpacing; - centerStackPos -= smallWidgetsSpacing + smallCenter.GetWidgetSize().X; - } + float requiredWidth = GetWidgetsWidth(smallLeftWidgets) + + GetWidgetsWidth(smallRightWidgets) + + GetWidgetsWidth(smallCenterWidgets) + + WidgetSpacing * (smallLeftWidgets.Count + smallRightWidgets.Count + smallCenterWidgets.Count + 0.25f) + + MiddleSpacing; - foreach (var smallCenter in smallCenterWidgets) - { - smallCenter.LocalPosition.X -= centerStackPos / 2 + smallWidgetsSpacing; - } - } + Size = new Vec2(Math.Max(minimumWidth, requiredWidth), Size.Y); + container.Size = new Vec2(Math.Max(1f, Size.X - 100f), Size.Y); + layoutDirty = false; + } - Vec2 size = Size; + private float BuildLayoutSignature() + { + float signature = Size.X * 0.03f + Size.Y; - float sizeTogether = 0f; - smallLeftWidgets.ForEach(x => sizeTogether += x.GetWidgetSize().X); - smallRightWidgets.ForEach(x => sizeTogether += x.GetWidgetSize().X); - smallCenterWidgets.ForEach(x => sizeTogether += x.GetWidgetSize().X); + AddWidgetSignature(smallLeftWidgets, ref signature); + AddWidgetSignature(smallCenterWidgets, ref signature); + AddWidgetSignature(smallRightWidgets, ref signature); - sizeTogether += smallWidgetsSpacing * (smallCenterWidgets.Count + smallLeftWidgets.Count + smallRightWidgets.Count + 0.25f) + middleWidgetsSpacing; + return signature; + } - size.X = (float)Math.Max(size.X, sizeTogether); + private static void AddWidgetSignature(List widgets, ref float signature) + { + signature += widgets.Count * 17f; + + foreach (var widget in widgets) + { + Vec2 size = widget.GetWidgetSize(); + signature += size.X * 0.37f + size.Y * 0.11f; + } } - public override ContextMenu? GetContextMenu() + private static float GetWidgetsWidth(List widgets) { - var ctx = new System.Windows.Controls.ContextMenu(); - bool anyWidgetsLeft = false; + float width = 0f; + foreach (var widget in widgets) + width += widget.GetWidgetSize().X; - MenuItem left = new MenuItem() { Header = "Left", Icon = ContextMenuUtils.LoadMenuIcon("Resources/icons/context/align-left.png") }; - MenuItem middle = new MenuItem() { Header = "Middle", Icon = ContextMenuUtils.LoadMenuIcon("Resources/icons/context/align-centre.png") }; - MenuItem right = new MenuItem() { Header = "Right", Icon = ContextMenuUtils.LoadMenuIcon("Resources/icons/context/align-right.png") }; + return width; + } - foreach (var availableWidget in Res.availableSmallWidgets) - { - if (Settings.smallWidgetsRight.Contains(availableWidget.GetType().FullName) || - Settings.smallWidgetsLeft.Contains(availableWidget.GetType().FullName) || - Settings.smallWidgetsMiddle.Contains(availableWidget.GetType().FullName)) continue; + private void ClearWidgets(List widgets) + { + for (int i = widgets.Count - 1; i >= 0; i--) + DestroyLocalObject(widgets[i]); - anyWidgetsLeft = true; + widgets.Clear(); + } - var itemR = new MenuItem() { Header = availableWidget.GetType().Namespace.Split('.')[0] + ": " + availableWidget.WidgetName }; - itemR.Click += (x, y) => - { - Settings.smallWidgetsRight.Add(availableWidget.GetType().FullName); - UpdateWidgetDisplay(); - }; + private static bool IsWidgetSelected(string fullName) + { + return Settings.smallWidgetsRight.Contains(fullName) + || Settings.smallWidgetsLeft.Contains(fullName) + || Settings.smallWidgetsMiddle.Contains(fullName); + } - var itemM = new MenuItem() { Header = availableWidget.GetType().Namespace.Split('.')[0] + ": " + availableWidget.WidgetName }; - itemM.Click += (x, y) => - { - Settings.smallWidgetsMiddle.Add(availableWidget.GetType().FullName); - UpdateWidgetDisplay(); - }; + private static MenuItem CreateAddItem(IRegisterableWidget widget, Action click) + { + var item = new MenuItem() { Header = $"{GetWidgetSourceName(widget)}: {widget.WidgetName}" }; + item.Click += (x, y) => click(); + return item; + } - var itemL = new MenuItem() { Header = availableWidget.GetType().Namespace.Split('.')[0] + ": " + availableWidget.WidgetName }; - itemL.Click += (x, y) => - { - Settings.smallWidgetsLeft.Add(availableWidget.GetType().FullName); - UpdateWidgetDisplay(); - }; + private static void MoveWidget(List widgets, string widget, int direction) + { + int currentIndex = widgets.IndexOf(widget); + if (currentIndex < 0) + return; - left.Items.Add(itemL); - middle.Items.Add(itemM); - right.Items.Add(itemR); - } + int nextIndex = Math.Clamp(currentIndex + direction, 0, widgets.Count - 1); + if (nextIndex == currentIndex) + return; - ctx.Items.Add(left); - ctx.Items.Add(middle); - ctx.Items.Add(right); + widgets.RemoveAt(currentIndex); + widgets.Insert(nextIndex, widget); + } - if (!anyWidgetsLeft) - { - var ctx2 = new ContextMenu(); - ctx2.Items.Add(new MenuItem() - { - Header = "No widgets available.", - IsEnabled = false - }); - return ctx2; - } + private static string GetWidgetSourceName(IRegisterableWidget widget) + { + string? widgetNamespace = widget.GetType().Namespace; + if (string.IsNullOrEmpty(widgetNamespace)) + return "DynamicWin"; - return ctx; + return widgetNamespace.Split('.')[0]; } } } diff --git a/DynamicWin/UI/UIElements/DWCheckbox.cs b/DynamicWin/UI/UIElements/DWCheckbox.cs index 3b2af2e..157fc75 100644 --- a/DynamicWin/UI/UIElements/DWCheckbox.cs +++ b/DynamicWin/UI/UIElements/DWCheckbox.cs @@ -1,37 +1,180 @@ -using DynamicWin.Resources; +using DynamicWin.Resources; using DynamicWin.Utils; +using SkiaSharp; namespace DynamicWin.UI.UIElements { - internal class DWCheckbox : DWImageButton + internal class DWCheckbox : UIObject { - bool isChecked = false; - public bool IsChecked { get { return isChecked; } set => SetChecked(value); } + private const float PreferredBoxSize = 25f; + private const float PreferredRowHeight = 32f; + private const float LabelGap = 15f; - void SetChecked(bool isChecked) + private readonly DWText label; + + private bool isChecked; + private float hoverAlpha; + private float checkAlpha; + private float pressScale = 1f; + private float boxSize = PreferredBoxSize; + private float lastLayoutWidth = float.NaN; + private float lastLayoutHeight = float.NaN; + + public Action? clickCallback; + + public bool IsChecked + { + get => isChecked; + set => SetChecked(value, animate: false); + } + + public DWCheckbox( + UIObject? parent, + string buttonText, + Vec2 position, + Vec2 size, + Action? clickCallback, + UIAlignment alignment = UIAlignment.TopCenter) + : base(parent, position, new Vec2(size.X, Math.Max(size.Y, PreferredRowHeight)), alignment) + { + this.clickCallback = clickCallback; + + UseGpuCaching = false; + Color = Theme.IconColor.Override(a: 0.16f); + roundRadius = PreferredBoxSize / 2f; + + label = new DWText(this, buttonText, new Vec2(PreferredBoxSize + LabelGap, 0), UIAlignment.MiddleLeft) + { + Anchor = new Vec2(0, 0.5f), + Color = Theme.TextSecond, + Font = Res.SFProRegular, + TextSize = PreferredBoxSize / 1.5f + }; + AddLocalObject(label); + + UpdateLayoutMetrics(force: true); + SetChecked(false, animate: false); + } + + public override bool WantsRealtimeUpdate + { + get + { + float targetHover = IsHovering ? 1f : 0f; + float targetCheck = isChecked ? 1f : 0f; + float targetScale = IsMouseDown ? 0.94f : 1f; + + return Math.Abs(hoverAlpha - targetHover) > 0.01f + || Math.Abs(checkAlpha - targetCheck) > 0.01f + || Math.Abs(pressScale - targetScale) > 0.002f + || IsMouseDown; + } + } + + public override void Update(float deltaTime) { - this.isChecked = isChecked; - Image.Image = isChecked ? Res.Check : null; + base.Update(deltaTime); + UpdateLayoutMetrics(force: false); + + hoverAlpha = Smooth(hoverAlpha, IsHovering ? 1f : 0f, 12f, deltaTime, 0.01f); + checkAlpha = Smooth(checkAlpha, isChecked ? 1f : 0f, 18f, deltaTime, 0.01f); + pressScale = Smooth(pressScale, IsMouseDown ? 0.94f : 1f, 18f, deltaTime, 0.002f); } - public DWCheckbox(UIObject? parent, string buttonText, Vec2 position, Vec2 size, Action clickCallback, UIAlignment alignment = UIAlignment.TopCenter) : base(parent, Res.Check, position, size, clickCallback, alignment) + public override void Draw(SKCanvas canvas) { - var text = new DWText(this, buttonText, new Vec2(15, 0), UIAlignment.MiddleRight); - text.Color = Theme.TextSecond; - text.Anchor.X = 0; - text.TextSize = size.Y / 1.5f; - AddLocalObject(text); + UpdateLayoutMetrics(force: false); + + var boxRect = GetBoxRect(); + int save = canvas.Save(); + canvas.Scale(pressScale, pressScale, boxRect.MidX, boxRect.MidY); + + using (var paint = GetPaint()) + { + paint.Color = Theme.IconColor.Override(a: 0.16f + hoverAlpha * 0.10f).Value(); + canvas.DrawRoundRect(new SKRoundRect(boxRect, boxSize / 2f), paint); + } + + if (checkAlpha > 0.01f && Res.Check != null) + { + using var iconPaint = GetPaint(); + using var iconFilter = SKColorFilter.CreateBlendMode( + Theme.IconColor.Override(a: checkAlpha).Value(), + SKBlendMode.SrcIn); - SetChecked(false); + iconPaint.Color = Theme.IconColor.Override(a: checkAlpha).Value(); + iconPaint.ColorFilter = iconFilter; - hoverScaleMulti = new Vec2(1.05f, 1f); - clickScaleMulti = new Vec2(0.975f, 1f); + float inset = boxSize * 0.27f; + var iconRect = SKRect.Create( + boxRect.Left + inset, + boxRect.Top + inset, + boxSize - inset * 2f, + boxSize - inset * 2f); + + canvas.DrawBitmap(Res.Check, iconRect, iconPaint); + } + + canvas.RestoreToCount(save); } public override void OnMouseUp() { - IsChecked = !IsChecked; - base.OnMouseUp(); + SetChecked(!isChecked, animate: true); + clickCallback?.Invoke(); + } + + private void SetChecked(bool value, bool animate) + { + if (isChecked == value) + { + if (!animate) + checkAlpha = value ? 1f : 0f; + + return; + } + + isChecked = value; + + if (!animate) + checkAlpha = value ? 1f : 0f; + } + + private SKRect GetBoxRect() + { + return SKRect.Create( + Position.X, + Position.Y + (Size.Y - boxSize) / 2f, + boxSize, + boxSize); + } + + private void UpdateLayoutMetrics(bool force) + { + if (!force + && Math.Abs(lastLayoutWidth - Size.X) <= 0.001f + && Math.Abs(lastLayoutHeight - Size.Y) <= 0.001f) + { + return; + } + + lastLayoutWidth = Size.X; + lastLayoutHeight = Size.Y; + boxSize = Math.Min(PreferredBoxSize, Math.Max(1f, Math.Min(Size.X, Size.Y))); + roundRadius = boxSize / 2f; + + label.Position = new Vec2(boxSize + LabelGap, 0); + label.LocalPosition = Vec2.zero; + label.TextSize = boxSize / 1.5f; + label.Size = label.GetBoundsForString(label.Text); + } + + private static float Smooth(float current, float target, float speed, float deltaTime, float epsilon) + { + if (Math.Abs(current - target) <= epsilon) + return target; + + return Mathf.Lerp(current, target, speed * deltaTime); } } }