// Light/dark palette and the owner-drawn controls of the programmer GUI. // Stock WinForms controls (tabs, buttons, check boxes, progress bar) are // drawn by the light Windows theme only, so these paint themselves. using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Runtime.InteropServices; using System.Windows.Forms; using Microsoft.Win32; namespace TangProgrammer { enum Tone { Text, Dim, Ok, Err, Warn, Title } sealed class Theme { public bool IsDark; public Color Window, Card, CardBorder, LogBg; public Color Text, TextDim, TextDisabled; public Color Button, ButtonHover, ButtonPressed, ButtonBorder, ButtonDisabled; public Color Accent, AccentHover, AccentPressed, AccentText, AccentDisabled, AccentTextDisabled; public Color ToggleFill, ToggleBorder; public Color Input, Track; public Color Ok, Err, Warn, Title; public Color DotOn, DotWarn, DotOff; public Color MenuBg, MenuHover, MenuBorder; public static readonly Theme Light = new Theme { Window = Rgb(0xF3F3F3), Card = Rgb(0xFFFFFF), CardBorder = Rgb(0xE3E3E3), LogBg = Rgb(0xFFFFFF), Text = Rgb(0x1B1B1B), TextDim = Rgb(0x626262), TextDisabled = Rgb(0xA3A3A3), Button = Rgb(0xFBFBFB), ButtonHover = Rgb(0xF3F3F3), ButtonPressed = Rgb(0xEBEBEB), ButtonBorder = Rgb(0xD3D3D3), ButtonDisabled = Rgb(0xF5F5F5), Accent = Rgb(0x0067C0), AccentHover = Rgb(0x1975C5), AccentPressed = Rgb(0x3183CA), AccentText = Rgb(0xFFFFFF), AccentDisabled = Rgb(0xC5C5C5), AccentTextDisabled = Rgb(0xFFFFFF), ToggleFill = Rgb(0xFAFAFA), ToggleBorder = Rgb(0x8A8A8A), Input = Rgb(0xFFFFFF), Track = Rgb(0xE0E0E0), Ok = Rgb(0x0F7B0F), Err = Rgb(0xC42B1C), Warn = Rgb(0x9D5D00), Title = Rgb(0x005FB8), DotOn = Rgb(0x13A10E), DotWarn = Rgb(0xE8911A), DotOff = Rgb(0xA0A0A0), MenuBg = Rgb(0xFFFFFF), MenuHover = Rgb(0xEDEDED), MenuBorder = Rgb(0xD5D5D5), }; public static readonly Theme Dark = new Theme { IsDark = true, Window = Rgb(0x202020), Card = Rgb(0x2B2B2B), CardBorder = Rgb(0x3A3A3A), LogBg = Rgb(0x252525), Text = Rgb(0xEDEDED), TextDim = Rgb(0xA6A6A6), TextDisabled = Rgb(0x6E6E6E), Button = Rgb(0x373737), ButtonHover = Rgb(0x3E3E3E), ButtonPressed = Rgb(0x333333), ButtonBorder = Rgb(0x484848), ButtonDisabled = Rgb(0x313131), Accent = Rgb(0x4CC2FF), AccentHover = Rgb(0x48B1E8), AccentPressed = Rgb(0x42A1D2), AccentText = Rgb(0x000000), AccentDisabled = Rgb(0x454545), AccentTextDisabled = Rgb(0x9A9A9A), ToggleFill = Rgb(0x262626), ToggleBorder = Rgb(0x9E9E9E), Input = Rgb(0x383838), Track = Rgb(0x474747), Ok = Rgb(0x6CCB5F), Err = Rgb(0xFF99A4), Warn = Rgb(0xFCE100), Title = Rgb(0x60CDFF), DotOn = Rgb(0x6CCB5F), DotWarn = Rgb(0xF7B955), DotOff = Rgb(0x6E6E6E), MenuBg = Rgb(0x2C2C2C), MenuHover = Rgb(0x3D3D3D), MenuBorder = Rgb(0x484848), }; public static Theme Current = Light; static Color Rgb(int rgb) { return Color.FromArgb((rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF); } public Color Of(Tone tone) { switch (tone) { case Tone.Dim: return TextDim; case Tone.Ok: return Ok; case Tone.Err: return Err; case Tone.Warn: return Warn; case Tone.Title: return Title; default: return Text; } } // "Choose your app mode" in the Windows settings public static bool SystemPrefersDark() { try { using (RegistryKey k = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize")) { object v = k == null ? null : k.GetValue("AppsUseLightTheme"); return v is int && (int)v == 0; } } catch (Exception) { return false; } } } static class Ui { public static float Scale = 1f; public static int S(float px) { return (int)Math.Round(px * Scale); } public static GraphicsPath Round(RectangleF r, float radius) { var p = new GraphicsPath(); float d = Math.Min(radius * 2, Math.Min(r.Width, r.Height)); if (d < 1f) { p.AddRectangle(r); return p; } p.AddArc(r.X, r.Y, d, d, 180, 90); p.AddArc(r.Right - d, r.Y, d, d, 270, 90); p.AddArc(r.Right - d, r.Bottom - d, d, d, 0, 90); p.AddArc(r.X, r.Bottom - d, d, d, 90, 90); p.CloseFigure(); return p; } public static Color ParentBack(Control c) { return c.Parent != null ? c.Parent.BackColor : c.BackColor; } [DllImport("uxtheme.dll", CharSet = CharSet.Unicode)] static extern int SetWindowTheme(IntPtr hwnd, string subAppName, string subIdList); [DllImport("dwmapi.dll")] static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int value, int size); [DllImport("user32.dll")] static extern IntPtr SendMessage(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam); // dark scroll bars and combo boxes of native controls (Windows 10 1809+); // null restores the default look public static void SetNativeTheme(Control c, string themeClass) { SetWindowTheme(c.Handle, themeClass, null); c.Invalidate(); } public static void SetDarkTitleBar(Form f, bool dark) { int v = dark ? 1 : 0; // DWMWA_USE_IMMERSIVE_DARK_MODE: 20, was 19 before Windows 10 20H1 if (DwmSetWindowAttribute(f.Handle, 20, ref v, 4) != 0) DwmSetWindowAttribute(f.Handle, 19, ref v, 4); } public static void SetRedraw(Control c, bool on) { SendMessage(c.Handle, 0x000B /* WM_SETREDRAW */, (IntPtr)(on ? 1 : 0), IntPtr.Zero); if (on) c.Invalidate(); } } // White (or dark) rounded panel with a thin border sealed class Card : Panel { public Color Border = Color.Gray; public Card() { SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.UserPaint, true); } protected override void OnPaintBackground(PaintEventArgs e) { Graphics g = e.Graphics; g.Clear(Ui.ParentBack(this)); g.SmoothingMode = SmoothingMode.AntiAlias; using (GraphicsPath p = Ui.Round(new RectangleF(0.5f, 0.5f, Width - 1f, Height - 1f), Ui.S(7))) using (var fill = new SolidBrush(BackColor)) using (var pen = new Pen(Border)) { g.FillPath(fill, p); g.DrawPath(pen, p); } } } // Text tabs with an accent underline (replaces the TabControl, which can't be dark) sealed class TabStrip : Control { readonly List items = new List(); int selected, hot = -1; Font bold; public event EventHandler SelectedIndexChanged; public TabStrip() { SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.UserPaint, true); SetStyle(ControlStyles.Selectable, false); } public void Add(string text) { items.Add(text); Invalidate(); } public int SelectedIndex { get { return selected; } set { if (value == selected || value < 0 || value >= items.Count) return; selected = value; Invalidate(); if (SelectedIndexChanged != null) SelectedIndexChanged(this, EventArgs.Empty); } } public int Count { get { return items.Count; } } Font Bold { get { if (bold == null || bold.Size != Font.Size) bold = new Font("Segoe UI Semibold", Font.Size); return bold; } } int ItemWidth(int i) { return TextRenderer.MeasureText(items[i], Bold).Width + Ui.S(24); } Rectangle ItemRect(int i) { int x = 0; for (int k = 0; k < i; k++) x += ItemWidth(k); return new Rectangle(x, 0, ItemWidth(i), Height); } int HitTest(Point p) { for (int i = 0; i < items.Count; i++) if (ItemRect(i).Contains(p)) return i; return -1; } public override Size GetPreferredSize(Size proposed) { int w = 0; for (int i = 0; i < items.Count; i++) w += ItemWidth(i); return new Size(w, Font.Height + Ui.S(16)); } protected override void OnPaintBackground(PaintEventArgs e) { } protected override void OnPaint(PaintEventArgs e) { Theme th = Theme.Current; Graphics g = e.Graphics; g.Clear(Ui.ParentBack(this)); int line = Ui.S(3); for (int i = 0; i < items.Count; i++) { Rectangle r = ItemRect(i); bool sel = i == selected; Color c = sel || i == hot ? th.Text : th.TextDim; TextRenderer.DrawText(g, items[i], sel ? Bold : Font, new Rectangle(r.X, 0, r.Width, r.Height - line), c, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.SingleLine | TextFormatFlags.NoPrefix); if (sel) { int w = TextRenderer.MeasureText(items[i], Bold).Width - Ui.S(6); var u = new RectangleF(r.X + (r.Width - w) / 2f, r.Bottom - line - Ui.S(1), w, line); g.SmoothingMode = SmoothingMode.AntiAlias; using (GraphicsPath p = Ui.Round(u, line / 2f)) using (var b = new SolidBrush(th.Accent)) g.FillPath(b, p); g.SmoothingMode = SmoothingMode.None; } } } protected override void OnMouseMove(MouseEventArgs e) { int h = HitTest(e.Location); if (h != hot) { hot = h; Cursor = h >= 0 && h != selected ? Cursors.Hand : Cursors.Default; Invalidate(); } base.OnMouseMove(e); } protected override void OnMouseLeave(EventArgs e) { hot = -1; Invalidate(); base.OnMouseLeave(e); } protected override void OnMouseDown(MouseEventArgs e) { int h = HitTest(e.Location); if (h >= 0 && e.Button == MouseButtons.Left) { SelectedIndex = h; Cursor = Cursors.Default; } base.OnMouseDown(e); } protected override void Dispose(bool disposing) { if (disposing && bold != null) bold.Dispose(); base.Dispose(disposing); } } enum Glyph { None, Contrast } // Rounded button in the colors of the current theme; Accent = the main action sealed class FlatButton : Button { public bool Accent; public Glyph Glyph; bool hover, pressed; public FlatButton() { SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.UserPaint, true); FlatStyle = FlatStyle.Flat; FlatAppearance.BorderSize = 0; UseVisualStyleBackColor = false; } public override Size GetPreferredSize(Size proposed) { Size t = TextRenderer.MeasureText(Text, Font, Size.Empty, TextFormatFlags.SingleLine | TextFormatFlags.NoPrefix); return new Size(Math.Max(MinimumSize.Width, t.Width + Padding.Horizontal + Ui.S(8)), Math.Max(MinimumSize.Height, t.Height + Padding.Vertical + Ui.S(6))); } protected override void OnMouseEnter(EventArgs e) { hover = true; Invalidate(); base.OnMouseEnter(e); } protected override void OnMouseLeave(EventArgs e) { hover = pressed = false; Invalidate(); base.OnMouseLeave(e); } protected override void OnMouseDown(MouseEventArgs e) { if (e.Button == MouseButtons.Left) { pressed = true; Invalidate(); } base.OnMouseDown(e); } protected override void OnMouseUp(MouseEventArgs e) { pressed = false; Invalidate(); base.OnMouseUp(e); } protected override void OnEnabledChanged(EventArgs e) { hover = pressed = false; Invalidate(); base.OnEnabledChanged(e); } protected override void OnPaintBackground(PaintEventArgs e) { } protected override void OnPaint(PaintEventArgs e) { Theme th = Theme.Current; Graphics g = e.Graphics; g.Clear(Ui.ParentBack(this)); Color bg, fg, border; if (Accent) { bg = !Enabled ? th.AccentDisabled : pressed ? th.AccentPressed : hover ? th.AccentHover : th.Accent; fg = Enabled ? th.AccentText : th.AccentTextDisabled; border = bg; } else { bg = !Enabled ? th.ButtonDisabled : pressed ? th.ButtonPressed : hover ? th.ButtonHover : th.Button; fg = Enabled ? th.Text : th.TextDisabled; border = th.ButtonBorder; } g.SmoothingMode = SmoothingMode.AntiAlias; using (GraphicsPath p = Ui.Round(new RectangleF(0.5f, 0.5f, Width - 1f, Height - 1f), Ui.S(4))) using (var b = new SolidBrush(bg)) using (var pen = new Pen(border)) { g.FillPath(b, p); g.DrawPath(pen, p); } if (Focused && ShowFocusCues) { using (GraphicsPath p = Ui.Round(new RectangleF(2.5f, 2.5f, Width - 5f, Height - 5f), Ui.S(3))) using (var pen = new Pen(fg)) g.DrawPath(pen, p); } if (Glyph == Glyph.Contrast) { float d = Math.Min(Width, Height) * 0.46f; var r = new RectangleF((Width - d) / 2f, (Height - d) / 2f, d, d); using (var b = new SolidBrush(fg)) g.FillPie(b, r.X, r.Y, r.Width, r.Height, 90, 180); using (var pen = new Pen(fg, Math.Max(1.2f, Ui.Scale * 1.3f))) g.DrawEllipse(pen, r); return; } g.SmoothingMode = SmoothingMode.None; TextRenderer.DrawText(g, Text, Font, ClientRectangle, fg, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.SingleLine | TextFormatFlags.NoPrefix | TextFormatFlags.EndEllipsis); } } static class TogglePainter { const TextFormatFlags Flags = TextFormatFlags.SingleLine | TextFormatFlags.NoPrefix; static int Box(Control c) { return Math.Max(13, c.Font.Height); } public static Size Measure(ButtonBase c) { Size t = TextRenderer.MeasureText(c.Text, c.Font, Size.Empty, Flags); int box = Box(c); return new Size(box + Ui.S(8) + t.Width + Ui.S(2), Math.Max(box, t.Height) + Ui.S(4)); } public static void Paint(ButtonBase c, Graphics g, bool on, bool radio, bool hover, bool focus) { Theme th = Theme.Current; g.Clear(Ui.ParentBack(c)); int box = Box(c); var r = new RectangleF(0.5f, (c.Height - box) / 2f + 0.5f, box - 1f, box - 1f); Color accent = !c.Enabled ? th.AccentDisabled : hover ? th.AccentHover : th.Accent; Color border = !c.Enabled ? th.TextDisabled : hover ? th.Text : th.ToggleBorder; g.SmoothingMode = SmoothingMode.AntiAlias; if (radio) { if (on) { using (var b = new SolidBrush(accent)) g.FillEllipse(b, r); float d = box * 0.4f; using (var b = new SolidBrush(th.AccentText)) g.FillEllipse(b, r.X + (r.Width - d) / 2f, r.Y + (r.Height - d) / 2f, d, d); } else { using (var b = new SolidBrush(th.ToggleFill)) g.FillEllipse(b, r); using (var pen = new Pen(border)) g.DrawEllipse(pen, r); } } else { using (GraphicsPath p = Ui.Round(r, box * 0.2f)) { if (on) { using (var b = new SolidBrush(accent)) g.FillPath(b, p); using (var pen = new Pen(th.AccentText, Math.Max(1.5f, box * 0.12f))) { pen.StartCap = pen.EndCap = LineCap.Round; pen.LineJoin = LineJoin.Round; g.DrawLines(pen, new[] { new PointF(r.X + box * 0.25f, r.Y + box * 0.52f), new PointF(r.X + box * 0.43f, r.Y + box * 0.70f), new PointF(r.X + box * 0.76f, r.Y + box * 0.32f), }); } } else { using (var b = new SolidBrush(th.ToggleFill)) g.FillPath(b, p); using (var pen = new Pen(border)) g.DrawPath(pen, p); } } } g.SmoothingMode = SmoothingMode.None; var tr = new Rectangle(box + Ui.S(8), 0, Math.Max(0, c.Width - box - Ui.S(8)), c.Height); TextRenderer.DrawText(g, c.Text, c.Font, tr, c.Enabled ? th.Text : th.TextDisabled, Flags | TextFormatFlags.VerticalCenter | TextFormatFlags.Left | TextFormatFlags.EndEllipsis); if (focus) { Size t = TextRenderer.MeasureText(c.Text, c.Font, Size.Empty, Flags); ControlPaint.DrawFocusRectangle(g, new Rectangle(tr.X - 1, (c.Height - t.Height) / 2, t.Width + 2, t.Height), th.Text, Ui.ParentBack(c)); } } } sealed class ThemedCheck : CheckBox { bool hover; public ThemedCheck() { SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.UserPaint, true); } public override Size GetPreferredSize(Size proposed) { return TogglePainter.Measure(this); } protected override void OnPaintBackground(PaintEventArgs e) { } protected override void OnPaint(PaintEventArgs e) { TogglePainter.Paint(this, e.Graphics, Checked, false, hover, Focused && ShowFocusCues); } protected override void OnMouseEnter(EventArgs e) { hover = true; Invalidate(); base.OnMouseEnter(e); } protected override void OnMouseLeave(EventArgs e) { hover = false; Invalidate(); base.OnMouseLeave(e); } } sealed class ThemedRadio : RadioButton { bool hover; public ThemedRadio() { SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.UserPaint, true); } public override Size GetPreferredSize(Size proposed) { return TogglePainter.Measure(this); } protected override void OnPaintBackground(PaintEventArgs e) { } protected override void OnPaint(PaintEventArgs e) { TogglePainter.Paint(this, e.Graphics, Checked, true, hover, Focused && ShowFocusCues); } protected override void OnMouseEnter(EventArgs e) { hover = true; Invalidate(); base.OnMouseEnter(e); } protected override void OnMouseLeave(EventArgs e) { hover = false; Invalidate(); base.OnMouseLeave(e); } } enum BarState { Running, Ok, Error, Paused } // Thin rounded progress bar; Marquee = "busy, amount unknown" sealed class FlatProgress : Control { int value; bool marquee; BarState state; float phase; readonly Timer timer = new Timer { Interval = 15 }; public FlatProgress() { SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.UserPaint, true); SetStyle(ControlStyles.Selectable, false); timer.Tick += (s, e) => { phase += 0.009f; if (phase > 1f) phase -= 1f; Invalidate(); }; } public int Value { get { return value; } set { this.value = Math.Max(0, Math.Min(100, value)); Invalidate(); } } public bool Marquee { get { return marquee; } set { marquee = value; phase = 0; timer.Enabled = value; Invalidate(); } } public BarState State { get { return state; } set { state = value; Invalidate(); } } protected override void OnPaintBackground(PaintEventArgs e) { } protected override void OnPaint(PaintEventArgs e) { Theme th = Theme.Current; Graphics g = e.Graphics; g.Clear(Ui.ParentBack(this)); g.SmoothingMode = SmoothingMode.AntiAlias; float h = Math.Min(Height, Ui.S(6)); var track = new RectangleF(0, (Height - h) / 2f, Width, h); using (GraphicsPath p = Ui.Round(track, h / 2f)) using (var b = new SolidBrush(th.Track)) g.FillPath(b, p); RectangleF bar; if (marquee) { float seg = Width * 0.28f; // ease in-out sweep across the track float t = (float)(0.5 - Math.Cos(phase * Math.PI) / 2); bar = RectangleF.Intersect(new RectangleF(-seg + t * (Width + seg), track.Y, seg, h), track); } else { bar = new RectangleF(0, track.Y, Width * value / 100f, h); } if (bar.Width < 0.5f) return; Color fill = state == BarState.Ok ? th.Ok : state == BarState.Error ? th.Err : state == BarState.Paused ? th.Warn : th.Accent; using (GraphicsPath p = Ui.Round(bar, h / 2f)) using (var b = new SolidBrush(fill)) g.FillPath(b, p); } protected override void Dispose(bool disposing) { if (disposing) timer.Dispose(); base.Dispose(disposing); } } sealed class StatusDot : Control { Color dotColor = Color.Silver; public StatusDot() { SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.UserPaint, true); } public Color DotColor { get { return dotColor; } set { dotColor = value; Invalidate(); } } protected override void OnPaintBackground(PaintEventArgs e) { } protected override void OnPaint(PaintEventArgs e) { e.Graphics.Clear(Ui.ParentBack(this)); e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; using (var b = new SolidBrush(dotColor)) e.Graphics.FillEllipse(b, 0.5f, 0.5f, Width - 1.5f, Height - 1.5f); } } // Context menus in the colors of the current theme sealed class MenuRenderer : ToolStripProfessionalRenderer { public MenuRenderer() : base(new MenuColors()) { RoundedEdges = false; } protected override void OnRenderItemText(ToolStripItemTextRenderEventArgs e) { e.TextColor = e.Item.Enabled ? Theme.Current.Text : Theme.Current.TextDisabled; base.OnRenderItemText(e); } protected override void OnRenderItemCheck(ToolStripItemImageRenderEventArgs e) { Rectangle r = e.ImageRectangle; float s = Math.Min(r.Width, r.Height); float x = r.X + (r.Width - s) / 2f, y = r.Y + (r.Height - s) / 2f; e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; using (var pen = new Pen(Theme.Current.Text, Math.Max(1.5f, s * 0.11f))) { pen.StartCap = pen.EndCap = LineCap.Round; pen.LineJoin = LineJoin.Round; e.Graphics.DrawLines(pen, new[] { new PointF(x + s * 0.22f, y + s * 0.52f), new PointF(x + s * 0.42f, y + s * 0.72f), new PointF(x + s * 0.78f, y + s * 0.30f), }); } e.Graphics.SmoothingMode = SmoothingMode.None; } sealed class MenuColors : ProfessionalColorTable { public MenuColors() { UseSystemColors = false; } static Theme T { get { return Theme.Current; } } public override Color ToolStripDropDownBackground { get { return T.MenuBg; } } public override Color ImageMarginGradientBegin { get { return T.MenuBg; } } public override Color ImageMarginGradientMiddle { get { return T.MenuBg; } } public override Color ImageMarginGradientEnd { get { return T.MenuBg; } } public override Color MenuBorder { get { return T.MenuBorder; } } public override Color MenuItemBorder { get { return T.MenuHover; } } public override Color MenuItemSelected { get { return T.MenuHover; } } public override Color MenuItemSelectedGradientBegin { get { return T.MenuHover; } } public override Color MenuItemSelectedGradientEnd { get { return T.MenuHover; } } public override Color SeparatorDark { get { return T.MenuBorder; } } public override Color SeparatorLight { get { return T.MenuBg; } } public override Color CheckBackground { get { return T.MenuBg; } } public override Color CheckSelectedBackground { get { return T.MenuHover; } } public override Color CheckPressedBackground { get { return T.MenuHover; } } } } }