From 1dc54e9a72f909177f4f0977cbeb784bbc69a67f Mon Sep 17 00:00:00 2001 From: Max Litruv Boonzaayer Date: Wed, 7 Jan 2026 11:32:40 +1100 Subject: [PATCH] Refactor StartupDialog to enhance UI with retro-styled controls and improve user experience --- StartupDialog.cs | 435 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 342 insertions(+), 93 deletions(-) diff --git a/StartupDialog.cs b/StartupDialog.cs index 6d92cd9..1d39174 100644 --- a/StartupDialog.cs +++ b/StartupDialog.cs @@ -1,109 +1,199 @@ using System.Diagnostics; +using System.Drawing.Drawing2D; using System.Runtime.Versioning; namespace DevDrive; /// -/// Dialog that prompts the user to select which startup apps to launch. +/// Retro-styled dialog that prompts the user to select which startup apps to launch. /// [SupportedOSPlatform("windows")] public class StartupDialog : Form { + private static readonly Color BackgroundDark = Color.FromArgb(24, 24, 32); + private static readonly Color BackgroundMid = Color.FromArgb(32, 32, 44); + private static readonly Color AccentCyan = Color.FromArgb(0, 255, 255); + private static readonly Color AccentMagenta = Color.FromArgb(255, 0, 128); + private static readonly Color TextLight = Color.FromArgb(240, 240, 255); + private static readonly Color TextDim = Color.FromArgb(140, 140, 160); + private static readonly Color BorderColor = Color.FromArgb(60, 60, 80); + private readonly List _apps; - private readonly List _checkboxes = []; - private readonly Button _launchButton; - private readonly Button _cancelButton; + private readonly List _checkboxes = []; + private readonly string _driveName; + private readonly System.Windows.Forms.Timer _animTimer; + private float _scanlineOffset; + private float _glowPulse; + private float _flickerTarget; + private float _flickerCurrent; + private float _brightLineY; + private int _flickerCounter; /// /// Creates a new startup dialog for the given apps. /// - /// List of startup apps to display. - /// Name of the drive for the title. public StartupDialog(List apps, string driveName) { _apps = apps; + _driveName = driveName; - Text = $"{driveName} - Startup Apps"; - FormBorderStyle = FormBorderStyle.FixedDialog; + SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.DoubleBuffer, true); + + Text = "STARTUP"; + FormBorderStyle = FormBorderStyle.None; StartPosition = FormStartPosition.CenterScreen; - MaximizeBox = false; - MinimizeBox = false; - AutoScaleMode = AutoScaleMode.Dpi; + BackColor = BackgroundDark; - var padding = 16; - var checkboxHeight = 28; - var buttonHeight = 32; - var buttonWidth = 100; - - var contentHeight = padding + (_apps.Count * checkboxHeight) + padding + buttonHeight + padding; - var contentWidth = 350; + var itemHeight = 44; + var headerHeight = 60; + var footerHeight = 70; + var padding = 20; + var contentWidth = 380; + var contentHeight = headerHeight + (_apps.Count * itemHeight) + footerHeight; ClientSize = new Size(contentWidth, contentHeight); - var yPos = padding; + var yPos = headerHeight; foreach (var app in _apps) { - var checkbox = new CheckBox + var checkbox = new RetroCheckBox { Text = app.Name, Tag = app, Checked = app.CheckedByDefault, Location = new Point(padding, yPos), - Size = new Size(contentWidth - (padding * 2), checkboxHeight), - Font = new Font("Segoe UI", 10) + Size = new Size(contentWidth - (padding * 2), itemHeight - 4) }; _checkboxes.Add(checkbox); Controls.Add(checkbox); - yPos += checkboxHeight; + yPos += itemHeight; } - yPos += padding; + var buttonWidth = 110; + var buttonHeight = 36; + var buttonY = contentHeight - footerHeight + 17; - _launchButton = new Button + var launchButton = new RetroButton { - Text = "Launch", - DialogResult = DialogResult.OK, - Location = new Point(contentWidth - padding - buttonWidth - 8 - buttonWidth, yPos), + Text = "► LAUNCH", + Location = new Point(contentWidth - padding - buttonWidth - 12 - buttonWidth, buttonY), Size = new Size(buttonWidth, buttonHeight), - Font = new Font("Segoe UI", 9) + IsPrimary = true }; - _launchButton.Click += OnLaunchClick; - Controls.Add(_launchButton); + launchButton.Click += (s, e) => { LaunchSelectedApps(); Close(); }; + Controls.Add(launchButton); - _cancelButton = new Button + var skipButton = new RetroButton { - Text = "Skip", - DialogResult = DialogResult.Cancel, - Location = new Point(contentWidth - padding - buttonWidth, yPos), + Text = "SKIP", + Location = new Point(contentWidth - padding - buttonWidth, buttonY), Size = new Size(buttonWidth, buttonHeight), - Font = new Font("Segoe UI", 9) + IsPrimary = false }; - Controls.Add(_cancelButton); + skipButton.Click += (s, e) => Close(); + Controls.Add(skipButton); - AcceptButton = _launchButton; - CancelButton = _cancelButton; + _animTimer = new System.Windows.Forms.Timer { Interval = 50 }; + _animTimer.Tick += OnAnimTick; + _animTimer.Start(); } - private void OnLaunchClick(object? sender, EventArgs e) + private void OnAnimTick(object? sender, EventArgs e) { - LaunchSelectedApps(); + _scanlineOffset = (_scanlineOffset + 0.3f) % 6f; + + _glowPulse = (float)(Math.Sin(Environment.TickCount / 800.0) * 20 + 20); + + _brightLineY = (_brightLineY + 1.5f) % (Height + 40); + + _flickerCounter++; + if (_flickerCounter >= 8) + { + _flickerCounter = 0; + _flickerTarget = Random.Shared.Next(0, 5); + } + _flickerCurrent += (_flickerTarget - _flickerCurrent) * 0.3f; + + Invalidate(); + } + + protected override void OnFormClosed(FormClosedEventArgs e) + { + _animTimer.Stop(); + _animTimer.Dispose(); + base.OnFormClosed(e); + } + + protected override void OnPaint(PaintEventArgs e) + { + base.OnPaint(e); + var g = e.Graphics; + g.SmoothingMode = SmoothingMode.AntiAlias; + + var glowAlpha = (int)(180 + _glowPulse); + var pulsingCyan = Color.FromArgb(Math.Min(255, glowAlpha), 0, 255, 255); + + using var borderPen = new Pen(pulsingCyan, 2); + g.DrawRectangle(borderPen, 1, 1, Width - 3, Height - 3); + + using var outerGlow = new Pen(Color.FromArgb((int)(20 + _glowPulse / 2), AccentCyan), 6); + g.DrawRectangle(outerGlow, -2, -2, Width + 3, Height + 3); + + using var innerBorder = new Pen(BorderColor, 1); + g.DrawRectangle(innerBorder, 4, 4, Width - 9, Height - 9); + + using var headerBrush = new LinearGradientBrush( + new Rectangle(0, 0, Width, 60), + Color.FromArgb((int)(40 + _glowPulse / 2), AccentCyan), + Color.FromArgb(5, AccentCyan), + LinearGradientMode.Vertical); + g.FillRectangle(headerBrush, 5, 5, Width - 10, 55); + + using var titleFont = new Font("Consolas", 14, FontStyle.Bold); + using var titleBrush = new SolidBrush(pulsingCyan); + var title = $"▸ {_driveName.ToUpper()}"; + g.DrawString(title, titleFont, titleBrush, 20, 15); + + using var subtitleFont = new Font("Consolas", 9); + using var subtitleBrush = new SolidBrush(TextDim); + g.DrawString("Select applications to launch", subtitleFont, subtitleBrush, 20, 38); + + using var linePen = new Pen(BorderColor, 1); + g.DrawLine(linePen, 10, 58, Width - 10, 58); + + var footerY = Height - 70; + g.DrawLine(linePen, 10, footerY, Width - 10, footerY); + + var scanOffset = (int)_scanlineOffset; + for (var y = scanOffset; y < Height; y += 3) + { + var scanAlpha = 10 + (y % 6 == scanOffset ? 5 : 0); + using var scanlinePen = new Pen(Color.FromArgb(scanAlpha, 0, 0, 0), 1); + g.DrawLine(scanlinePen, 0, y, Width, y); + } + + var brightY = (int)_brightLineY - 20; + if (brightY >= 0 && brightY < Height) + { + using var brightScanline = new Pen(Color.FromArgb(12, 255, 255, 255), 3); + g.DrawLine(brightScanline, 0, brightY, Width, brightY); + } + + var flickerAlpha = (int)_flickerCurrent; + if (flickerAlpha > 0) + { + using var flickerBrush = new SolidBrush(Color.FromArgb(flickerAlpha, 255, 255, 255)); + g.FillRectangle(flickerBrush, 0, 0, Width, Height); + } } - /// - /// Launches all selected apps. - /// private void LaunchSelectedApps() { foreach (var checkbox in _checkboxes) { - if (!checkbox.Checked) - { - continue; - } - - if (checkbox.Tag is not StartupApp app) + if (!checkbox.Checked || checkbox.Tag is not StartupApp app) { continue; } @@ -139,8 +229,6 @@ public class StartupDialog : Form /// /// Shows the startup dialog if there are apps to display. /// - /// List of startup apps. - /// Name of the drive. public static void ShowIfNeeded(List apps, string driveName) { if (apps.Count == 0) @@ -148,54 +236,215 @@ public class StartupDialog : Form return; } - if (apps.Count == 1) - { - var app = apps[0]; - var result = MessageBox.Show( - $"Launch {app.Name}?", - $"{driveName} - Startup", - MessageBoxButtons.YesNo, - MessageBoxIcon.Question - ); - - if (result == DialogResult.Yes) - { - LaunchSingleApp(app); - } - return; - } - Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); using var dialog = new StartupDialog(apps, driveName); dialog.ShowDialog(); } - private static void LaunchSingleApp(StartupApp app) + protected override void OnMouseDown(MouseEventArgs e) { - try + base.OnMouseDown(e); + if (e.Button == MouseButtons.Left && e.Y < 60) { - var startInfo = new ProcessStartInfo - { - FileName = Environment.ExpandEnvironmentVariables(app.Path), - UseShellExecute = true - }; - - if (!string.IsNullOrWhiteSpace(app.Arguments)) - { - startInfo.Arguments = Environment.ExpandEnvironmentVariables(app.Arguments); - } - - if (!string.IsNullOrWhiteSpace(app.WorkingDirectory)) - { - startInfo.WorkingDirectory = Environment.ExpandEnvironmentVariables(app.WorkingDirectory); - } - - Process.Start(startInfo); - Console.WriteLine($"[DevDrive] Launched: {app.Name}"); - } - catch (Exception ex) - { - Console.WriteLine($"[DevDrive] Failed to launch {app.Name}: {ex.Message}"); + NativeMethods.ReleaseCapture(); + NativeMethods.SendMessage(Handle, 0xA1, 0x2, 0); } } + + private static class NativeMethods + { + [System.Runtime.InteropServices.DllImport("user32.dll")] + public static extern int SendMessage(IntPtr hWnd, int Msg, int wParam, int lParam); + + [System.Runtime.InteropServices.DllImport("user32.dll")] + public static extern bool ReleaseCapture(); + } +} + +/// +/// Custom retro-styled checkbox control. +/// +[SupportedOSPlatform("windows")] +public class RetroCheckBox : Control +{ + private static readonly Color BackgroundDark = Color.FromArgb(24, 24, 32); + private static readonly Color AccentCyan = Color.FromArgb(0, 255, 255); + private static readonly Color AccentMagenta = Color.FromArgb(255, 0, 128); + private static readonly Color TextLight = Color.FromArgb(240, 240, 255); + private static readonly Color BorderColor = Color.FromArgb(60, 60, 80); + + private bool _checked; + private bool _hovering; + + public bool Checked + { + get => _checked; + set { _checked = value; Invalidate(); } + } + + public RetroCheckBox() + { + SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.DoubleBuffer, true); + Size = new Size(200, 40); + Cursor = Cursors.Hand; + } + + protected override void OnPaint(PaintEventArgs e) + { + var g = e.Graphics; + g.SmoothingMode = SmoothingMode.AntiAlias; + + var bgColor = _hovering ? Color.FromArgb(40, 40, 56) : Color.FromArgb(28, 28, 40); + using var bgBrush = new SolidBrush(bgColor); + g.FillRectangle(bgBrush, 0, 0, Width, Height); + + var boxSize = 20; + var boxX = 8; + var boxY = (Height - boxSize) / 2; + + var boxBorderColor = _checked ? AccentCyan : BorderColor; + using var boxPen = new Pen(boxBorderColor, _checked ? 2 : 1); + g.DrawRectangle(boxPen, boxX, boxY, boxSize, boxSize); + + if (_checked) + { + using var checkBrush = new SolidBrush(AccentCyan); + var innerPadding = 5; + g.FillRectangle(checkBrush, boxX + innerPadding, boxY + innerPadding, + boxSize - (innerPadding * 2) + 1, boxSize - (innerPadding * 2) + 1); + + using var glowPen = new Pen(Color.FromArgb(60, AccentCyan), 4); + g.DrawRectangle(glowPen, boxX - 1, boxY - 1, boxSize + 2, boxSize + 2); + } + + using var textFont = new Font("Consolas", 11, FontStyle.Bold); + var textColor = _checked ? TextLight : Color.FromArgb(160, 160, 180); + using var textBrush = new SolidBrush(textColor); + var textX = boxX + boxSize + 14; + var textY = (Height - textFont.Height) / 2; + g.DrawString(Text, textFont, textBrush, textX, textY); + + if (_checked) + { + using var accentPen = new Pen(AccentCyan, 2); + g.DrawLine(accentPen, 0, Height - 1, Width, Height - 1); + } + } + + protected override void OnMouseEnter(EventArgs e) + { + _hovering = true; + Invalidate(); + base.OnMouseEnter(e); + } + + protected override void OnMouseLeave(EventArgs e) + { + _hovering = false; + Invalidate(); + base.OnMouseLeave(e); + } + + protected override void OnClick(EventArgs e) + { + Checked = !Checked; + base.OnClick(e); + } +} + +/// +/// Custom retro-styled button control. +/// +[SupportedOSPlatform("windows")] +public class RetroButton : Control +{ + private static readonly Color AccentCyan = Color.FromArgb(0, 255, 255); + private static readonly Color AccentMagenta = Color.FromArgb(255, 0, 128); + private static readonly Color TextLight = Color.FromArgb(240, 240, 255); + private static readonly Color BorderColor = Color.FromArgb(60, 60, 80); + + private bool _hovering; + private bool _pressing; + + public bool IsPrimary { get; set; } + + public RetroButton() + { + SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.DoubleBuffer, true); + Size = new Size(100, 36); + Cursor = Cursors.Hand; + } + + protected override void OnPaint(PaintEventArgs e) + { + var g = e.Graphics; + g.SmoothingMode = SmoothingMode.AntiAlias; + + var accentColor = IsPrimary ? AccentCyan : AccentMagenta; + + Color bgColor; + if (_pressing) + { + bgColor = Color.FromArgb(60, accentColor); + } + else if (_hovering) + { + bgColor = Color.FromArgb(30, accentColor); + } + else + { + bgColor = Color.FromArgb(10, accentColor); + } + + using var bgBrush = new SolidBrush(bgColor); + g.FillRectangle(bgBrush, 0, 0, Width, Height); + + var borderWidth = _hovering ? 2 : 1; + using var borderPen = new Pen(accentColor, borderWidth); + g.DrawRectangle(borderPen, borderWidth / 2, borderWidth / 2, + Width - borderWidth, Height - borderWidth); + + if (_hovering && IsPrimary) + { + using var glowPen = new Pen(Color.FromArgb(40, accentColor), 4); + g.DrawRectangle(glowPen, -1, -1, Width + 1, Height + 1); + } + + using var textFont = new Font("Consolas", 10, FontStyle.Bold); + using var textBrush = new SolidBrush(accentColor); + var textSize = g.MeasureString(Text, textFont); + var textX = (Width - textSize.Width) / 2; + var textY = (Height - textSize.Height) / 2; + g.DrawString(Text, textFont, textBrush, textX, textY); + } + + protected override void OnMouseEnter(EventArgs e) + { + _hovering = true; + Invalidate(); + base.OnMouseEnter(e); + } + + protected override void OnMouseLeave(EventArgs e) + { + _hovering = false; + _pressing = false; + Invalidate(); + base.OnMouseLeave(e); + } + + protected override void OnMouseDown(MouseEventArgs e) + { + _pressing = true; + Invalidate(); + base.OnMouseDown(e); + } + + protected override void OnMouseUp(MouseEventArgs e) + { + _pressing = false; + Invalidate(); + base.OnMouseUp(e); + } }