diff --git a/Horizon/App.xaml b/Horizon/App.xaml index 574fc12..d7a09ee 100644 --- a/Horizon/App.xaml +++ b/Horizon/App.xaml @@ -1,7 +1,14 @@  + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> + + + + + + + + \ No newline at end of file diff --git a/Horizon/Horizon.csproj b/Horizon/Horizon.csproj index 798c5cb..7f09dc9 100644 --- a/Horizon/Horizon.csproj +++ b/Horizon/Horizon.csproj @@ -11,6 +11,7 @@ alpha false Horizon.ico + true diff --git a/Horizon/Resources/UI/Animations.xaml b/Horizon/Resources/UI/Animations.xaml new file mode 100644 index 0000000..c066f10 --- /dev/null +++ b/Horizon/Resources/UI/Animations.xaml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/Horizon/Resources/UI/Brushes.xaml b/Horizon/Resources/UI/Brushes.xaml new file mode 100644 index 0000000..d9ec3fa --- /dev/null +++ b/Horizon/Resources/UI/Brushes.xaml @@ -0,0 +1,25 @@ + + + #2A2A2D + #000000 + #FFFFFF + #990000 + #770000 + #7AC1FF + #4A4A4D + #8A8A8D + #C95100 + + + + + + + + + + + + + \ No newline at end of file diff --git a/Horizon/Resources/UI/ControlDefinitions/WindowDefinition.xaml b/Horizon/Resources/UI/ControlDefinitions/WindowDefinition.xaml new file mode 100644 index 0000000..46d2ba1 --- /dev/null +++ b/Horizon/Resources/UI/ControlDefinitions/WindowDefinition.xaml @@ -0,0 +1,94 @@ + + + \ No newline at end of file diff --git a/Horizon/Resources/UI/Controls.xaml b/Horizon/Resources/UI/Controls.xaml new file mode 100644 index 0000000..998643d --- /dev/null +++ b/Horizon/Resources/UI/Controls.xaml @@ -0,0 +1,15 @@ + + + + + + + + + \ No newline at end of file diff --git a/Horizon/Resources/UI/Converters.xaml b/Horizon/Resources/UI/Converters.xaml new file mode 100644 index 0000000..42f1cd1 --- /dev/null +++ b/Horizon/Resources/UI/Converters.xaml @@ -0,0 +1,3 @@ + + \ No newline at end of file diff --git a/Horizon/Utilities/KeyConstants.cs b/Horizon/Utilities/KeyConstants.cs new file mode 100644 index 0000000..56aefa3 --- /dev/null +++ b/Horizon/Utilities/KeyConstants.cs @@ -0,0 +1,138 @@ +using System.Windows.Input; + +namespace Horizon.Utilities; + +/// +/// Contains constants for handling key codes. +/// +public static class KeyConstants +{ + /// + /// Contains a list of keys that should be rendered as medium size. + /// + public static List MediumSizeKeys { get; } = + [ + Key.Tab, + Key.CapsLock, + Key.LeftShift, + Key.RightShift, + Key.LeftAlt, + Key.RightAlt, + Key.LeftCtrl, + Key.RightCtrl, + Key.Back, + Key.Apps, + Key.Enter, + Key.Add, + Key.Insert + ]; + + /// + /// Contains the friendly names for each defined key on the keyboard. + /// + public static Dictionary KeyNames { get; } = new() + { + [Key.Escape] = "Esc", + [Key.F1] = "F1", + [Key.F2] = "F2", + [Key.F3] = "F3", + [Key.F4] = "F4", + [Key.F5] = "F5", + [Key.F6] = "F6", + [Key.F7] = "F7", + [Key.F8] = "F8", + [Key.F9] = "F9", + [Key.F10] = "F10", + [Key.F11] = "F11", + [Key.F12] = "F12", + [Key.PrintScreen] = "Print Sc", + [Key.Scroll] = "Scroll Lock", + [Key.Pause] = "Pause", + [Key.Oem3] = "`", + [Key.D1] = "1", + [Key.D2] = "2", + [Key.D3] = "3", + [Key.D4] = "4", + [Key.D5] = "5", + [Key.D6] = "6", + [Key.D7] = "7", + [Key.D8] = "8", + [Key.D9] = "9", + [Key.D0] = "0", + [Key.OemMinus] = "-", + [Key.OemPlus] = "+", + [Key.Back] = "Backspace", + [Key.Tab] = "Tab", + [Key.Q] = "Q", + [Key.W] = "W", + [Key.E] = "E", + [Key.R] = "R", + [Key.T] = "T", + [Key.Y] = "Y", + [Key.U] = "U", + [Key.I] = "I", + [Key.O] = "O", + [Key.P] = "P", + [Key.Oem4] = "[", + [Key.Oem6] = "]", + [Key.Oem5] = "\\", + [Key.CapsLock] = "Caps Lock", + [Key.A] = "A", + [Key.S] = "S", + [Key.D] = "D", + [Key.F] = "F", + [Key.G] = "G", + [Key.H] = "H", + [Key.J] = "J", + [Key.K] = "K", + [Key.L] = "L", + [Key.Oem1] = ";", + [Key.Oem7] = "'", + [Key.Enter] = "Enter", + [Key.LeftShift] = "Shift", + [Key.Z] = "Z", + [Key.X] = "X", + [Key.C] = "C", + [Key.V] = "V", + [Key.B] = "B", + [Key.N] = "N", + [Key.M] = "M", + [Key.OemComma] = ",", + [Key.OemPeriod] = ".", + [Key.Oem2] = "/", + [Key.RightShift] = "Shift", + [Key.LeftCtrl] = "Ctrl", + [Key.LWin] = "Win", + [Key.LeftAlt] = "Alt", + [Key.Space] = "Space", + [Key.RightAlt] = "Alt", + [Key.Apps] = "≣", + [Key.RightCtrl] = "Ctrl", + [Key.Insert] = "Insert", + [Key.Home] = "Home", + [Key.PageUp] = "Pg Up", + [Key.Delete] = "Delete", + [Key.End] = "End", + [Key.PageDown] = "Pg Dn", + [Key.Up] = "↑", + [Key.Down] = "↓", + [Key.Left] = "←", + [Key.Right] = "→", + [Key.NumLock] = "Num Lock", + [Key.Divide] = "Numpad /", + [Key.Multiply] = "Numpad *", + [Key.Subtract] = "Numpad -", + [Key.Add] = "Numpad +", + [Key.Decimal] = "Numpad .", + [Key.NumPad1] = "N1", + [Key.NumPad2] = "N2", + [Key.NumPad3] = "N3", + [Key.NumPad4] = "N4", + [Key.NumPad5] = "N5", + [Key.NumPad6] = "N6", + [Key.NumPad7] = "N7", + [Key.NumPad8] = "N8", + [Key.NumPad9] = "N9", + [Key.NumPad0] = "N0" + }; +} \ No newline at end of file diff --git a/Horizon/Utilities/NativeUtilities.cs b/Horizon/Utilities/NativeUtilities.cs new file mode 100644 index 0000000..546f747 --- /dev/null +++ b/Horizon/Utilities/NativeUtilities.cs @@ -0,0 +1,32 @@ +using System.Runtime.InteropServices; + +namespace Horizon.Utilities; + +/// +/// Magic native methods used for windows and popups. +/// +internal static partial class NativeUtilities +{ + internal static uint TPM_LEFTALIGN; + + internal static uint TPM_RETURNCMD; + + static NativeUtilities() + { + TPM_LEFTALIGN = 0; + TPM_RETURNCMD = 256; + } + + [LibraryImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool EnableMenuItem(IntPtr hMenu, uint uIDEnableItem, uint uEnable); + + [LibraryImport("user32.dll", SetLastError = true)] + internal static partial IntPtr GetSystemMenu(IntPtr hWnd, [MarshalAs(UnmanagedType.Bool)] bool bRevert); + + [LibraryImport("user32.dll")] + internal static partial IntPtr PostMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); + + [LibraryImport("user32.dll")] + internal static partial int TrackPopupMenuEx(IntPtr hmenu, uint fuFlags, int x, int y, IntPtr hwnd, IntPtr lptpm); +} \ No newline at end of file diff --git a/Horizon/Utilities/RECT.cs b/Horizon/Utilities/RECT.cs new file mode 100644 index 0000000..93056f4 --- /dev/null +++ b/Horizon/Utilities/RECT.cs @@ -0,0 +1,60 @@ +using System.Windows; + +namespace Horizon.Utilities; + +/// +/// A custom rect struct used only for native utilities. +/// +[Serializable] +internal struct RECT +{ + public int Bottom; + + public int Left; + + public int Right; + + public int Top; + + public RECT(int left, int top, int right, int bottom) + { + this.Left = left; + this.Top = top; + this.Right = right; + this.Bottom = bottom; + } + + public RECT(Rect rect) + { + this.Left = (int)rect.Left; + this.Top = (int)rect.Top; + this.Right = (int)rect.Right; + this.Bottom = (int)rect.Bottom; + } + + public int Height + { + readonly get => this.Bottom - this.Top; + set => this.Bottom = this.Top + value; + } + + public readonly Point Position => new(this.Left, this.Top); + + public readonly Size Size => new(this.Width, this.Height); + + public int Width + { + readonly get => this.Right - this.Left; + set => this.Right = this.Left + value; + } + + public void Offset(int dx, int dy) + { + this.Left += dx; + this.Right += dx; + this.Top += dy; + this.Bottom += dy; + } + + public readonly Int32Rect ToInt32Rect() => new(this.Left, this.Top, this.Width, this.Height); +} \ No newline at end of file diff --git a/Horizon/Utilities/SystemHelper.cs b/Horizon/Utilities/SystemHelper.cs new file mode 100644 index 0000000..9bf15fd --- /dev/null +++ b/Horizon/Utilities/SystemHelper.cs @@ -0,0 +1,29 @@ +using System.Reflection; +using System.Windows; +using System.Windows.Forms; + +namespace Horizon.Utilities; + +/// +/// A helper used for low-level windows system methods such as getting DPI and mouse position. +/// +internal static class SystemHelper +{ + /// + /// Gets the current system DPI. + /// + /// The DPI as an . + public static int GetCurrentDPI() => ((int?)typeof(SystemParameters).GetProperty("Dpi", BindingFlags.Static | BindingFlags.NonPublic)?.GetValue(null, null)) ?? 0; + + /// + /// Gets the current system DPI scale factor. + /// + /// The scale factor as a . + public static double GetCurrentDPIScaleFactor() => (double)GetCurrentDPI() / 96; + + /// + /// Gets the mouse position as defined in Windows Forms. + /// + /// A representing the mouse position. + public static Point GetMousePositionWindowsForms() => new(Control.MousePosition.X, Control.MousePosition.Y); +} \ No newline at end of file diff --git a/Horizon/View/Windows/BorderlessReactiveWindow.cs b/Horizon/View/Windows/BorderlessReactiveWindow.cs new file mode 100644 index 0000000..d99cfe0 --- /dev/null +++ b/Horizon/View/Windows/BorderlessReactiveWindow.cs @@ -0,0 +1,68 @@ +using ReactiveUI; +using System.Windows; + +namespace Horizon.View.Windows; + +/// +/// A that is reactive. +/// +/// +/// +/// This class is a that is also reactive. That is, it implements . You can +/// extend this class to get an implementation of rather than writing one yourself. +/// +/// +/// Note that the XAML for your control must specify the same base class, including the generic argument you provide for your +/// view model. To do this, use the TypeArguments attribute as follows: +/// +/// +/// +/// +///]]> +/// +/// +/// +/// The type of the view model backing the view. +public abstract class BorderlessReactiveWindow : + BorderlessWindow, IViewFor + where TViewModel : class +{ + /// + /// The view model dependency property. + /// + public static readonly DependencyProperty ViewModelProperty = + DependencyProperty.Register( + "ViewModel", + typeof(TViewModel), + typeof(BorderlessReactiveWindow), + new PropertyMetadata(null)); + + /// + /// Gets the binding root view model. + /// + public TViewModel? BindingRoot => this.ViewModel; + + /// + public TViewModel? ViewModel + { + get => (TViewModel?)this.GetValue(ViewModelProperty); + set => this.SetValue(ViewModelProperty, value); + } + + /// + object? IViewFor.ViewModel + { + get => this.ViewModel; + set => this.ViewModel = (TViewModel?)value; + } +} \ No newline at end of file diff --git a/Horizon/View/Windows/BorderlessWindow.cs b/Horizon/View/Windows/BorderlessWindow.cs new file mode 100644 index 0000000..8377fdd --- /dev/null +++ b/Horizon/View/Windows/BorderlessWindow.cs @@ -0,0 +1,404 @@ +using Horizon.Utilities; +using JetBrains.Annotations; +using Microsoft.Win32; +using System.Diagnostics; +using System.Drawing; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Forms; +using System.Windows.Input; +using System.Windows.Interop; +using Button = System.Windows.Controls.Button; +using MouseEventArgs = System.Windows.Input.MouseEventArgs; +using MouseEventHandler = System.Windows.Input.MouseEventHandler; +using Point = System.Windows.Point; + +namespace Horizon.View.Windows; + +public class BorderlessWindow : Window +{ +#pragma warning disable IDE0052 // Remove unread private members + [UsedImplicitly] private HwndSource hwndSource = null!; +#pragma warning restore IDE0052 // Remove unread private members + + private bool isManualDrag; + + private bool isMouseButtonDown; + + private Point mouseDownPosition; + + private Point positionBeforeDrag; + + private Point previousScreenBounds; + + public Button CloseButton { get; private set; } = null!; + + public Grid HeaderBar { get; private set; } = null!; + + public double HeightBeforeMaximize { get; private set; } = 0; + + public Grid LayoutRoot { get; private set; } = null!; + + public Button MaximizeButton { get; private set; } = null!; + + public Button MinimizeButton { get; private set; } = null!; + + public WindowState PreviousState { get; private set; } + + public Button RestoreButton { get; private set; } = null!; + + public double WidthBeforeMaximize { get; private set; } = 0; + + public Grid WindowRoot { get; private set; } = null!; + + static BorderlessWindow() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(BorderlessWindow), new FrameworkPropertyMetadata(typeof(BorderlessWindow))); + } + + public BorderlessWindow() + { + double currentDPIScaleFactor = SystemHelper.GetCurrentDPIScaleFactor(); + Screen screen = Screen.FromHandle(new WindowInteropHelper(this).Handle); + this.SizeChanged += this.OnSizeChanged; + this.StateChanged += this.OnStateChanged; + this.Loaded += this.OnLoaded; + Rectangle workingArea = screen.WorkingArea; + this.MaxHeight = (workingArea.Height + 16) / currentDPIScaleFactor; + SystemEvents.DisplaySettingsChanged += this.SystemEvents_DisplaySettingsChanged; + this.AddHandler(MouseLeftButtonUpEvent, new MouseButtonEventHandler(this.OnMouseButtonUp), true); + this.AddHandler(MouseMoveEvent, new MouseEventHandler(this.OnMouseMove)); + } + + public T GetRequiredTemplateChild(string childName) where T : DependencyObject => (T)this.GetTemplateChild(childName); + + public override void OnApplyTemplate() + { + this.WindowRoot = this.GetRequiredTemplateChild("WindowRoot"); + this.LayoutRoot = this.GetRequiredTemplateChild("LayoutRoot"); + this.MinimizeButton = this.GetRequiredTemplateChild