From 60197b6f921793232687a211a62680ef119acfb8 Mon Sep 17 00:00:00 2001 From: bloom <108045000+studiobloom@users.noreply.github.com> Date: Thu, 2 Apr 2026 04:36:44 -0400 Subject: [PATCH] Gadget Styling (Background Color) (#2282) * Add background tinting and color picker to gadget -Replaced barblue.png and gadget.png with desaturated versions. -Added background tinting support (_backTinted, _backgroundColor, _backgroundDirty), switched _barFore to use bar.png, and dispose tinted background resources during cleanup. -Reworked font color selection UX and mirrored the same chooser flow for background color. * Fix Gadget bars and HSV color picker UI + Icon -Fixed gadget bars to tint with selected background color -Updated color picker flow with a themed, resizable HSV picker. -Applied LibreHardwareMonitor icon and current app theme to the picker window. * Hex values and finalize color tinting -Added Copy/Paste to hex value field. -Fixed color tinting to respect true grayscale/dark hex values (e.g. #000000) . -Utilization bars now follow font color tinting. * Utilization bars fix -Fixed utilization bar tinting to follow the selected font color directly * Github Copilot Suggestions -Forced grayscale-safe background tint behavior, switched background tint processing to LockBits. -Preserved custom bar override images by skipping tint on user-provided bar assets. -Fixed PArgb premultiplication in tint output to remove edge halo artifacts. * Restrict workingSource to Format32bppArgb -Normalized CreateBackgroundTint sources to Format32bppArgb before LockBits so premultiplied PArgb images are not processed with non-premultiplied ARGB tint math. * Windows accessibility text scaling -Adjusted picker layout to scale correctly when Windows Text Size is increased in Accessibility settings. --- LibreHardwareMonitor/Resources/bar.png | Bin 0 -> 1011 bytes LibreHardwareMonitor/Resources/barblue.png | Bin 571 -> 0 bytes LibreHardwareMonitor/Resources/gadget.png | Bin 5969 -> 7765 bytes LibreHardwareMonitor/UI/SensorGadget.cs | 601 ++++++++++++++++++++- 4 files changed, 577 insertions(+), 24 deletions(-) create mode 100644 LibreHardwareMonitor/Resources/bar.png delete mode 100644 LibreHardwareMonitor/Resources/barblue.png diff --git a/LibreHardwareMonitor/Resources/bar.png b/LibreHardwareMonitor/Resources/bar.png new file mode 100644 index 0000000000000000000000000000000000000000..7d7bb663d7e1594aebd4c758e10e59e847e10bbb GIT binary patch literal 1011 zcmVHdV1<+ z&$I6N`MI0VUx0bSOSS{i(g}W@bX^avWqpSBLl5P<27OxSTeZqT*8%j^b~%R{t!+EZ!oNCesOy0o zcJH_cX$<#jK`5Pja-S7S?=xe8}q8 zN*0S(#p~jyESF1pTfQk)tCc9;SMvV;F6;GLu~~0qv)RaYyS>A1yOZs1D?8=kBkp}_ z$(1}~0^Fcxx7+1<=7`_gGtJt`Z)nx0C3F^b=!@DdK`!SwpV)5Ehdkb-_3*hv!dmy@ zj@inFmma zrXB-`fFn6P#(58om!K9J+VWV>IHV=;NNSw-13i57@Z=nP+Tg^~Cop4Jqr7s^lseQn zorC%thrCA&{#Jj>kxy_Q7ISzkPamMqBL(V#fSq{@-sT5xM9?SaoRfb500960?!p$& h00006Nklu4^P)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2ige+ z5(zIdyID#A000?uMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HM@dakSAh-}0004&Nklu>{#*^u&4y;sGY|5uy7`mBJluIaW}CnFPu;3 zB>MF2yXV7!_fw$~lp_Amzo&Pzko-P%o*o{kM+NiKBR&NwQ6!`bq7lNFbuZ#|3uOXq z?<{<=ut^)+;N+;F9`Aoqmd*(%5_Ls$?7$Qlai&XM5ck;EIQ9i7Jn(V_#CZWtBg_JH zn@8YS3st@2-Zy6>2v7!&0+|?NiN3;_d;z$>#5K*|clTMr3HFvgWe1YsJ5Cvppnm!M zm7hQE$>-CSI+a31K&hw{6a|GqF%q|?avZ+$O0TIqr|n!%r*V#O(HAmvtk*O~oA~hY zjyku-+cy)>Au^Lp5Cy3q3Q&=SQUer$cYwb3k6zh9j<%5twlOivED`ka42TxAxiPm! zEpubKxk052k2jJ>br^@YN;txYE=nAJaY6$=U002ov JPDHLkV1htN?c4wW diff --git a/LibreHardwareMonitor/Resources/gadget.png b/LibreHardwareMonitor/Resources/gadget.png index 94c3d9943629faf074be6f6a61f0bc2589827b0f..1ec1e528773de816ec33b3301112f7819b2619e7 100644 GIT binary patch literal 7765 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 providedColors = Enum.GetValues(typeof(KnownColor)) - .Cast() - .Select(x => Color.FromKnownColor(x)) - .Where(x => !x.IsSystemColor && x.Name.Length < 7); - ToolStripMenuItem fontColorMenu = new ToolStripMenuItem("Font Color"); - foreach (Color color in providedColors) + ToolStripItem chooseFontColorItem = new ToolStripMenuItem("Choose..."); + chooseFontColorItem.Click += delegate { - ToolStripItem item = new ToolStripMenuItem(color.Name) { Checked = fontColorArgb == color.ToArgb() }; - item.Click += delegate + if (TrySelectColor(fontColor, out Color selectedColor)) { - SetFontColor(color); - settings.SetValue("sensorGadget.FontColor", color); - foreach (ToolStripMenuItem mi in fontColorMenu.DropDownItems) - mi.Checked = mi == item; - }; - fontColorMenu.DropDownItems.Add(item); - } + fontColor = selectedColor; + SetFontColor(fontColor); + settings.SetValue("sensorGadget.FontColor", fontColor); + Redraw(); + } + }; + fontColorMenu.DropDownItems.Add(chooseFontColorItem); + + ToolStripItem defaultFontColorItem = new ToolStripMenuItem("Default"); + defaultFontColorItem.Click += delegate + { + fontColor = Color.White; + SetFontColor(fontColor); + settings.Remove("sensorGadget.FontColor"); + Redraw(); + }; + fontColorMenu.DropDownItems.Add(defaultFontColorItem); contextMenuStrip.Items.Add(fontColorMenu); + + Color backgroundColor = settings.GetValue("sensorGadget.BackgroundColor", Color.FromArgb(0)); + SetBackgroundColor(backgroundColor); + + ToolStripMenuItem backgroundColorMenu = new ToolStripMenuItem("Background Color"); + ToolStripItem chooseBackgroundItem = new ToolStripMenuItem("Choose..."); + chooseBackgroundItem.Click += delegate + { + if (TrySelectColor(backgroundColor.A == 0 ? Color.White : backgroundColor, out Color selectedColor)) + { + backgroundColor = selectedColor; + SetBackgroundColor(backgroundColor); + settings.SetValue("sensorGadget.BackgroundColor", backgroundColor); + } + }; + backgroundColorMenu.DropDownItems.Add(chooseBackgroundItem); + + ToolStripItem defaultBackgroundItem = new ToolStripMenuItem("Default"); + defaultBackgroundItem.Click += delegate + { + Color color = Color.FromArgb(0); + backgroundColor = color; + SetBackgroundColor(color); + settings.Remove("sensorGadget.BackgroundColor"); + }; + backgroundColorMenu.DropDownItems.Add(defaultBackgroundItem); + contextMenuStrip.Items.Add(backgroundColorMenu); contextMenuStrip.Items.Add(new ToolStripSeparator()); ToolStripMenuItem lockItem = new ToolStripMenuItem("Lock Position and Size"); contextMenuStrip.Items.Add(lockItem); @@ -306,6 +349,22 @@ public class SensorGadget : Gadget _barBack.Dispose(); _barBack = null; + if (_backTinted != null) + { + _backTinted.Dispose(); + _backTinted = null; + } + if (_barForeTinted != null) + { + _barForeTinted.Dispose(); + _barForeTinted = null; + } + if (_barBackTinted != null) + { + _barBackTinted.Dispose(); + _barBackTinted = null; + } + _background.Dispose(); _background = null; @@ -461,8 +520,498 @@ public class SensorGadget : Gadget private void SetFontColor(Color color) { + _fontColor = color; _textBrush?.Dispose(); _textBrush = new SolidBrush(color); + + _barBackTinted?.Dispose(); + _barBackTinted = _customBarBack ? null : CreateBarTint(_barBack, _fontColor); + _barForeTinted?.Dispose(); + _barForeTinted = _customBarFore ? null : CreateBarTint(_barFore, _fontColor); + } + + private void SetBackgroundColor(Color color) + { + _backgroundColor = color; + _backTinted?.Dispose(); + _backTinted = null; + + // Transparent means "Default" and keeps the embedded/custom image as-is. + if (_backgroundColor.A > 0) + { + _backTinted = CreateBackgroundTint(_back, _backgroundColor); + } + + _backgroundDirty = true; + Redraw(); + } + + private static Image CreateBackgroundTint(Image source, Color targetColor) + { + Bitmap sourceBitmap = new Bitmap(source); + Rectangle rect = new Rectangle(0, 0, sourceBitmap.Width, sourceBitmap.Height); + Bitmap workingSource = sourceBitmap.PixelFormat == PixelFormat.Format32bppArgb + ? sourceBitmap + : sourceBitmap.Clone(rect, PixelFormat.Format32bppArgb); + Bitmap result = new Bitmap(workingSource.Width, workingSource.Height, PixelFormat.Format32bppPArgb); + + float targetHue = targetColor.GetHue() / 360f; + float targetSaturation = targetColor.GetSaturation(); + float targetValue = GetColorValue(targetColor); + bool isGrayscaleTarget = targetSaturation <= 0.001f; + + BitmapData srcData = null; + BitmapData dstData = null; + try + { + srcData = workingSource.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); + dstData = result.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format32bppPArgb); + + int srcLength = srcData.Stride * workingSource.Height; + int dstLength = dstData.Stride * result.Height; + byte[] srcBuffer = new byte[srcLength]; + byte[] dstBuffer = new byte[dstLength]; + Marshal.Copy(srcData.Scan0, srcBuffer, 0, srcLength); + + for (int y = 0; y < workingSource.Height; y++) + { + int srcRow = y * srcData.Stride; + int dstRow = y * dstData.Stride; + for (int x = 0; x < workingSource.Width; x++) + { + int srcIndex = srcRow + (x * 4); + int dstIndex = dstRow + (x * 4); + byte b = srcBuffer[srcIndex + 0]; + byte g = srcBuffer[srcIndex + 1]; + byte r = srcBuffer[srcIndex + 2]; + byte a = srcBuffer[srcIndex + 3]; + + if (a == 0) + { + // For PArgb targets, keep fully transparent pixels black. + dstBuffer[dstIndex + 0] = 0; + dstBuffer[dstIndex + 1] = 0; + dstBuffer[dstIndex + 2] = 0; + dstBuffer[dstIndex + 3] = 0; + continue; + } + + Color c = Color.FromArgb(a, r, g, b); + float sourceSaturation = c.GetSaturation(); + float sourceValue = c.GetBrightness(); + float saturation = isGrayscaleTarget + ? 0f + : Math.Min(1f, Math.Max(sourceSaturation * 0.25f, targetSaturation * 0.85f)); + float value = Math.Min(1f, (sourceValue * 0.55f) + (targetValue * 0.45f)); + Color tinted = ColorFromHsv(targetHue, saturation, value, a); + + // Destination format is 32bppPArgb, so RGB must be premultiplied by A. + dstBuffer[dstIndex + 0] = (byte)((tinted.B * tinted.A + 127) / 255); + dstBuffer[dstIndex + 1] = (byte)((tinted.G * tinted.A + 127) / 255); + dstBuffer[dstIndex + 2] = (byte)((tinted.R * tinted.A + 127) / 255); + dstBuffer[dstIndex + 3] = tinted.A; + } + } + + Marshal.Copy(dstBuffer, 0, dstData.Scan0, dstLength); + } + finally + { + if (srcData != null) + workingSource.UnlockBits(srcData); + if (dstData != null) + result.UnlockBits(dstData); + + if (!ReferenceEquals(workingSource, sourceBitmap)) + workingSource.Dispose(); + + sourceBitmap.Dispose(); + } + + return result; + } + + private static Image CreateBarTint(Image source, Color targetColor) + { + Bitmap sourceBitmap = new Bitmap(source); + Bitmap result = new Bitmap(sourceBitmap.Width, sourceBitmap.Height, PixelFormat.Format32bppPArgb); + + for (int y = 0; y < sourceBitmap.Height; y++) + { + for (int x = 0; x < sourceBitmap.Width; x++) + { + Color c = sourceBitmap.GetPixel(x, y); + if (c.A == 0) + { + result.SetPixel(x, y, c); + continue; + } + + // Bars should follow selected text color directly. + // Preserve source alpha for antialiasing/edge transparency. + Color tinted = Color.FromArgb(c.A, targetColor.R, targetColor.G, targetColor.B); + result.SetPixel(x, y, tinted); + } + } + + sourceBitmap.Dispose(); + return result; + } + + private static Color ColorFromHsv(float hue, float saturation, float value, int alpha) + { + if (saturation <= 0) + { + int v = (int)Math.Round(value * 255); + return Color.FromArgb(alpha, v, v, v); + } + + float h = (hue % 1.0f + 1.0f) % 1.0f * 6.0f; + int i = (int)Math.Floor(h); + float f = h - i; + float p = value * (1 - saturation); + float q = value * (1 - saturation * f); + float t = value * (1 - saturation * (1 - f)); + + (float r, float g, float b) = i switch + { + 0 => (value, t, p), + 1 => (q, value, p), + 2 => (p, value, t), + 3 => (p, q, value), + 4 => (t, p, value), + _ => (value, p, q) + }; + + return Color.FromArgb(alpha, + (int)Math.Round(r * 255), + (int)Math.Round(g * 255), + (int)Math.Round(b * 255)); + } + + private static bool TrySelectColor(Color initialColor, out Color selectedColor) + { + using Form form = new Form + { + Text = "Select Color", + FormBorderStyle = FormBorderStyle.Sizable, + StartPosition = FormStartPosition.CenterScreen, + MinimizeBox = false, + MaximizeBox = false, + ClientSize = new Size(460, 340), + MinimumSize = new Size(420, 320), + AutoScaleMode = AutoScaleMode.Font + }; + form.Icon = EmbeddedResources.GetIcon("icon.ico"); + + Panel svBox = new Panel + { + BorderStyle = BorderStyle.FixedSingle, + Cursor = Cursors.Cross + }; + Panel hueBox = new Panel + { + BorderStyle = BorderStyle.FixedSingle, + Cursor = Cursors.Cross + }; + SetDoubleBuffered(svBox); + SetDoubleBuffered(hueBox); + SetDoubleBuffered(form); + + float currentHue = initialColor.GetHue() / 360f; + float currentSaturation = initialColor.GetSaturation(); + float currentValue = GetColorValue(initialColor); + Color current = ColorFromHsv(currentHue, currentSaturation, currentValue, 255); + + int svSelectorX = 0; + int svSelectorY = 0; + int hueSelectorY = 0; + + Panel preview = new Panel + { + Size = new Size(34, 34), + BorderStyle = BorderStyle.FixedSingle, + BackColor = current + }; + + Label hexLabel = new Label + { + AutoSize = true, + Location = new Point(0, 0), + Text = "Hex:" + }; + TextBox hexTextBox = new TextBox + { + Width = 110, + Text = $"#{current.R:X2}{current.G:X2}{current.B:X2}" + }; + + Button okButton = new Button + { + Text = "OK", + DialogResult = DialogResult.OK, + AutoSize = true, + AutoSizeMode = AutoSizeMode.GrowAndShrink, + MinimumSize = new Size(75, 0), + Padding = new Padding(6, 2, 6, 2) + }; + Button cancelButton = new Button + { + Text = "Cancel", + DialogResult = DialogResult.Cancel, + AutoSize = true, + AutoSizeMode = AutoSizeMode.GrowAndShrink, + MinimumSize = new Size(75, 0), + Padding = new Padding(6, 2, 6, 2) + }; + bool updatingHexText = false; + + void LayoutControls() + { + const int padding = 12; + const int hueBarWidth = 24; + const int hueGap = 8; + int bottomRowHeight = Math.Max( + Math.Max(preview.Height, Math.Max(okButton.Height, cancelButton.Height)), + Math.Max(hexTextBox.PreferredSize.Height, hexLabel.Height)) + 8; + int rowTop = form.ClientSize.Height - padding - bottomRowHeight; + + svBox.Location = new Point(padding, padding); + svBox.Size = new Size( + Math.Max(200, form.ClientSize.Width - (2 * padding) - hueBarWidth - hueGap), + Math.Max(120, form.ClientSize.Height - (3 * padding) - bottomRowHeight)); + hueBox.Location = new Point(svBox.Right + hueGap, padding); + hueBox.Size = new Size(hueBarWidth, svBox.Height); + + preview.Location = new Point(padding, rowTop + (bottomRowHeight - preview.Height) / 2); + hexLabel.Location = new Point(preview.Right + 10, rowTop + (bottomRowHeight - hexLabel.Height) / 2); + hexTextBox.Location = new Point(hexLabel.Right + 6, rowTop + (bottomRowHeight - hexTextBox.Height) / 2); + + cancelButton.Location = new Point(form.ClientSize.Width - padding - cancelButton.Width, rowTop + (bottomRowHeight - cancelButton.Height) / 2); + okButton.Location = new Point(cancelButton.Left - 8 - okButton.Width, rowTop + (bottomRowHeight - okButton.Height) / 2); + } + + void SyncSelectorsFromCurrent() + { + svSelectorX = Math.Min(Math.Max(0, (int)Math.Round(currentSaturation * Math.Max(1, svBox.ClientSize.Width - 1))), Math.Max(0, svBox.ClientSize.Width - 1)); + svSelectorY = Math.Min(Math.Max(0, (int)Math.Round((1f - currentValue) * Math.Max(1, svBox.ClientSize.Height - 1))), Math.Max(0, svBox.ClientSize.Height - 1)); + hueSelectorY = Math.Min(Math.Max(0, (int)Math.Round(currentHue * Math.Max(1, hueBox.ClientSize.Height - 1))), Math.Max(0, hueBox.ClientSize.Height - 1)); + } + + void UpdateCurrentColor() + { + current = ColorFromHsv(currentHue, currentSaturation, currentValue, 255); + preview.BackColor = current; + updatingHexText = true; + hexTextBox.Text = $"#{current.R:X2}{current.G:X2}{current.B:X2}"; + updatingHexText = false; + } + + void UpdateSvFromPoint(Point p) + { + svSelectorX = Math.Min(Math.Max(0, p.X), Math.Max(0, svBox.ClientSize.Width - 1)); + svSelectorY = Math.Min(Math.Max(0, p.Y), Math.Max(0, svBox.ClientSize.Height - 1)); + currentSaturation = svSelectorX / (float)Math.Max(1, svBox.ClientSize.Width - 1); + currentValue = 1f - svSelectorY / (float)Math.Max(1, svBox.ClientSize.Height - 1); + UpdateCurrentColor(); + svBox.Invalidate(); + } + + void UpdateHueFromPoint(Point p) + { + int newHueSelectorY = Math.Min(Math.Max(0, p.Y), Math.Max(0, hueBox.ClientSize.Height - 1)); + if (newHueSelectorY == hueSelectorY) + return; + + hueSelectorY = newHueSelectorY; + currentHue = hueSelectorY / (float)Math.Max(1, hueBox.ClientSize.Height - 1); + UpdateCurrentColor(); + svBox.Invalidate(); + hueBox.Invalidate(); + } + + bool draggingSv = false; + bool draggingHue = false; + svBox.MouseDown += delegate(object sender, MouseEventArgs e) + { + if (e.Button != MouseButtons.Left) + return; + draggingSv = true; + UpdateSvFromPoint(e.Location); + }; + svBox.MouseMove += delegate(object sender, MouseEventArgs e) + { + if (draggingSv) + UpdateSvFromPoint(e.Location); + }; + svBox.MouseUp += delegate { draggingSv = false; }; + + hueBox.MouseDown += delegate(object sender, MouseEventArgs e) + { + if (e.Button != MouseButtons.Left) + return; + draggingHue = true; + UpdateHueFromPoint(e.Location); + }; + hueBox.MouseMove += delegate(object sender, MouseEventArgs e) + { + if (draggingHue) + UpdateHueFromPoint(e.Location); + }; + hueBox.MouseUp += delegate { draggingHue = false; }; + + svBox.Paint += delegate(object sender, PaintEventArgs e) + { + Rectangle rect = svBox.ClientRectangle; + if (rect.Width <= 1 || rect.Height <= 1) + return; + + rect.Width -= 1; + rect.Height -= 1; + e.Graphics.SmoothingMode = SmoothingMode.None; + + using (SolidBrush hueBrush = new SolidBrush(ColorFromHsv(currentHue, 1f, 1f, 255))) + e.Graphics.FillRectangle(hueBrush, rect); + + using (LinearGradientBrush satBrush = new LinearGradientBrush(rect, Color.White, Color.Transparent, LinearGradientMode.Horizontal)) + { + ColorBlend satBlend = new ColorBlend + { + Positions = new[] { 0f, 1f }, + Colors = new[] { Color.FromArgb(255, 255, 255, 255), Color.FromArgb(0, 255, 255, 255) } + }; + satBrush.InterpolationColors = satBlend; + e.Graphics.FillRectangle(satBrush, rect); + } + + using (LinearGradientBrush valueBrush = new LinearGradientBrush(rect, Color.Transparent, Color.Black, LinearGradientMode.Vertical)) + { + ColorBlend valueBlend = new ColorBlend + { + Positions = new[] { 0f, 1f }, + Colors = new[] { Color.FromArgb(0, 0, 0, 0), Color.FromArgb(255, 0, 0, 0) } + }; + valueBrush.InterpolationColors = valueBlend; + e.Graphics.FillRectangle(valueBrush, rect); + } + + Rectangle marker = new Rectangle(svSelectorX - 4, svSelectorY - 4, 8, 8); + using Pen outer = new Pen(Color.Black, 2f); + using Pen inner = new Pen(Color.White, 1f); + e.Graphics.DrawEllipse(outer, marker); + e.Graphics.DrawEllipse(inner, marker); + }; + hueBox.Paint += delegate(object sender, PaintEventArgs e) + { + Rectangle rect = hueBox.ClientRectangle; + if (rect.Width <= 1 || rect.Height <= 1) + return; + + rect.Width -= 1; + rect.Height -= 1; + + using (LinearGradientBrush hueBrush = new LinearGradientBrush(rect, Color.Red, Color.Red, LinearGradientMode.Vertical)) + { + hueBrush.InterpolationColors = new ColorBlend + { + Positions = new[] { 0f, 1f / 6f, 2f / 6f, 3f / 6f, 4f / 6f, 5f / 6f, 1f }, + Colors = new[] { Color.Red, Color.Yellow, Color.Lime, Color.Cyan, Color.Blue, Color.Magenta, Color.Red } + }; + e.Graphics.FillRectangle(hueBrush, rect); + } + + Rectangle marker = new Rectangle(0, hueSelectorY - 2, hueBox.ClientSize.Width - 1, 4); + using Pen outer = new Pen(Color.Black, 2f); + using Pen inner = new Pen(Color.White, 1f); + e.Graphics.DrawRectangle(outer, marker); + e.Graphics.DrawRectangle(inner, marker); + }; + hexTextBox.TextChanged += delegate + { + if (updatingHexText) + return; + + if (!TryParseHexColor(hexTextBox.Text, out Color parsedColor)) + return; + + float parsedHue = parsedColor.GetHue() / 360f; + float parsedSaturation = parsedColor.GetSaturation(); + float parsedValue = GetColorValue(parsedColor); + + currentHue = parsedHue; + currentSaturation = parsedSaturation; + currentValue = parsedValue; + SyncSelectorsFromCurrent(); + UpdateCurrentColor(); + svBox.Invalidate(); + hueBox.Invalidate(); + }; + + form.Controls.Add(svBox); + form.Controls.Add(hueBox); + form.Controls.Add(preview); + form.Controls.Add(hexLabel); + form.Controls.Add(hexTextBox); + form.Controls.Add(okButton); + form.Controls.Add(cancelButton); + form.AcceptButton = okButton; + form.CancelButton = cancelButton; + Theme.Current.Apply(form); + form.Resize += delegate + { + LayoutControls(); + SyncSelectorsFromCurrent(); + UpdateCurrentColor(); + svBox.Invalidate(); + hueBox.Invalidate(); + }; + LayoutControls(); + SyncSelectorsFromCurrent(); + UpdateCurrentColor(); + + if (form.ShowDialog() == DialogResult.OK) + { + selectedColor = current; + return true; + } + + selectedColor = initialColor; + return false; + } + + private static void SetDoubleBuffered(Control control) + { + typeof(Control).GetProperty("DoubleBuffered", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) + ?.SetValue(control, true, null); + } + + private static float GetColorValue(Color color) + { + return Math.Max(color.R, Math.Max(color.G, color.B)) / 255f; + } + + private static bool TryParseHexColor(string input, out Color color) + { + color = Color.Empty; + if (string.IsNullOrWhiteSpace(input)) + return false; + + string hex = input.Trim(); + if (hex.StartsWith("#")) + hex = hex.Substring(1); + + if (hex.Length != 6) + return false; + + if (!int.TryParse(hex.Substring(0, 2), System.Globalization.NumberStyles.HexNumber, System.Globalization.CultureInfo.InvariantCulture, out int r)) + return false; + if (!int.TryParse(hex.Substring(2, 2), System.Globalization.NumberStyles.HexNumber, System.Globalization.CultureInfo.InvariantCulture, out int g)) + return false; + if (!int.TryParse(hex.Substring(4, 2), System.Globalization.NumberStyles.HexNumber, System.Globalization.CultureInfo.InvariantCulture, out int b)) + return false; + + color = Color.FromArgb(r, g, b); + return true; } private void Resize() @@ -514,14 +1063,14 @@ public class SensorGadget : Gadget int w = Size.Width; int h = Size.Height; - if (w != _background.Width || h != _background.Height) + if (_backgroundDirty || w != _background.Width || h != _background.Height) { _background.Dispose(); _background = new Bitmap(w, h, PixelFormat.Format32bppPArgb); using (Graphics graphics = Graphics.FromImage(_background)) { - DrawImageWidthBorder(graphics, w, h, _back, TopBorder, BottomBorder,LeftBorder, RightBorder); + DrawImageWidthBorder(graphics, w, h, _backTinted ?? _back, TopBorder, BottomBorder, LeftBorder, RightBorder); if (_fore != null) DrawImageWidthBorder(graphics, w, h, _fore, TopBorder, BottomBorder, LeftBorder, RightBorder); @@ -553,6 +1102,8 @@ public class SensorGadget : Gadget graphics.DrawImage(_image, new RectangleF(LeftBorder + xOffset, TopBorder + yOffset, destWidth, destHeight)); } } + + _backgroundDirty = false; } g.DrawImageUnscaled(_background, 0, 0); @@ -560,13 +1111,15 @@ public class SensorGadget : Gadget private void DrawProgress(Graphics g, float x, float y, float width, float height, float progress) { - g.DrawImage(_barBack, + Image barBack = _barBackTinted ?? _barBack; + Image barFore = _barForeTinted ?? _barFore; + g.DrawImage(barBack, new RectangleF(x + width * progress, y, width * (1 - progress), height), - new RectangleF(_barBack.Width * progress, 0, (1 - progress) * _barBack.Width, _barBack.Height), + new RectangleF(barBack.Width * progress, 0, (1 - progress) * barBack.Width, barBack.Height), GraphicsUnit.Pixel); - g.DrawImage(_barFore, + g.DrawImage(barFore, new RectangleF(x, y, width * progress, height), - new RectangleF(0, 0, progress * _barFore.Width, _barFore.Height), GraphicsUnit.Pixel); + new RectangleF(0, 0, progress * barFore.Width, barFore.Height), GraphicsUnit.Pixel); } protected override void OnPaint(PaintEventArgs e)