diff --git a/GDIPlus-copilot-instructions.md b/GDIPlus-copilot-instructions.md new file mode 100644 index 00000000000..54294a4c285 --- /dev/null +++ b/GDIPlus-copilot-instructions.md @@ -0,0 +1,380 @@ +# WinForms GDI+ Best Practices: Performance, Quality, and Resource Management + +This document provides comprehensive guidelines for optimizing GDI+ usage in the WinForms runtime. Following these practices ensures consistent performance, proper resource management, and high-quality rendering across the codebase. + +## 1. Object Caching for Performance + +### 1.1 Cached Pen and SolidBrush Objects + +WinForms runtime provides a caching mechanism for `Pen` and `SolidBrush` objects to improve performance and reduce memory pressure. The underlying system uses: + +```csharp +internal abstract partial class RefCountedCache +``` + +These cached objects have implicit conversions that make them behave like actual GDI+ types while significantly reducing allocation overhead. + +### 1.2 Using Cached Objects + +Always prefer cached objects over direct instantiation: + +```csharp +// INCORRECT: Direct instantiation +using (var pen = new Pen(Color.Red)) +{ + g.DrawLine(pen, p1, p2); +} + +// CORRECT: Using cached object +using var pen = Color.Red.GetCachedPenScope(); +g.DrawLine(pen, p1, p2); +``` + +Available cached objects: + +- **Pens:** + ```csharp + // Default width (1) + using var pen = color.GetCachedPenScope(); + + // Custom width (integer only) + using var thickPen = color.GetCachedPenScope(2); + ``` + +- **SolidBrushes:** + ```csharp + using var brush = color.GetCachedSolidBrushScope(); + ``` + +Always use `var` for brevity and always apply `using` to ensure proper disposal. + +### 1.3 Transforming Helper Methods + +When refactoring existing code: + +- Identify methods that return `Pen` or `SolidBrush` +- For private helpers, change the return type to the corresponding cache-scope type +- For public/internal helpers, create new methods returning scope types while preserving the original methods + +```csharp +// BEFORE +private Pen GetHighlightPen() +{ + return new Pen(SystemColors.Highlight); +} + +// AFTER +private PenCache.Scope GetHighlightPenScope() + => SystemColors.Highlight.GetCachedPenScope(); +``` + +## 2. GraphicsInternal Usage + +### 2.1 When to Use GraphicsInternal + +Always prefer `GraphicsInternal` over `Graphics` for performance improvements: + +```csharp +// INCORRECT: Using Graphics directly +void Paint(PaintEventArgs e) +{ + e.Graphics.DrawRectangle(pen, rect); +} + +// CORRECT: Using GraphicsInternal +void Paint(PaintEventArgs e) +{ + e.GraphicsInternal.DrawRectangle(pen, rect); +} +``` + +From the `PaintEventArgs` class: + +```csharp +/// +/// For internal use to improve performance. DO NOT use this method if you modify the Graphics Clip or Transform. +/// +internal Graphics GraphicsInternal => _event.GetOrCreateGraphicsInternal(SaveStateIfNeeded); +``` + +### 2.2 State Management + +If you must modify the clip or transform with `GraphicsInternal`, always implement proper state management: + +```csharp +GraphicsState? previousState = null; +try +{ + // Save the current state before modifying + previousState = graphicsInternal.Save(); + + // Now safe to modify the clip or transform + graphicsInternal.TranslateTransform(x, y); + graphicsInternal.SetClip(rect); + + // Perform drawing operations + // ... +} +finally +{ + // CRITICAL: Always restore the previous state + if (previousState is not null) + { + graphicsInternal.Restore(previousState); + } +} +``` + +## 3. Graphics Quality Settings + +When changing graphics quality settings, always restore the previous setting: + +```csharp +// Store original smoothing mode +SmoothingMode originalMode = g.SmoothingMode; + +try +{ + // Set temporary mode for specific drawing + g.SmoothingMode = SmoothingMode.AntiAlias; + + // Perform drawing operations + // ... +} +finally +{ + // Restore original mode when done + g.SmoothingMode = originalMode; +} +``` + +Always preserve and restore these settings: +- TextRenderingHint +- InterpolationMode +- CompositingQuality +- PixelOffsetMode +- SmoothingMode + +## 4. Resource Management + +### 4.1 Disposable Objects + +Always properly dispose GDI+ objects that cannot be cached: + +```csharp +// Objects with custom settings cannot be cached +using (var customPen = new Pen(Color.Red) { DashStyle = DashStyle.Dash }) +{ + g.DrawLine(customPen, p1, p2); +} +``` + +### 4.2 GraphicsPath Objects + +Never cache GraphicsPath objects - always create, use, and dispose them locally: + +```csharp +// CORRECT: Local creation and disposal +using (var path = new GraphicsPath()) +{ + path.AddEllipse(rect); + g.FillPath(brush, path); +} + +// INCORRECT: Never store as a field +// private GraphicsPath _cachedPath; // BAD PRACTICE! +``` + +### 4.3 Avoiding Premature Disposal + +Ensure objects are valid throughout their entire usage lifecycle: + +```csharp +// INCORRECT: Potentially disposing before use +using (var brush = color.GetCachedSolidBrushScope()) +{ + someObject.SomeFutureOperation(brush); // brush might be disposed when used! +} + +// CORRECT: Immediate usage within scope +using (var brush = color.GetCachedSolidBrushScope()) +{ + g.FillRectangle(brush, rect); +} +``` + +## 5. Code Style and Quality Guidelines + +When working on GDI+ related code, follow these general coding guidelines: + +### 5.1 C# Language Features + +- Use C# 13 features and patterns throughout the codebase +- Apply Nullable Reference Types (NRT) consistently + ```csharp + // Update event handler signatures + public event EventHandler? SomeEvent; + private void OnSomeEvent(object? sender, EventArgs e) { ... } + ``` +- Always insert empty lines after structure blocks and before `return` statements +- Use pattern matching, `is`, `and`, `or`, and switch expressions where applicable + ```csharp + if (obj is Control control && control.Visible) + { + // Use 'control' variable + } + ``` + +### 5.2 Visibility and Scope + +- Use the narrowest possible scope for classes, methods, and fields + - Prefer `private` over `internal` over `public` + - Consider `private protected` for overridable members in internal classes +- Mark appropriate methods as `static` when they don't access instance state + +### 5.3 Naming and Formatting + +- Prefix static fields with `s_` + ```csharp + private static readonly int s_defaultBorderWidth = 1; + ``` +- Prefix instance fields with `_` + ```csharp + private int _borderWidth; + ``` +- Use PascalCase for constants, properties, and public/internal fields + ```csharp + public const int DefaultWidth = 100; + internal int BorderWidth { get; set; } + ``` +- Use explicit type names for primitive types (not `var`) + ```csharp + int count = 5; // Correct + string name = "Button"; // Correct + var index = 0; // Avoid for primitives + ``` +- Use `var` for complex types or when the type is obvious from initialization + +### 5.4 XML Documentation + +- Format XML comments with single-space indentation + ```csharp + /// + /// This is a properly formatted summary. + /// + ``` +- Use `` tags for multiple paragraphs in documentation + ```csharp + /// + /// First paragraph of documentation. + /// + /// Second paragraph with additional details. + /// + /// + ``` +- Include appropriate XML tags (``, ``, ``, etc.) + +### 5.5 Expression Bodies + +- Use expression bodies for simple properties and methods +- For longer expressions, use line breaks with proper alignment: + ```csharp + internal int SomeFooIntegerProperty => + _someFooIntegerProperty; + + private bool IsValidSize(Size size) => + size.Width > 0 && + size.Height > 0; + ``` + +## 6. Complete Examples + +### Basic Rendering with Cached Resources + +```csharp +private void PaintControl(PaintEventArgs e) +{ + // Use GraphicsInternal for better performance + var g = e.GraphicsInternal; + + // Use cached pen and brush + using var borderPen = SystemColors.ActiveBorder.GetCachedPenScope(); + using var fillBrush = SystemColors.Control.GetCachedSolidBrushScope(); + + // Draw with cached resources + g.FillRectangle(fillBrush, ClientRectangle); + g.DrawRectangle(borderPen, 0, 0, Width - 1, Height - 1); +} +``` + +### Complex Rendering with State Management + +```csharp +private void DrawComplexControl(PaintEventArgs e) +{ + GraphicsState? previousState = null; + SmoothingMode originalMode = SmoothingMode.Default; + + try + { + var g = e.GraphicsInternal; + + // Save state before modifications + previousState = g.Save(); + originalMode = g.SmoothingMode; + + // Apply quality settings + g.SmoothingMode = SmoothingMode.AntiAlias; + + // Apply transform + g.TranslateTransform(10, 10); + + // Use cached resources + using var pen = Color.Blue.GetCachedPenScope(2); + using var brush = Color.LightBlue.GetCachedSolidBrushScope(); + + // Create a path (never cache paths) + using (var path = new GraphicsPath()) + { + path.AddEllipse(0, 0, 100, 50); + g.FillPath(brush, path); + g.DrawPath(pen, path); + } + } + finally + { + // Restore state + if (previousState is not null) + { + e.GraphicsInternal.Restore(previousState); + } + + // Restore quality settings + e.GraphicsInternal.SmoothingMode = originalMode; + } +} +``` + +### Helper Method Transformation + +```csharp +// BEFORE +private Pen CreateBorderPen() +{ + return new Pen(SystemColors.ActiveBorder); +} + +// AFTER +private PenCache.Scope GetBorderPenScope() + => SystemColors.ActiveBorder.GetCachedPenScope(); + +// For public/internal APIs, create a new method and keep the original +internal Pen CreateHighlightPen() +{ + return new Pen(SystemColors.Highlight); +} + +// Add a new method returning the cached version +internal PenCache.Scope GetHighlightPenScope() + => SystemColors.Highlight.GetCachedPenScope(); +``` diff --git a/Winforms.sln b/Winforms.sln index c1f2af710c0..570aa795271 100644 --- a/Winforms.sln +++ b/Winforms.sln @@ -104,8 +104,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "interop", "interop", "{A31B EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DesignSurface", "DesignSurface", "{43E46506-7DF8-4E7A-A579-996CA43041EB}" ProjectSection(SolutionItems) = preProject - src\test\integration\DesignSurface\README.md = src\test\integration\DesignSurface\README.md src\test\integration\DesignSurface\Directory.Build.props = src\test\integration\DesignSurface\Directory.Build.props + src\test\integration\DesignSurface\README.md = src\test\integration\DesignSurface\README.md EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DemoConsole", "src\test\integration\DesignSurface\DemoConsole\DemoConsole.csproj", "{93310A19-DDCA-4BCD-AEDE-5C5D788DAFB4}" @@ -209,6 +209,16 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Analyzers", "Analyzers", "{ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Private.Windows.GdiPlus", "src\System.Private.Windows.GdiPlus\System.Private.Windows.GdiPlus.csproj", "{442C867C-51C0-8CE5-F067-DF065008E3DA}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Copilot Support", "Copilot Support", "{A647B5E4-AB95-B22D-2364-699264C22E13}" + ProjectSection(SolutionItems) = preProject + .github\copilot-instructions.md = .github\copilot-instructions.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Rendering", "Rendering", "{83A024D8-6E4A-1445-1D5C-70D931A1B072}" + ProjectSection(SolutionItems) = preProject + GDIPlus-copilot-instructions.md = GDIPlus-copilot-instructions.md + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1195,6 +1205,7 @@ Global {656C66A4-59CD-4E14-8AE4-1F5BCEECB553} = {8B4B1E09-B3C7-4044-B223-94EDEC1CAA20} {D4D97D78-D213-45DF-B003-9C4C9F2E5E1C} = {8B4B1E09-B3C7-4044-B223-94EDEC1CAA20} {442C867C-51C0-8CE5-F067-DF065008E3DA} = {77FEDB47-F7F6-490D-AF7C-ABB4A9E0B9D7} + {83A024D8-6E4A-1445-1D5C-70D931A1B072} = {A647B5E4-AB95-B22D-2364-699264C22E13} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7B1B0433-F612-4E5A-BE7E-FCF5B9F6E136} diff --git a/src/System.Windows.Forms/PublicAPI.Shipped.txt b/src/System.Windows.Forms/PublicAPI.Shipped.txt index e7b1bf0086f..541a98d9146 100644 --- a/src/System.Windows.Forms/PublicAPI.Shipped.txt +++ b/src/System.Windows.Forms/PublicAPI.Shipped.txt @@ -1,23 +1,10 @@ #nullable enable [WFO5001]static System.Windows.Forms.Application.SetColorMode(System.Windows.Forms.SystemColorMode systemColorMode) -> void [WFO5001]System.Windows.Forms.ControlStyles.ApplyThemingImplicitly = 524288 -> System.Windows.Forms.ControlStyles -[WFO5001]System.Windows.Forms.Form.FormBorderColorChanged -> System.EventHandler? -[WFO5001]System.Windows.Forms.Form.FormCaptionBackColorChanged -> System.EventHandler? -[WFO5001]System.Windows.Forms.Form.FormCaptionTextColorChanged -> System.EventHandler? -[WFO5001]System.Windows.Forms.Form.FormCornerPreferenceChanged -> System.EventHandler? -[WFO5001]System.Windows.Forms.FormCornerPreference -[WFO5001]System.Windows.Forms.FormCornerPreference.Default = 0 -> System.Windows.Forms.FormCornerPreference -[WFO5001]System.Windows.Forms.FormCornerPreference.DoNotRound = 1 -> System.Windows.Forms.FormCornerPreference -[WFO5001]System.Windows.Forms.FormCornerPreference.Round = 2 -> System.Windows.Forms.FormCornerPreference -[WFO5001]System.Windows.Forms.FormCornerPreference.RoundSmall = 3 -> System.Windows.Forms.FormCornerPreference [WFO5001]System.Windows.Forms.SystemColorMode [WFO5001]System.Windows.Forms.SystemColorMode.Classic = 0 -> System.Windows.Forms.SystemColorMode [WFO5001]System.Windows.Forms.SystemColorMode.Dark = 2 -> System.Windows.Forms.SystemColorMode [WFO5001]System.Windows.Forms.SystemColorMode.System = 1 -> System.Windows.Forms.SystemColorMode -[WFO5001]virtual System.Windows.Forms.Form.OnFormBorderColorChanged(System.EventArgs! e) -> void -[WFO5001]virtual System.Windows.Forms.Form.OnFormCaptionBackColorChanged(System.EventArgs! e) -> void -[WFO5001]virtual System.Windows.Forms.Form.OnFormCaptionTextColorChanged(System.EventArgs! e) -> void -[WFO5001]virtual System.Windows.Forms.Form.OnFormCornerPreferenceChanged(System.EventArgs! e) -> void [WFO5002]static System.Windows.Forms.TaskDialog.ShowDialogAsync(nint hwndOwner, System.Windows.Forms.TaskDialogPage! page, System.Windows.Forms.TaskDialogStartupLocation startupLocation = System.Windows.Forms.TaskDialogStartupLocation.CenterOwner) -> System.Threading.Tasks.Task! [WFO5002]static System.Windows.Forms.TaskDialog.ShowDialogAsync(System.Windows.Forms.IWin32Window! owner, System.Windows.Forms.TaskDialogPage! page, System.Windows.Forms.TaskDialogStartupLocation startupLocation = System.Windows.Forms.TaskDialogStartupLocation.CenterOwner) -> System.Threading.Tasks.Task! [WFO5002]static System.Windows.Forms.TaskDialog.ShowDialogAsync(System.Windows.Forms.TaskDialogPage! page, System.Windows.Forms.TaskDialogStartupLocation startupLocation = System.Windows.Forms.TaskDialogStartupLocation.CenterScreen) -> System.Threading.Tasks.Task! diff --git a/src/System.Windows.Forms/PublicAPI.Unshipped.txt b/src/System.Windows.Forms/PublicAPI.Unshipped.txt index e69de29bb2d..dca55358f31 100644 --- a/src/System.Windows.Forms/PublicAPI.Unshipped.txt +++ b/src/System.Windows.Forms/PublicAPI.Unshipped.txt @@ -0,0 +1,13 @@ +System.Windows.Forms.FormCornerPreference +System.Windows.Forms.FormCornerPreference.Default = 0 -> System.Windows.Forms.FormCornerPreference +System.Windows.Forms.FormCornerPreference.DoNotRound = 1 -> System.Windows.Forms.FormCornerPreference +System.Windows.Forms.FormCornerPreference.Round = 2 -> System.Windows.Forms.FormCornerPreference +System.Windows.Forms.FormCornerPreference.RoundSmall = 3 -> System.Windows.Forms.FormCornerPreference +virtual System.Windows.Forms.Form.OnFormBorderColorChanged(System.EventArgs! e) -> void +virtual System.Windows.Forms.Form.OnFormCaptionBackColorChanged(System.EventArgs! e) -> void +virtual System.Windows.Forms.Form.OnFormCaptionTextColorChanged(System.EventArgs! e) -> void +virtual System.Windows.Forms.Form.OnFormCornerPreferenceChanged(System.EventArgs! e) -> void +[WFO5001]System.Windows.Forms.Form.FormBorderColorChanged -> System.EventHandler? +[WFO5001]System.Windows.Forms.Form.FormCaptionBackColorChanged -> System.EventHandler? +[WFO5001]System.Windows.Forms.Form.FormCaptionTextColorChanged -> System.EventHandler? +[WFO5001]System.Windows.Forms.Form.FormCornerPreferenceChanged -> System.EventHandler? \ No newline at end of file diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/StatusStrip.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/StatusStrip.cs index db4ae69246c..e9fd8229e03 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/StatusStrip.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/StatusStrip.cs @@ -118,7 +118,7 @@ public override DockStyle Dock set => base.LayoutStyle = value; } - // we do some custom stuff with padding to accomodate size grip. + // we do some custom stuff with padding to accommodate size grip. // changing this is not supported at DT [Browsable(false)] public new Padding Padding diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStrip.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStrip.cs index b2c1df93b04..cc5f21256ce 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStrip.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStrip.cs @@ -1549,7 +1549,7 @@ public ToolStripRenderMode RenderMode return ToolStripRenderMode.ManagerRenderMode; } - if (_renderer is not null && !_renderer.IsAutoGenerated) + if (_renderer is not null && !_renderer.IsSystemDefaultAlternative) { return ToolStripRenderMode.Custom; } @@ -3250,7 +3250,7 @@ protected override void OnLayout(LayoutEventArgs e) { LayoutRequired = false; - // we need to do this to prevent autosizing to happen while we're reparenting. + // we need to do this to prevent auto-sizing to happen while we're reparenting. ToolStripOverflow? overflow = GetOverflow(); if (overflow is not null) { @@ -3636,6 +3636,7 @@ protected override void OnPaintBackground(PaintEventArgs e) Graphics g = e.GraphicsInternal; GraphicsState graphicsState = g.Save(); + try { using (Region? transparentRegion = Renderer.GetTransparentRegion(this)) diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripManager.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripManager.cs index 1ebaa91aefb..9fbcf50dbb8 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripManager.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripManager.cs @@ -489,7 +489,7 @@ public static ToolStripManagerRenderMode RenderMode { Type currentType = CurrentRendererType; - if (t_defaultRenderer is not null && !t_defaultRenderer.IsAutoGenerated) + if (t_defaultRenderer is not null && !t_defaultRenderer.IsSystemDefaultAlternative) { return ToolStripManagerRenderMode.Custom; } @@ -544,37 +544,19 @@ public static bool VisualStylesEnabled } } - internal static ToolStripRenderer CreateRenderer(ToolStripManagerRenderMode renderMode) + internal static ToolStripRenderer CreateRenderer(ToolStripManagerRenderMode renderMode) => renderMode switch { - switch (renderMode) - { - case ToolStripManagerRenderMode.System: - return new ToolStripSystemRenderer(isDefault: true); - case ToolStripManagerRenderMode.Professional: -#pragma warning disable WFO5001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - if (Application.IsDarkModeEnabled) - { - return new ToolStripProfessionalRenderer(new DarkProfessionalColors()); - } -#pragma warning restore WFO5001 + ToolStripManagerRenderMode.System => new ToolStripSystemRenderer(isDefault: true), + ToolStripManagerRenderMode.Professional => new ToolStripProfessionalRenderer(isDefault: true), + _ => new ToolStripSystemRenderer(isDefault: true), + }; - return new ToolStripProfessionalRenderer(isDefault: true); - - case ToolStripManagerRenderMode.Custom: - default: - return new ToolStripSystemRenderer(isDefault: true); - } - } - - internal static ToolStripRenderer CreateRenderer(ToolStripRenderMode renderMode) + internal static ToolStripRenderer CreateRenderer(ToolStripRenderMode renderMode) => renderMode switch { - return renderMode switch - { - ToolStripRenderMode.System => new ToolStripSystemRenderer(isDefault: true), - ToolStripRenderMode.Professional => new ToolStripProfessionalRenderer(isDefault: true), - _ => new ToolStripSystemRenderer(isDefault: true), - }; - } + ToolStripRenderMode.System => new ToolStripSystemRenderer(isDefault: true), + ToolStripRenderMode.Professional => new ToolStripProfessionalRenderer(isDefault: true), + _ => new ToolStripSystemRenderer(isDefault: true), + }; internal static WeakRefCollection ToolStripPanels => t_activeToolStripPanels ??= []; diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripRenderer.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripRenderer.cs index bf91c4d9d16..9603d2bad1d 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripRenderer.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripRenderer.cs @@ -3,6 +3,7 @@ using System.ComponentModel; using System.Drawing; +using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.Drawing.Text; using System.Windows.Forms.Layout; @@ -36,7 +37,7 @@ public abstract class ToolStripRenderer private static ColorMatrix? s_disabledImageColorMatrix; private EventHandlerList? _events; - private readonly bool _isAutoGenerated; + private readonly bool _isSystemDefaultAlternative; private static bool s_isScalingInitialized; internal int _previousDeviceDpi = ScaleHelper.InitialSystemDpi; @@ -51,35 +52,53 @@ public abstract class ToolStripRenderer private static int s_offset4X = OFFSET_4PIXELS; private static int s_offset4Y = OFFSET_4PIXELS; - // Used in building up the half pyramid of rectangles that are drawn in a - // status strip sizing grip. - private static readonly Rectangle[] s_baseSizeGripRectangles = [ - new(8, 0, 2, 2), - new(8, 4, 2, 2), - new(8, 8, 2, 2), - new(4, 4, 2, 2), - new(4, 8, 2, 2), - new(0, 8, 2, 2) - ]; - protected ToolStripRenderer() { } - internal ToolStripRenderer(bool isAutoGenerated) - { - _isAutoGenerated = isAutoGenerated; - } + internal ToolStripRenderer(bool isAutoGenerated) => + _isSystemDefaultAlternative = isAutoGenerated; // Used in building disabled images. private static ColorMatrix DisabledImageColorMatrix { get { - if (s_disabledImageColorMatrix is null) + if (s_disabledImageColorMatrix is not null) { - // This is the result of a GreyScale matrix multiplied by a transparency matrix of .5 + return s_disabledImageColorMatrix; + } +#pragma warning disable WFO5001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + bool isDarkMode = Application.IsDarkModeEnabled; +#pragma warning restore WFO5001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + + if (isDarkMode) + { + // Dark mode color matrix + float[][] greyscale = + [ + [0.2125f, 0.2125f, 0.2125f, 0, 0], + [0.2577f, 0.2577f, 0.2577f, 0, 0], + [0.0361f, 0.0361f, 0.0361f, 0, 0], + [0, 0, 0, 1, 0], + [-0.1f, -0.1f, -0.1f, 0, 1], + ]; + + float[][] transparency = + [ + [1, 0, 0, 0, 0], + [0, 1, 0, 0, 0], + [0, 0, 1, 0, 0], + [0, 0, 0, 0.8f, 0], + [0, 0, 0, 0, 0], + ]; + + s_disabledImageColorMatrix = ControlPaint.MultiplyColorMatrix(transparency, greyscale); + } + else + { + // Light mode color matrix float[][] greyscale = [ [0.2125f, 0.2125f, 0.2125f, 0, 0], @@ -94,7 +113,7 @@ private static ColorMatrix DisabledImageColorMatrix [1, 0, 0, 0, 0], [0, 1, 0, 0, 0], [0, 0, 1, 0, 0], - [0, 0, 0, .7F, 0], + [0, 0, 0, 0.7f, 0], [0, 0, 0, 0, 0], ]; @@ -118,19 +137,16 @@ private EventHandlerList Events } } - internal bool IsAutoGenerated - { - get { return _isAutoGenerated; } - } + /// + /// Defines, if there is a variation of the system default renderer, that is chosen by the system + /// to address the current environment context. Like DarkMode, HighContrast, LowRes etc. + /// (Used to be 'AutoGenerated', presuming to indicate, that the renderer was 'generated' (picked) by the system.) + /// + internal bool IsSystemDefaultAlternative + => _isSystemDefaultAlternative; // if we're in a low contrast, high resolution situation, use this renderer under the covers instead. - internal virtual ToolStripRenderer? RendererOverride - { - get - { - return null; - } - } + internal virtual ToolStripRenderer? RendererOverride => null; public event ToolStripArrowRenderEventHandler RenderArrow { @@ -299,21 +315,13 @@ public event ToolStripSeparatorRenderEventHandler RenderSeparator #region EventHandlerSecurity - private void AddHandler(object key, Delegate value) - { - Events.AddHandler(key, value); - } + private void AddHandler(object key, Delegate value) => Events.AddHandler(key, value); - private void RemoveHandler(object key, Delegate value) - { - Events.RemoveHandler(key, value); - } + private void RemoveHandler(object key, Delegate value) => Events.RemoveHandler(key, value); #endregion - public static Image CreateDisabledImage(Image normalImage) - { - return CreateDisabledImage(normalImage, null); - } + public static Image CreateDisabledImage(Image normalImage) => + CreateDisabledImage(normalImage, null); public void DrawArrow(ToolStripArrowRenderEventArgs e) { @@ -515,7 +523,6 @@ public void DrawToolStripStatusLabelBackground(ToolStripItemRenderEventArgs e) } } - // public void DrawStatusStripSizingGrip(ToolStripRenderEventArgs e) { OnRenderStatusStripSizingGrip(e); @@ -557,10 +564,7 @@ public void DrawToolStripContentPanelBackground(ToolStripContentPanelRenderEvent } // consider make public - internal virtual Region? GetTransparentRegion(ToolStrip toolStrip) - { - return null; - } + internal virtual Region? GetTransparentRegion(ToolStrip toolStrip) => null; protected internal virtual void Initialize(ToolStrip toolStrip) { @@ -601,6 +605,10 @@ protected static void ScaleArrowOffsetsIfNeeded(int dpi) s_offset4Y = ScaleHelper.ScaleToDpi(OFFSET_4PIXELS, dpi); } + /// + /// Renders an arrow on the ToolStrip control. + /// + /// A ToolStripArrowRenderEventArgs that contains the event data. protected virtual void OnRenderArrow(ToolStripArrowRenderEventArgs e) { ArgumentNullException.ThrowIfNull(e); @@ -611,17 +619,33 @@ protected virtual void OnRenderArrow(ToolStripArrowRenderEventArgs e) return; } + RenderArrowCore(e, e.ArrowColor); + } + + /// + /// Base class method that handles shared arrow rendering functionality. + /// + /// The event arguments containing rendering information. + /// The color to use for the arrow. + /// The rendered arrow points. + private protected Point[] RenderArrowCore( + ToolStripArrowRenderEventArgs e, + Color arrowColor) + { + ArgumentNullException.ThrowIfNull(e); + Graphics g = e.Graphics; + Rectangle dropDownRect = e.ArrowRectangle; - using var brush = e.ArrowColor.GetCachedSolidBrushScope(); - Point middle = new(dropDownRect.Left + dropDownRect.Width / 2, dropDownRect.Top + dropDownRect.Height / 2); - // if the width is odd - favor pushing it over one pixel right. - // middle.X += (dropDownRect.Width % 2); - Point[]? arrow = null; + Point middle = new( + dropDownRect.Left + dropDownRect.Width / 2, + dropDownRect.Top + dropDownRect.Height / 2); - // We need to check for null here, since at design time at this point Item can be null. - if (e.Item is not null && e.Item.DeviceDpi != _previousDeviceDpi && ScaleHelper.IsThreadPerMonitorV2Aware) + // Scale arrow offsets if needed + if (e.Item is not null + && e.Item.DeviceDpi != _previousDeviceDpi + && ScaleHelper.IsThreadPerMonitorV2Aware) { _previousDeviceDpi = e.Item.DeviceDpi; ScaleArrowOffsetsIfNeeded(e.Item.DeviceDpi); @@ -631,10 +655,13 @@ protected virtual void OnRenderArrow(ToolStripArrowRenderEventArgs e) ScaleArrowOffsetsIfNeeded(); } - // using (offset4X - Offset2X) instead of (Offset2X) to compensate for rounding error in scaling - int horizontalOffset = ScaleHelper.IsScalingRequirementMet ? s_offset4X - Offset2X : Offset2X; + // Using (offset4X - Offset2X) instead of (Offset2X) to compensate + // for rounding error in scaling + int horizontalOffset = ScaleHelper.IsScalingRequirementMet + ? s_offset4X - Offset2X + : Offset2X; - arrow = e.Direction switch + Point[] arrow = e.Direction switch { ArrowDirection.Up => [ @@ -661,7 +688,11 @@ protected virtual void OnRenderArrow(ToolStripArrowRenderEventArgs e) new(middle.X, middle.Y + Offset2Y) ], }; + + using var brush = arrowColor.GetCachedSolidBrushScope(); g.FillPolygon(brush, arrow); + + return arrow; } /// @@ -780,6 +811,7 @@ protected virtual void OnRenderItemImage(ToolStripItemImageRenderEventArgs e) if (imageRect != Rectangle.Empty && image is not null) { bool disposeImage = false; + if (e.ShiftOnPress && e.Item is not null && e.Item.Pressed) { imageRect.X++; @@ -820,30 +852,32 @@ protected virtual void OnRenderItemCheck(ToolStripItemImageRenderEventArgs e) Rectangle imageRect = e.ImageRectangle; Image? image = e.Image; - if (imageRect != Rectangle.Empty && image is not null) + if (imageRect == Rectangle.Empty || image is null) + { + return; + } + + if (e.Item is not null) { - if (e.Item is not null) + if (!e.Item.Enabled) { - if (!e.Item.Enabled) - { - image = CreateDisabledImage(image, e.ImageAttributes); - } + image = CreateDisabledImage(image, e.ImageAttributes); + } - if (SystemInformation.HighContrast && image is Bitmap bitmap) - { - Color backgroundColor = e.Item.Selected ? SystemColors.Highlight : e.Item.BackColor; + if (SystemInformation.HighContrast && image is Bitmap bitmap) + { + Color backgroundColor = e.Item.Selected ? SystemColors.Highlight : e.Item.BackColor; - if (ControlPaint.IsDark(backgroundColor)) - { - Image invertedImage = ControlPaint.CreateBitmapWithInvertedForeColor(bitmap, e.Item.BackColor); - image = invertedImage; - } + if (ControlPaint.IsDark(backgroundColor)) + { + Image invertedImage = ControlPaint.CreateBitmapWithInvertedForeColor(bitmap, e.Item.BackColor); + image = invertedImage; } } - - e.Graphics.DrawImage(image, imageRect, 0, 0, imageRect.Width, - imageRect.Height, GraphicsUnit.Pixel, e.ImageAttributes); } + + e.Graphics.DrawImage(image, imageRect, 0, 0, imageRect.Width, + imageRect.Height, GraphicsUnit.Pixel, e.ImageAttributes); } /// @@ -867,25 +901,41 @@ protected virtual void OnRenderItemText(ToolStripItemTextRenderEventArgs e) string? text = e.Text; Rectangle textRect = e.TextRectangle; TextFormatFlags textFormat = e.TextFormat; - // if we're disabled draw in a different color. - textColor = (item is not null && item.Enabled) ? textColor : SystemColors.GrayText; - if (e.TextDirection != ToolStripTextDirection.Horizontal && textRect.Width > 0 && textRect.Height > 0) - { - // Perf: this is a bit heavy handed.. perhaps we can share the bitmap. - Size textSize = LayoutUtils.FlipSize(textRect.Size); - using Bitmap textBmp = new(textSize.Width, textSize.Height, PixelFormat.Format32bppPArgb); - using Graphics textGraphics = Graphics.FromImage(textBmp); - // now draw the text.. - textGraphics.TextRenderingHint = TextRenderingHint.AntiAlias; - TextRenderer.DrawText(textGraphics, text, textFont, new Rectangle(Point.Empty, textSize), textColor, textFormat); - textBmp.RotateFlip((e.TextDirection == ToolStripTextDirection.Vertical90) ? RotateFlipType.Rotate90FlipNone : RotateFlipType.Rotate270FlipNone); - g.DrawImage(textBmp, textRect); - } - else + // If we're disabled draw in a different color. + textColor = (item is not null && item.Enabled) + ? textColor + : SystemColors.GrayText; + + if (e.TextDirection == ToolStripTextDirection.Horizontal + || textRect.Width <= 0 + || textRect.Height <= 0) { TextRenderer.DrawText(g, text, textFont, textRect, textColor, textFormat); + return; } + + // Perf: this is a bit heavy handed.. perhaps we can share the bitmap. + Size textSize = LayoutUtils.FlipSize(textRect.Size); + using Bitmap textBmp = new(textSize.Width, textSize.Height, PixelFormat.Format32bppPArgb); + using Graphics textGraphics = Graphics.FromImage(textBmp); + + // Now draw the text. + textGraphics.TextRenderingHint = TextRenderingHint.AntiAlias; + + TextRenderer.DrawText( + dc: textGraphics, + text: text, + font: textFont, + bounds: new Rectangle(Point.Empty, textSize), + foreColor: textColor, + flags: textFormat); + + textBmp.RotateFlip((e.TextDirection == ToolStripTextDirection.Vertical90) + ? RotateFlipType.Rotate90FlipNone + : RotateFlipType.Rotate270FlipNone); + + g.DrawImage(textBmp, textRect); } /// @@ -952,6 +1002,22 @@ protected virtual void OnRenderToolStripStatusLabelBackground(ToolStripItemRende } } + // Used in building up the half pyramid of rectangles that are drawn in a + // status strip sizing grip. + private static readonly Rectangle[] s_baseSizeGripRectangles = + [ + new(12, 0, 2, 2), + new(8, 4, 2, 2), + new(4, 8, 2, 2), + new(0, 12, 2, 2), + new(8, 0, 2, 2), + new(4, 4, 2, 2), + new(0, 8, 2, 2), + new(4, 0, 2, 2), + new(0, 4, 2, 2), + new(1, 1, 2, 2), + ]; + protected virtual void OnRenderStatusStripSizingGrip(ToolStripRenderEventArgs e) { ArgumentNullException.ThrowIfNull(e); @@ -962,45 +1028,117 @@ protected virtual void OnRenderStatusStripSizingGrip(ToolStripRenderEventArgs e) return; } - Graphics g = e.Graphics; + OnRenderStatusStripSizingGrip( + eArgs: e, + highLightBrush: SystemBrushes.ButtonHighlight, + shadowBrush: SystemBrushes.GrayText); + } - // we have a set of stock rectangles. Translate them over to where the grip is to be drawn - // for the white set, then translate them up and right one pixel for the grey. + private protected static void OnRenderStatusStripSizingGrip( + ToolStripRenderEventArgs eArgs, + Brush highLightBrush, + Brush shadowBrush) + { + if (eArgs.ToolStrip is not StatusStrip statusStrip) + { + return; + } - if (e.ToolStrip is StatusStrip statusStrip) + Rectangle sizeGripBounds = statusStrip.SizeGripBounds; + + if (LayoutUtils.IsZeroWidthOrHeight(sizeGripBounds)) { - Rectangle sizeGripBounds = statusStrip.SizeGripBounds; + return; + } - if (!LayoutUtils.IsZeroWidthOrHeight(sizeGripBounds)) - { - Rectangle[] whiteRectangles = new Rectangle[s_baseSizeGripRectangles.Length]; - Rectangle[] greyRectangles = new Rectangle[s_baseSizeGripRectangles.Length]; + Graphics g = eArgs.Graphics; + ReadOnlySpan baseRects = s_baseSizeGripRectangles; - for (int i = 0; i < s_baseSizeGripRectangles.Length; i++) - { - Rectangle baseRect = s_baseSizeGripRectangles[i]; - if (statusStrip.RightToLeft == RightToLeft.Yes) - { - baseRect.X = sizeGripBounds.Width - baseRect.X - baseRect.Width; - } - - baseRect.Offset(sizeGripBounds.X, sizeGripBounds.Bottom - 12 /*height of pyramid (10px) + 2px padding from bottom*/); - whiteRectangles[i] = baseRect; - if (statusStrip.RightToLeft == RightToLeft.Yes) - { - baseRect.Offset(1, -1); - } - else - { - baseRect.Offset(-1, -1); - } - - greyRectangles[i] = baseRect; - } + // Use device DPI for scaling + float dpiScale = 1.0f; + + if (statusStrip.DeviceDpi > 0 && ScaleHelper.IsThreadPerMonitorV2Aware) + { + dpiScale = statusStrip.DeviceDpi / 96f; + } + + // Create a buffer on the stack for the scaled rectangles + Span scaledRects = stackalloc Rectangle[baseRects.Length]; + + // Scale the base rectangles for the grip dots + for (int i = 0; i < baseRects.Length; i++) + { + Rectangle r = baseRects[i]; + + scaledRects[i] = new Rectangle( + (int)(r.X * dpiScale), + (int)(r.Y * dpiScale), + Math.Max((int)(r.Width * dpiScale), 2), + Math.Max((int)(r.Height * dpiScale), 2)); + } - g.FillRectangles(SystemBrushes.ButtonHighlight, whiteRectangles); - g.FillRectangles(SystemBrushes.ButtonShadow, greyRectangles); + (int cornerOffset, Rectangle lastRect) = GetCornerOffset(statusStrip); + scaledRects[^1] = lastRect; + + SmoothingMode oldSmoothing = g.SmoothingMode; + g.SmoothingMode = SmoothingMode.AntiAlias; + + // Optimize for RTL check by determining the calculation function once + bool isRtl = statusStrip.RightToLeft == RightToLeft.Yes; + + // Draw the grip dots, bottom-right aligned (mirrored for RTL) + Span workingRects = stackalloc Rectangle[3]; // actualRect, highlightRect, shadowRect + + for (int i = 0; i < scaledRects.Length; i++) + { + ref Rectangle dotRect = ref scaledRects[i]; + ref Rectangle actualRect = ref workingRects[0]; + ref Rectangle highlightRect = ref workingRects[1]; + ref Rectangle shadowRect = ref workingRects[2]; + + actualRect = isRtl + ? new Rectangle( + x: sizeGripBounds.Left + cornerOffset + dotRect.X, + y: sizeGripBounds.Bottom - cornerOffset - dotRect.Y - dotRect.Height, + width: dotRect.Width, + height: dotRect.Height) + : new Rectangle( + x: sizeGripBounds.Right - cornerOffset - dotRect.X - dotRect.Width, + y: sizeGripBounds.Bottom - cornerOffset - dotRect.Y - dotRect.Height, + width: dotRect.Width, + height: dotRect.Height); + + // Highlight dot (top-left) + highlightRect = actualRect; + highlightRect.Offset(-1, -1); + g.FillEllipse(highLightBrush, highlightRect); + + // Shadow dot (bottom-right) + shadowRect = actualRect; + shadowRect.Offset(1, 1); + g.FillEllipse(shadowBrush, shadowRect); + } + + g.SmoothingMode = oldSmoothing; + + // We need to account for Windows 11+ AND whatever styled corners we have. + static (int cornerOffset, Rectangle rect) GetCornerOffset(StatusStrip statusStrip) + { + // Default values + (int offset, Rectangle rect) cornerDef = (2, new(1, 1, 2, 2)); + + if (Environment.OSVersion.Version >= new Version(10, 0, 22000) + && statusStrip.FindForm() is Form f) + { + cornerDef = f.FormCornerPreference switch + { + FormCornerPreference.Round => (4, new(1, 1, 2, 2)), + FormCornerPreference.RoundSmall => (3, new(1, 1, 2, 2)), + _ => (2, new(0, 0, 2, 2)) + }; } + + return cornerDef; } } @@ -1016,11 +1154,10 @@ protected virtual void OnRenderSplitButtonBackground(ToolStripItemRenderEventArg } } - // Only paint background effects if no BackColor has been set or no background image has been set. - internal static bool ShouldPaintBackground(Control control) - { - return (control.RawBackColor == Color.Empty && control.BackgroundImage is null); - } + // Only paint background effects if no BackColor has been set + // or no background image has been set. + internal static bool ShouldPaintBackground(Control control) => + control.RawBackColor == Color.Empty && control.BackgroundImage is null; private static Bitmap CreateDisabledImage(Image normalImage, ImageAttributes? imgAttrib) { @@ -1033,13 +1170,18 @@ private static Bitmap CreateDisabledImage(Image normalImage, ImageAttributes? im Size size = normalImage.Size; Bitmap disabledBitmap = new(size.Width, size.Height); + using (Graphics graphics = Graphics.FromImage(disabledBitmap)) { - graphics.DrawImage(normalImage, - new Rectangle(0, 0, size.Width, size.Height), - 0, 0, size.Width, size.Height, - GraphicsUnit.Pixel, - imgAttrib); + graphics.DrawImage( + image: normalImage, + destRect: new Rectangle(0, 0, size.Width, size.Height), + srcX: 0, + srcY: 0, + srcWidth: size.Width, + srcHeight: size.Height, + srcUnit: GraphicsUnit.Pixel, + imageAttr: imgAttrib); } return disabledBitmap; diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripRendererSwitcher.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripRendererSwitcher.cs index 1788be8d09b..3b04339744e 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripRendererSwitcher.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripRendererSwitcher.cs @@ -80,7 +80,7 @@ public ToolStripRenderMode RenderMode return ToolStripRenderMode.ManagerRenderMode; } - if (_renderer is not null && !_renderer.IsAutoGenerated) + if (_renderer is not null && !_renderer.IsSystemDefaultAlternative) { return ToolStripRenderMode.Custom; } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemDarkModeRenderer.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemDarkModeRenderer.cs new file mode 100644 index 00000000000..08e39e50440 --- /dev/null +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemDarkModeRenderer.cs @@ -0,0 +1,692 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Drawing; + +namespace System.Windows.Forms; + +/// +/// Provides dark mode rendering capabilities for ToolStrip controls in Windows Forms. +/// +/// +/// +/// This renderer is designed to be used with the ToolStripSystemRenderer to provide dark mode +/// styling while maintaining accessibility features. It inherits from ToolStripRenderer +/// and overrides necessary methods to provide dark-themed rendering. +/// +/// +internal class ToolStripSystemDarkModeRenderer : ToolStripRenderer +{ + /// + /// Initializes a new instance of the ToolStripSystemDarkModeRenderer class. + /// + public ToolStripSystemDarkModeRenderer() + { + } + + /// + /// Initializes a new instance of the ToolStripSystemDarkModeRenderer class with the specified default state. + /// + /// + /// True if this should be seen as a variation of the default renderer + /// (so, no _custom_ renderer provided by the user); otherwise, false. + /// + internal ToolStripSystemDarkModeRenderer(bool isSystemDefaultAlternative) + : base(isSystemDefaultAlternative) + { + } + + /// + /// Gets dark-appropriate system colors based on the control type. + /// + /// The color to convert to a dark mode equivalent. + /// A color suitable for dark mode. + private static Color GetDarkModeColor(Color color) + { + // Map system colors to some slightly different colors we would get + // form the actual system colors in dark mode, since the visual style + // renderer in light mode would also not "hit" (for contrast and styling + // reasons) the exact same palette settings as the system colors. + if (color == SystemColors.Control) + return Color.FromArgb(45, 45, 45); + if (color == SystemColors.ControlLight) + return Color.FromArgb(60, 60, 60); + if (color == SystemColors.ControlDark) + return Color.FromArgb(30, 30, 30); + if (color == SystemColors.ControlText) + return Color.FromArgb(240, 240, 240); + if (color == SystemColors.ButtonFace) + return Color.FromArgb(45, 45, 45); + if (color == SystemColors.Highlight) + return Color.FromArgb(0, 120, 215); + if (color == SystemColors.HighlightText) + return Color.White; + if (color == SystemColors.Window) + return Color.FromArgb(32, 32, 32); + if (color == SystemColors.WindowText) + return Color.FromArgb(240, 240, 240); + if (color == SystemColors.GrayText) + return Color.FromArgb(153, 153, 153); + if (color == SystemColors.InactiveBorder) + return Color.FromArgb(70, 70, 70); + if (color == SystemColors.ButtonHighlight) + return Color.FromArgb(80, 80, 80); + if (color == SystemColors.ButtonShadow) + return Color.FromArgb(20, 20, 20); + if (color == SystemColors.Menu) + return Color.FromArgb(45, 45, 45); + if (color == SystemColors.MenuText) + return Color.FromArgb(240, 240, 240); + + // For any other colors, darken them if they're bright + if (color.GetBrightness() > 0.5) + { + // Create a darker version for light colors + return ControlPaint.Dark(color, 0.2f); + } + + return color; + } + + /// + /// Creates a dark mode compatible brush. Important: + /// Always do: `using var brush = GetDarkModeBrush(color)`, + /// since you're dealing with a cached brush => scope, really! + /// + /// The system color to convert. + /// A brush with the dark mode color. + private static SolidBrushCache.Scope GetDarkModeBrush(Color color) + => GetDarkModeColor(color).GetCachedSolidBrushScope(); + + /// + /// Creates a dark mode compatible pen. Important: + /// Always do: `using var somePen = GetDarkModePen(color)`, + /// since you're dealing with a cached pen => scope, really! + /// + /// The system color to convert. + /// A pen with the dark mode color. + private static PenCache.Scope GetDarkModePen(Color color) + => GetDarkModeColor(color).GetCachedPenScope(); + + /// + /// Returns whether the background should be painted. + /// + /// The ToolStrip to check. + /// true if the background should be painted; otherwise, false. + private static bool ShouldPaintBackground(ToolStrip toolStrip) + => toolStrip?.BackgroundImage is null; + + /// + /// Fills the background with the specified color. + /// + /// The Graphics to draw on. + /// The bounds to fill. + /// The background color. + private static void FillBackground(Graphics g, Rectangle bounds, Color backColor) + { + if (bounds.Width <= 0 || bounds.Height <= 0) + return; + + // Use a dark mode color + using var brush = GetDarkModeBrush(backColor); + g.FillRectangle(brush, bounds); + } + + /// + /// Raises the RenderToolStripBackground event. + /// + /// A ToolStripRenderEventArgs that contains the event data. + protected override void OnRenderToolStripBackground(ToolStripRenderEventArgs e) + { + ArgumentNullException.ThrowIfNull(e); + + ToolStrip toolStrip = e.ToolStrip; + Graphics g = e.Graphics; + Rectangle bounds = e.AffectedBounds; + + if (!ShouldPaintBackground(toolStrip)) + return; + + if (toolStrip is StatusStrip) + { + RenderStatusStripBackground(e); + } + else + { + if (toolStrip.IsDropDown) + { + // Dark mode dropdown background + FillBackground(g, bounds, GetDarkModeColor(SystemColors.Menu)); + } + else if (toolStrip is MenuStrip) + { + // Dark mode menu background + FillBackground(g, bounds, GetDarkModeColor(SystemColors.Menu)); + } + else + { + // Standard ToolStrip background + FillBackground(g, bounds, GetDarkModeColor(e.BackColor)); + } + } + } + + /// + /// Renders the StatusStrip background in dark mode. + /// + /// A ToolStripRenderEventArgs that contains the event data. + internal static void RenderStatusStripBackground(ToolStripRenderEventArgs e) + { + Graphics g = e.Graphics; + Rectangle bounds = e.AffectedBounds; + + // Dark mode StatusStrip background + FillBackground(g, bounds, GetDarkModeColor(SystemColors.Control)); + } + + /// + /// Raises the RenderToolStripBorder event. + /// + /// A ToolStripRenderEventArgs that contains the event data. + protected override void OnRenderToolStripBorder(ToolStripRenderEventArgs e) + { + ArgumentNullException.ThrowIfNull(e); + + ToolStrip toolStrip = e.ToolStrip; + Graphics g = e.Graphics; + Rectangle bounds = e.ToolStrip.ClientRectangle; + + if (toolStrip is StatusStrip) + { + RenderStatusStripBorder(e); + } + else if (toolStrip is ToolStripDropDown) + { + ToolStripDropDown? toolStripDropDown = toolStrip as ToolStripDropDown; + + Debug.Assert(toolStripDropDown is not null, $"ToolStripDropDown cannot be null in {nameof(OnRenderToolStripBorder)}."); + + if (toolStripDropDown.DropShadowEnabled) + { + bounds.Width -= 1; + bounds.Height -= 1; + + using var borderPen = GetDarkModePen(SystemColors.ControlDark); + g.DrawRectangle(borderPen, bounds); + } + else + { + using var borderPen = GetDarkModePen(SystemColors.ControlDark); + g.DrawRectangle(borderPen, bounds); + } + } + else + { + // Draw a subtle bottom border for toolstrips + using var borderPen = GetDarkModePen(SystemColors.ControlDark); + g.DrawLine(borderPen, 0, bounds.Bottom - 1, bounds.Width, bounds.Bottom - 1); + } + } + + /// + /// Renders the StatusStrip border in dark mode. + /// + /// A ToolStripRenderEventArgs that contains the event data. + private static void RenderStatusStripBorder(ToolStripRenderEventArgs e) + { + Graphics g = e.Graphics; + Rectangle bounds = e.ToolStrip.ClientRectangle; + + // Dark mode StatusStrip border (usually top border only) + using var borderPen = GetDarkModePen(SystemColors.ControlDark); + g.DrawLine(borderPen, 0, 0, bounds.Width, 0); + } + + /// + /// Raises the RenderItemBackground event. + /// + /// A ToolStripItemRenderEventArgs that contains the event data. + protected override void OnRenderItemBackground(ToolStripItemRenderEventArgs e) + { + ArgumentNullException.ThrowIfNull(e); + + Rectangle bounds = new Rectangle(Point.Empty, e.Item.Size); + + // For items on dropdowns, adjust the bounds + if (e.Item.IsOnDropDown) + { + bounds.X += 2; + bounds.Width -= 3; + } + + if (e.Item.Selected || e.Item.Pressed) + { + // Dark mode selection highlight + using var highlightBrush = GetDarkModeBrush(SystemColors.Highlight); + e.Graphics.FillRectangle(highlightBrush, bounds); + } + else + { + // Render background image if available + if (e.Item.BackgroundImage is not null) + { + ControlPaint.DrawBackgroundImage( + e.Graphics, + e.Item.BackgroundImage, + GetDarkModeColor(e.Item.BackColor), + e.Item.BackgroundImageLayout, + e.Item.ContentRectangle, + bounds); + } + else if (e.Item.BackColor != Color.Transparent && e.Item.BackColor != Color.Empty) + { + // Custom background color (apply dark mode transformation) + FillBackground(e.Graphics, bounds, e.Item.BackColor); + } + } + } + + /// + /// Raises the RenderButtonBackground event. + /// + /// A ToolStripItemRenderEventArgs that contains the event data. + protected override void OnRenderButtonBackground(ToolStripItemRenderEventArgs e) + { + ArgumentNullException.ThrowIfNull(e); + + Rectangle bounds = new Rectangle(Point.Empty, e.Item.Size); + bool isPressed; + bool isSelected; + + if (e.Item is ToolStripButton button) + { + isPressed = button.Pressed; + isSelected = button.Selected || button.Checked; + } + else + { + isPressed = e.Item.Pressed; + isSelected = e.Item.Selected; + } + + if (isPressed || isSelected) + { + using var fillColor = isPressed + ? GetDarkModeBrush(SystemColors.ControlDark) + : GetDarkModeBrush(SystemColors.Highlight); + + e.Graphics.FillRectangle(fillColor, bounds); + } + } + + /// + /// Raises the RenderDropDownButtonBackground event. + /// + /// A ToolStripItemRenderEventArgs that contains the event data. + protected override void OnRenderDropDownButtonBackground(ToolStripItemRenderEventArgs e) + { + // Reuse button background drawing for dropdown buttons + OnRenderButtonBackground(e); + } + + /// + /// Raises the RenderSplitButtonBackground event. + /// + /// A ToolStripItemRenderEventArgs that contains the event data. + protected override void OnRenderSplitButtonBackground(ToolStripItemRenderEventArgs e) + { + ArgumentNullException.ThrowIfNull(e); + + if (e.Item is not ToolStripSplitButton splitButton) + { + return; + } + + Rectangle bounds = new Rectangle(Point.Empty, e.Item.Size); + + // Render the background based on state + if (splitButton.Selected || splitButton.Pressed) + { + using var fillColor = splitButton.Pressed + ? GetDarkModeBrush(SystemColors.ControlDark) + : GetDarkModeBrush(SystemColors.Highlight); + + e.Graphics.FillRectangle(fillColor, bounds); + } + + // Draw the split line + Rectangle dropDownRect = splitButton.DropDownButtonBounds; + using var linePen = GetDarkModePen(SystemColors.ControlDark); + + e.Graphics.DrawLine( + linePen, + dropDownRect.Left - 1, + dropDownRect.Top + 2, + dropDownRect.Left - 1, + dropDownRect.Bottom - 2); + + DrawArrow(new ToolStripArrowRenderEventArgs( + e.Graphics, + e.Item, + dropDownRect, + SystemColors.ControlText, + ArrowDirection.Down)); + } + + /// + /// Raises the RenderSeparator event. + /// + /// A ToolStripSeparatorRenderEventArgs that contains the event data. + protected override void OnRenderSeparator(ToolStripSeparatorRenderEventArgs e) + { + ArgumentNullException.ThrowIfNull(e); + + Rectangle bounds = e.Item.ContentRectangle; + bool isVertical = e.Vertical; + Graphics g = e.Graphics; + + if (bounds.Width <= 0 || bounds.Height <= 0) + { + return; + } + + bool rightToLeft = e.Item.RightToLeft == RightToLeft.Yes; + + if (isVertical) + { + int startX = bounds.Width / 2; + + if (rightToLeft) + { + using var leftPen = GetDarkModeColor(SystemColors.ControlDark).GetCachedPenScope(); + g.DrawLine(leftPen, startX, bounds.Top, startX, bounds.Bottom); + + startX++; + + using var rightPen = GetDarkModeColor(SystemColors.ButtonShadow).GetCachedPenScope(); + g.DrawLine(rightPen, startX, bounds.Top, startX, bounds.Bottom); + } + else + { + using var leftPen = GetDarkModeColor(SystemColors.ButtonShadow).GetCachedPenScope(); + g.DrawLine(leftPen, startX, bounds.Top, startX, bounds.Bottom); + + startX++; + using var rightPen = GetDarkModeColor(SystemColors.ControlDark).GetCachedPenScope(); + g.DrawLine(rightPen, startX, bounds.Top, startX, bounds.Bottom); + } + } + else + { + // Horizontal separator + if (bounds.Width >= 4) + { + bounds.Inflate(-2, 0); // Scoot over 2px and start drawing + } + + int startY = bounds.Height / 2; + using var foreColorPen = GetDarkModeColor(SystemColors.ControlDark).GetCachedPenScope(); + g.DrawLine(foreColorPen, bounds.Left, startY, bounds.Right, startY); + + startY++; + using var darkModePen = GetDarkModeColor(SystemColors.ButtonShadow).GetCachedPenScope(); + g.DrawLine(darkModePen, bounds.Left, startY, bounds.Right, startY); + } + } + + /// + /// Raises the RenderOverflowButtonBackground event. + /// + /// A ToolStripItemRenderEventArgs that contains the event data. + protected override void OnRenderOverflowButtonBackground(ToolStripItemRenderEventArgs e) + { + ArgumentNullException.ThrowIfNull(e); + + if (e.Item is not ToolStripOverflowButton item) + return; + + Rectangle bounds = new Rectangle(Point.Empty, e.Item.Size); + + // Render the background based on state + if (item.Selected || item.Pressed) + { + using var fillBrush = item.Pressed + ? GetDarkModeColor(SystemColors.ControlDark).GetCachedSolidBrushScope() + : GetDarkModeColor(SystemColors.Highlight).GetCachedSolidBrushScope(); + + e.Graphics.FillRectangle(fillBrush, bounds); + } + + // Draw the overflow arrow + Rectangle arrowRect = item.ContentRectangle; + + Point middle = new Point( + arrowRect.Left + arrowRect.Width / 2, + arrowRect.Top + arrowRect.Height / 2); + + // Default to down arrow for overflow buttons + ArrowDirection arrowDirection = ArrowDirection.Down; + + // Determine actual direction based on dropdown direction + ToolStripDropDownDirection direction = item.DropDownDirection; + + if (direction is ToolStripDropDownDirection.AboveLeft or ToolStripDropDownDirection.AboveRight) + { + arrowDirection = ArrowDirection.Up; + } + else if (direction == ToolStripDropDownDirection.Left) + { + arrowDirection = ArrowDirection.Left; + } + else if (direction == ToolStripDropDownDirection.Right) + { + arrowDirection = ArrowDirection.Right; + } + + // else default to ArrowDirection.Down + + // Set arrow color based on state + using var arrowBrush = GetDarkModeColor( + item.Pressed || item.Selected + ? SystemColors.HighlightText + : SystemColors.ControlText) + .GetCachedSolidBrushScope(); + + // Define arrow polygon based on direction + Point[] arrow = arrowDirection switch + { + ArrowDirection.Up => + [ + new Point(middle.X - 2, middle.Y + 1), + new Point(middle.X + 3, middle.Y + 1), + new Point(middle.X, middle.Y - 2) + ], + ArrowDirection.Left => + [ + new Point(middle.X + 2, middle.Y - 4), + new Point(middle.X + 2, middle.Y + 4), + new Point(middle.X - 2, middle.Y) + ], + ArrowDirection.Right => + [ + new Point(middle.X - 2, middle.Y - 4), + new Point(middle.X - 2, middle.Y + 4), + new Point(middle.X + 2, middle.Y) + ], + _ => + [ + new Point(middle.X - 2, middle.Y - 1), + new Point(middle.X + 3, middle.Y - 1), + new Point(middle.X, middle.Y + 2) + ], + }; + + // Draw the arrow + e.Graphics.FillPolygon(arrowBrush, arrow); + } + + /// + /// Raises the RenderGrip event. + /// + /// A ToolStripGripRenderEventArgs that contains the event data. + protected override void OnRenderGrip(ToolStripGripRenderEventArgs e) + { + ArgumentNullException.ThrowIfNull(e); + + if (e.GripBounds.Width <= 0 || e.GripBounds.Height <= 0) + return; + + // Use dark mode colors for the grip dots + using var darkColorBrush = GetDarkModeColor(SystemColors.ControlDark).GetCachedSolidBrushScope(); + using var lightColorBrush = GetDarkModeColor(SystemColors.ControlLight).GetCachedSolidBrushScope(); + + ToolStrip toolStrip = e.ToolStrip; + Graphics g = e.Graphics; + Rectangle bounds = e.GripBounds; + + // Draw grip dots + if (toolStrip.Orientation == Orientation.Horizontal) + { + // Draw vertical grip + int y = bounds.Top + 2; + + while (y < bounds.Bottom - 3) + { + g.FillRectangle(darkColorBrush, bounds.Left + 2, y, 1, 1); + g.FillRectangle(lightColorBrush, bounds.Left + 3, y + 1, 1, 1); + y += 3; + } + } + else + { + // Draw horizontal grip + int x = bounds.Left + 2; + + while (x < bounds.Right - 3) + { + g.FillRectangle(darkColorBrush, x, bounds.Top + 2, 1, 1); + g.FillRectangle(lightColorBrush, x + 1, bounds.Top + 3, 1, 1); + x += 3; + } + } + } + + /// + /// Raises the RenderArrow event. + /// + /// A ToolStripArrowRenderEventArgs that contains the event data. + /// + /// Raises the RenderArrow event in the derived class with dark mode support. + /// + protected override void OnRenderArrow(ToolStripArrowRenderEventArgs e) + { + ArgumentNullException.ThrowIfNull(e); + Debug.Assert(e.Item is not null, "The ToolStripItem should not be null on rendering the Arrow."); + + Color arrowColor = GetDarkModeColor(e.ArrowColor); + + // Use white arrow for selected/highlighted items + if (e.Item.Selected || e.Item.Pressed) + { + arrowColor = GetDarkModeColor(SystemColors.HighlightText); + } + + RenderArrowCore(e, arrowColor); + } + + /// + /// Raises the RenderImageMargin event. + /// + /// A ToolStripRenderEventArgs that contains the event data. + protected override void OnRenderImageMargin(ToolStripRenderEventArgs e) + { + ArgumentNullException.ThrowIfNull(e); + + // Fill the image margin with a slightly different color than the background + using var marginColorBrush = GetDarkModeColor(SystemColors.ControlLight).GetCachedSolidBrushScope(); + + e.Graphics.FillRectangle(marginColorBrush, e.AffectedBounds); + } + + /// + /// Raises the RenderItemText event. + /// + /// A ToolStripItemTextRenderEventArgs that contains the event data. + protected override void OnRenderItemText(ToolStripItemTextRenderEventArgs e) + { + ArgumentNullException.ThrowIfNull(e); + + // Set text color based on selection state + Color textColor; + + if (e.Item.Selected || e.Item.Pressed) + { + textColor = GetDarkModeColor(SystemColors.HighlightText); + } + else if (!e.Item.Enabled) + { + textColor = GetDarkModeColor(SystemColors.GrayText); + } + else + { + // Use the original text color but make sure it's dark mode compatible + textColor = GetDarkModeColor(e.TextColor); + } + + // Draw the text + TextRenderer.DrawText( + e.Graphics, + e.Text, + e.TextFont, + e.TextRectangle, + textColor, + e.TextFormat); + } + + /// + /// Raises the RenderItemImage event. + /// + /// A ToolStripItemImageRenderEventArgs that contains the event data. + protected override void OnRenderItemImage(ToolStripItemImageRenderEventArgs e) + { + ArgumentNullException.ThrowIfNull(e); + + if (e.Image is null) + return; + + // DarkMode adjustments for the image are done by + // the base class implementation already. + Image image = !e.Item.Enabled + ? CreateDisabledImage(e.Image) + : e.Image; + + e.Graphics.DrawImage(image, e.ImageRectangle); + } + + /// + /// Raises the RenderLabelBackground event. + /// + /// A ToolStripItemRenderEventArgs that contains the event data. + protected override void OnRenderLabelBackground(ToolStripItemRenderEventArgs e) + { + // Use default item background rendering + OnRenderItemBackground(e); + } + + /// + /// Raises the RenderToolStripStatusLabelBackground event. + /// + /// A ToolStripItemRenderEventArgs that contains the event data. + protected override void OnRenderStatusStripSizingGrip(ToolStripRenderEventArgs e) + { + ArgumentNullException.ThrowIfNull(e); + + using var highLightBrush = GetDarkModeBrush(SystemColors.GrayText); + using var shadowBrush = GetDarkModeBrush(SystemColors.ButtonShadow); + + OnRenderStatusStripSizingGrip( + eArgs: e, + highLightBrush: highLightBrush, + shadowBrush: shadowBrush); + } +} diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemRenderer.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemRenderer.cs index a8539cc6c51..3af571eeddb 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemRenderer.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemRenderer.cs @@ -6,47 +6,83 @@ namespace System.Windows.Forms; +/// +/// Provides system rendering for ToolStrip controls with support for dark mode. +/// public class ToolStripSystemRenderer : ToolStripRenderer { [ThreadStatic] private static VisualStyleRenderer? t_renderer; + private ToolStripRenderer? _toolStripHighContrastRenderer; + private ToolStripRenderer? _toolStripDarkModeRenderer; + /// + /// Initializes a new instance of the ToolStripSystemRenderer class. + /// public ToolStripSystemRenderer() { } + /// + /// Initializes a new instance of the ToolStripSystemRenderer class with the specified default state. + /// + /// true if this is the default renderer; otherwise, false. internal ToolStripSystemRenderer(bool isDefault) : base(isDefault) { } - internal override ToolStripRenderer? RendererOverride + /// + /// Gets the HighContrastRenderer for accessibility support. + /// + internal ToolStripRenderer HighContrastRenderer { get { - if (DisplayInformation.HighContrast) - { - return HighContrastRenderer; - } + _toolStripHighContrastRenderer ??= new ToolStripHighContrastRenderer(systemRenderMode: false); + return _toolStripHighContrastRenderer; + } + } - return null; + /// + /// Gets the DarkModeRenderer for dark mode support. + /// + internal ToolStripRenderer DarkModeRenderer + { + get + { + _toolStripDarkModeRenderer ??= new ToolStripSystemDarkModeRenderer(isSystemDefaultAlternative: false); + return _toolStripDarkModeRenderer; } } - internal ToolStripRenderer HighContrastRenderer + /// + /// Gets the renderer that should be used based on current display settings. + /// + internal override ToolStripRenderer? RendererOverride { get { - // If system in high contrast mode 'false' flag should be passed to render filled selected button background. - // This is in consistence with ToolStripProfessionalRenderer. - _toolStripHighContrastRenderer ??= new ToolStripHighContrastRenderer(systemRenderMode: false); + // First, check for high contrast mode (accessibility takes precedence) + if (DisplayInformation.HighContrast) + { + return HighContrastRenderer; + } - return _toolStripHighContrastRenderer; + // Then check for dark mode +#pragma warning disable WFO5001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + if (Application.IsDarkModeEnabled) + { + return DarkModeRenderer; + } +#pragma warning restore WFO5001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + + return null; } } /// - /// Draw the background color + /// Get the Visual Style Renderer. This is used to draw the background of the ToolStrip. /// private static VisualStyleRenderer? VisualStyleRenderer { @@ -182,6 +218,18 @@ private static ToolBarState GetToolBarState(ToolStripItem item) /// protected override void OnRenderToolStripBackground(ToolStripRenderEventArgs e) { + // It is not entirely clear, why we're not leaving it to the respective renderers + // what to do here, but for keeping as much as possible of the original behavior, + // we need to check if the renderer override is set, test for particular renderer types + // and then decide what to do. + if (RendererOverride is ToolStripSystemDarkModeRenderer darkModeRenderer + && darkModeRenderer.IsSystemDefaultAlternative) + { + base.OnRenderToolStripBackground(e); + + return; + } + ToolStrip toolStrip = e.ToolStrip; Graphics g = e.Graphics; Rectangle bounds = e.AffectedBounds; @@ -203,19 +251,24 @@ protected override void OnRenderToolStripBackground(ToolStripRenderEventArgs e) } else if (DisplayInformation.LowResolution) { - FillBackground(g, bounds, (toolStrip is ToolStripDropDown) ? SystemColors.ControlLight : e.BackColor); + FillBackground(g, bounds, toolStrip is ToolStripDropDown + ? SystemColors.ControlLight + : e.BackColor); } else if (toolStrip.IsDropDown) { - FillBackground(g, bounds, (!ToolStripManager.VisualStylesEnabled) ? - e.BackColor : SystemColors.Menu); + FillBackground(g, bounds, ToolStripManager.VisualStylesEnabled + ? SystemColors.Menu + : e.BackColor); } else if (toolStrip is MenuStrip) { - FillBackground(g, bounds, (!ToolStripManager.VisualStylesEnabled) ? - e.BackColor : SystemColors.MenuBar); + FillBackground(g, bounds, ToolStripManager.VisualStylesEnabled + ? SystemColors.MenuBar + : e.BackColor); } - else if (ToolStripManager.VisualStylesEnabled && VisualStyleRenderer.IsElementDefined(VisualStyleElement.Rebar.Band.Normal)) + else if (ToolStripManager.VisualStylesEnabled + && VisualStyleRenderer.IsElementDefined(VisualStyleElement.Rebar.Band.Normal)) { VisualStyleRenderer vsRenderer = VisualStyleRenderer!; vsRenderer.SetParameters(VisualStyleElement.ToolBar.Bar.Normal); @@ -223,8 +276,9 @@ protected override void OnRenderToolStripBackground(ToolStripRenderEventArgs e) } else { - FillBackground(g, bounds, (!ToolStripManager.VisualStylesEnabled) ? - e.BackColor : SystemColors.MenuBar); + FillBackground(g, bounds, ToolStripManager.VisualStylesEnabled + ? SystemColors.MenuBar + : e.BackColor); } } } @@ -279,7 +333,8 @@ protected override void OnRenderGrip(ToolStripGripRenderEventArgs e) Rectangle bounds = new(Point.Empty, e.GripBounds.Size); bool verticalGrip = e.GripDisplayStyle == ToolStripGripDisplayStyle.Vertical; - if (ToolStripManager.VisualStylesEnabled && VisualStyleRenderer.IsElementDefined(VisualStyleElement.Rebar.Gripper.Normal)) + if (ToolStripManager.VisualStylesEnabled + && VisualStyleRenderer.IsElementDefined(VisualStyleElement.Rebar.Gripper.Normal)) { VisualStyleRenderer vsRenderer = VisualStyleRenderer!; @@ -472,7 +527,7 @@ protected override void OnRenderMenuItemBackground(ToolStripItemRenderEventArgs { ControlPaint.DrawBackgroundImage(g, item.BackgroundImage, item.BackColor, item.BackgroundImageLayout, item.ContentRectangle, fillRect); } - else if (!ToolStripManager.VisualStylesEnabled && (item.RawBackColor != Color.Empty)) + else if (!(ToolStripManager.VisualStylesEnabled || item.RawBackColor == Color.Empty)) { FillBackground(g, fillRect, item.BackColor); } @@ -521,11 +576,18 @@ protected override void OnRenderSplitButtonBackground(ToolStripItemRenderEventAr Graphics g = e.Graphics; bool rightToLeft = splitButton.RightToLeft == RightToLeft.Yes; - Color arrowColor = splitButton.Enabled ? SystemColors.ControlText : SystemColors.ControlDark; + Color arrowColor = splitButton.Enabled + ? SystemColors.ControlText + : SystemColors.ControlDark; // in right to left - we need to swap the parts so we don't draw v][ toolStripSplitButton - VisualStyleElement splitButtonDropDownPart = rightToLeft ? VisualStyleElement.ToolBar.SplitButton.Normal : VisualStyleElement.ToolBar.SplitButtonDropDown.Normal; - VisualStyleElement splitButtonPart = rightToLeft ? VisualStyleElement.ToolBar.DropDownButton.Normal : VisualStyleElement.ToolBar.SplitButton.Normal; + VisualStyleElement splitButtonDropDownPart = rightToLeft + ? VisualStyleElement.ToolBar.SplitButton.Normal + : VisualStyleElement.ToolBar.SplitButtonDropDown.Normal; + + VisualStyleElement splitButtonPart = rightToLeft + ? VisualStyleElement.ToolBar.DropDownButton.Normal + : VisualStyleElement.ToolBar.SplitButton.Normal; if (ToolStripManager.VisualStylesEnabled && VisualStyleRenderer.IsElementDefined(splitButtonDropDownPart) @@ -536,10 +598,10 @@ protected override void OnRenderSplitButtonBackground(ToolStripItemRenderEventAr // Draw the SplitButton Button portion of it. vsRenderer.SetParameters(splitButtonPart.ClassName, splitButtonPart.Part, GetSplitButtonItemState(splitButton)); - // the lovely Windows theming for split button comes in three pieces: + // the Windows theming for split button comes in three pieces: // SplitButtonDropDown: [ v | - // Separator: | - // SplitButton: | ] + // Separator: | + // SplitButton: | ] // this is great except if you want to swap the button in RTL. In this case we need // to use the DropDownButton instead of the SplitButtonDropDown and paint the arrow ourselves. Rectangle splitButtonBounds = splitButton.ButtonBounds; diff --git a/src/System.Windows.Forms/System/Windows/Forms/Form.cs b/src/System.Windows.Forms/System/Windows/Forms/Form.cs index f5da26c3ea1..78d01467ad0 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Form.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Form.cs @@ -2183,7 +2183,6 @@ protected override void SetVisibleCore(bool value) [SRDescription(nameof(SR.FormCornerPreferenceDescr))] [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - [Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)] public FormCornerPreference FormCornerPreference { get => Properties.GetValueOrDefault(s_propFormCornerPreference, FormCornerPreference.Default); @@ -2221,7 +2220,6 @@ public FormCornerPreference FormCornerPreference /// /// An that contains the event data, in this case empty. /// - [Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)] protected virtual void OnFormCornerPreferenceChanged(EventArgs e) { if (Events[s_formCornerPreferenceChanged] is EventHandler eventHandler) @@ -2230,9 +2228,7 @@ protected virtual void OnFormCornerPreferenceChanged(EventArgs e) } } -#pragma warning disable WFO5001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. private unsafe void SetFormCornerPreferenceInternal(FormCornerPreference cornerPreference) -#pragma warning restore WFO5001 { DWM_WINDOW_CORNER_PREFERENCE dwmCornerPreference = cornerPreference switch { @@ -2275,7 +2271,6 @@ private unsafe void SetFormCornerPreferenceInternal(FormCornerPreference cornerP [SRDescription(nameof(SR.FormBorderColorDescr))] [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - [Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)] public Color FormBorderColor { get => Properties.GetValueOrDefault(s_propFormBorderColor, Color.Empty); @@ -2303,7 +2298,6 @@ public Color FormBorderColor /// /// An that contains the event data, in this case empty. /// - [Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)] protected virtual void OnFormBorderColorChanged(EventArgs e) { if (Events[s_formBorderColorChanged] is EventHandler eventHandler) @@ -2337,7 +2331,6 @@ protected virtual void OnFormBorderColorChanged(EventArgs e) [SRDescription(nameof(SR.FormCaptionBackColorDescr))] [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - [Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)] public Color FormCaptionBackColor { get => Properties.GetValueOrDefault(s_propFormCaptionBackColor, Color.Empty); @@ -2366,7 +2359,6 @@ public Color FormCaptionBackColor /// /// An that contains the event data, in this case empty. /// - [Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)] protected virtual void OnFormCaptionBackColorChanged(EventArgs e) { if (Events[s_formCaptionBackColorChanged] is EventHandler eventHandler) @@ -2400,7 +2392,6 @@ protected virtual void OnFormCaptionBackColorChanged(EventArgs e) [SRDescription(nameof(SR.FormCaptionTextColorDescr))] [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - [Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)] public Color FormCaptionTextColor { get => Properties.GetValueOrDefault(s_propFormCaptionTextColor, Color.Empty); @@ -2429,7 +2420,6 @@ public Color FormCaptionTextColor /// /// An that contains the event data, in this case empty. /// - [Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)] protected virtual void OnFormCaptionTextColorChanged(EventArgs e) { if (Events[s_formCaptionTextColorChanged] is EventHandler eventHandler) diff --git a/src/System.Windows.Forms/System/Windows/Forms/FormCornerPreference.cs b/src/System.Windows.Forms/System/Windows/Forms/FormCornerPreference.cs index 7b978054b6b..b6b4228c41d 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/FormCornerPreference.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/FormCornerPreference.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Windows.Forms.Analyzers.Diagnostics; using Windows.Win32.Graphics.Dwm; namespace System.Windows.Forms; @@ -10,7 +9,6 @@ namespace System.Windows.Forms; /// Specifies the corner preference for a which can be /// set using the property. /// -[Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)] public enum FormCornerPreference { ///