Skip to content

DarkMode (b) DarkModeButtonRenderer #13408

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: klaus/preview5/A
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# C# Refactor: Pen & Brush to Cached Scope API

You are an expert C# code transformer.

**Goal:** Rewrite every creation and usage of `Pen` and `SolidBrush` in the input file to use our `RefCountedCache` (or `PenCache`/`SolidBrushCache`) scoped API instead of direct `new` calls or helper methods returning non-cached objects.

---

## Transformation Rules

1. **Direct Instantiations**
- **Pens**
- Replace `new Pen(color)` with:
```csharp
using var pen = color.GetCachedPenScope();
```
- Replace `new Pen(color, width)` with:
```csharp
using var pen = color.GetCachedPenScope(width);
```
- Preserve original variable names, e.g.:
```csharp
using var borderPen = color.GetCachedPenScope();
```

- **SolidBrushes**
- Replace `new SolidBrush(color)` with:
```csharp
using var brush = color.GetCachedSolidBrushScope();
```
- Preserve original variable names.

2. **Helper Methods**
- **Detection:**
- Identify any methods in the class that return `Pen` or `SolidBrush`.
- If a helper returns a raw `Pen` or `SolidBrush`, transform it to return the corresponding cache‐scope type (`PenCache.Scope` or `SolidBrushCache.Scope`).

- **Private Helpers:**
- Change the return type to the scope type.
- Update the method body to call the cache API directly (e.g., `color.GetCachedPenScope()`).

- **Public/Internal Helpers:**
- If the helper is externally visible, create a new class‐scoped helper method (e.g., `GetDarkModePenScope`) returning `PenCache.Scope` or `SolidBrushCache.Scope`, based on the original logic, and leave the original helper untouched.

3. **Usages in Drawing Calls**
- All `g.Draw...` or `g.Fill...` invocations that previously took a `Pen` or `Brush` should work unchanged with the new scoped objects via the implicit converter.

4. **Preserve Formatting & Logic**
- Do not alter any logic that is unrelated to Pen/Brush instantiation.
- Maintain existing code style and formatting.

---

## Example

**Before:**
```csharp
using (var p = new Pen(Color.Red, 2))
{
g.DrawEllipse(p, bounds);
}

private Pen GetCustomBorderPen(Color color)
{
return new Pen(color);
}
```

**After:**
```csharp
using var p = Color.Red.GetCachedPenScope(2);
g.DrawEllipse(p, bounds);

private PenCache.Scope GetCustomBorderPenScope(Color color)
=> color.GetCachedPenScope();
```

---

**Task:** Rewrite the entire input file accordingly and output the modified code.
13 changes: 12 additions & 1 deletion Winforms.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down Expand Up @@ -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}") = "AI-Prompts", "AI-Prompts", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
ProjectSection(SolutionItems) = preProject
.github\copilot-instructions.md = .github\copilot-instructions.md
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GDI", "GDI", "{C2AEC915-89F2-4FE1-B03D-C811684D98AF}"
ProjectSection(SolutionItems) = preProject
.github\AI-Prompts\GDI-Refactoring\AIP-cache_pens-and_brushes.md = .github\AI-Prompts\GDI-Refactoring\AIP-cache_pens-and_brushes.md
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -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}
{C2AEC915-89F2-4FE1-B03D-C811684D98AF} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {7B1B0433-F612-4E5A-BE7E-FCF5B9F6E136}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ public AutoSizeMode AutoSizeMode

internal override ButtonBaseAdapter CreateStandardAdapter() => new ButtonStandardAdapter(this);

internal override ButtonBaseAdapter CreateDarkModeAdapter() => new ButtonDarkModeAdapter(this);

internal override Size GetPreferredSizeCore(Size proposedConstraints)
{
if (FlatStyle != FlatStyle.System)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,6 @@ protected ButtonBase()
SetExtendedState(ExtendedStates.UserPreferredSizeCache, true);

SetStyle(ControlStyles.UserMouse | ControlStyles.UserPaint, OwnerDraw);

#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.
SetStyle(ControlStyles.ApplyThemingImplicitly, true);
#pragma warning restore WFO5001

SetFlag(FlagUseMnemonic, true);
SetFlag(FlagShowToolTip, false);
}
Expand Down Expand Up @@ -617,7 +612,11 @@ internal virtual Rectangle OverChangeRectangle
}
}

internal bool OwnerDraw => FlatStyle != FlatStyle.System;
#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.
internal bool OwnerDraw =>
(FlatStyle != FlatStyle.System)
|| 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.

bool? ICommandBindingTargetProvider.PreviousEnabledStatus { get; set; }

Expand Down Expand Up @@ -957,35 +956,44 @@ public override Size GetPreferredSize(Size proposedSize)

internal override Size GetPreferredSizeCore(Size proposedConstraints)
{
Size preferedSize = Adapter.GetPreferredSizeCore(proposedConstraints);
return LayoutUtils.UnionSizes(preferedSize + Padding.Size, MinimumSize);
Size preferredSize = Adapter.GetPreferredSizeCore(proposedConstraints);
return LayoutUtils.UnionSizes(preferredSize + Padding.Size, MinimumSize);
}

internal ButtonBaseAdapter Adapter
{
get
{
if (_adapter is null || FlatStyle != _cachedAdapterType)
#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 (_adapter is null
|| FlatStyle != _cachedAdapterType)
{
switch (FlatStyle)
if (Application.IsDarkModeEnabled)
{
case FlatStyle.Standard:
_adapter = CreateStandardAdapter();
break;
case FlatStyle.Popup:
_adapter = CreatePopupAdapter();
break;
case FlatStyle.Flat:
_adapter = CreateFlatAdapter();
;
break;
default:
Debug.Fail($"Unsupported FlatStyle: \"{FlatStyle}\"");
break;
_adapter = CreateDarkModeAdapter();
}
else
{
switch (FlatStyle)
{
case FlatStyle.Standard:
_adapter = CreateStandardAdapter();
break;
case FlatStyle.Popup:
_adapter = CreatePopupAdapter();
break;
case FlatStyle.Flat:
_adapter = CreateFlatAdapter();
break;
default:
Debug.Fail($"Unsupported FlatStyle: \"{FlatStyle}\"");
break;
}

_cachedAdapterType = FlatStyle;
_cachedAdapterType = FlatStyle;
}
}
#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 _adapter;
}
Expand All @@ -1009,6 +1017,14 @@ internal virtual ButtonBaseAdapter CreateStandardAdapter()
return null;
}

internal virtual ButtonBaseAdapter CreateDarkModeAdapter()
{
// When a button-derived class does not have a dedicated DarkMode adapter implementation,
// we're falling back to the standard adapter, to not _force_ the derived class to implement
// a dark mode adapter.
return CreateStandardAdapter();
}

internal virtual StringFormat CreateStringFormat()
{
if (Adapter is null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -643,15 +643,18 @@ private ColorOptions CommonRender(IDeviceContext deviceContext) =>
Enabled = Control.Enabled
};

protected ColorOptions PaintRender(IDeviceContext deviceContext) => CommonRender(deviceContext);
protected ColorOptions PaintRender(IDeviceContext deviceContext)
=> CommonRender(deviceContext);

internal static ColorOptions PaintFlatRender(Graphics g, Color foreColor, Color backColor, bool enabled) =>
CommonRender(g, foreColor, backColor, enabled);

protected ColorOptions PaintFlatRender(IDeviceContext deviceContext) => CommonRender(deviceContext);
protected ColorOptions PaintFlatRender(IDeviceContext deviceContext)
=> CommonRender(deviceContext);

internal static ColorOptions PaintPopupRender(Graphics g, Color foreColor, Color backColor, bool enabled) =>
CommonRender(g, foreColor, backColor, enabled);

protected ColorOptions PaintPopupRender(IDeviceContext deviceContext) => CommonRender(deviceContext);
protected ColorOptions PaintPopupRender(IDeviceContext deviceContext)
=> CommonRender(deviceContext);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// 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;
using System.Windows.Forms.VisualStyles;

namespace System.Windows.Forms.ButtonInternal;

internal class ButtonDarkModeAdapter : ButtonBaseAdapter
{
internal ButtonDarkModeAdapter(ButtonBase control) : base(control) { }

internal override void PaintUp(PaintEventArgs e, CheckState state)
{
var smoothingMode = e.Graphics.SmoothingMode;
e.Graphics.SmoothingMode = Drawing.Drawing2D.SmoothingMode.AntiAlias;

LayoutData layout = CommonLayout().Layout();

ButtonDarkModeRenderer.RenderButton(
e.Graphics,
Control.ClientRectangle,
Control.FlatStyle,
ToPushButtonState(state, Control.Enabled),
Control.IsDefault,
Control.Focused,
Control.ShowFocusCues,
Control.Parent?.BackColor ?? Control.BackColor,
_ => PaintImage(e, layout),
(_, textColor, drawFocus) => PaintField(
e,
layout,
PaintDarkModeRender(e).Calculate(),
textColor,
drawFocus: false)
);

e.Graphics.SmoothingMode = smoothingMode;
}

internal override void PaintDown(PaintEventArgs e, CheckState state)
{
// Set the smoothing mode to AntiAlias for better rendering quality
var smoothingMode = e.Graphics.SmoothingMode;
e.Graphics.SmoothingMode = Drawing.Drawing2D.SmoothingMode.AntiAlias;

LayoutData layout = CommonLayout().Layout();
ButtonDarkModeRenderer.RenderButton(
e.Graphics,
Control.ClientRectangle,
Control.FlatStyle,
PushButtonState.Pressed,
Control.IsDefault,
Control.Focused,
Control.ShowFocusCues,
Control.Parent?.BackColor ?? Control.BackColor,
_ => PaintImage(e, layout),
(_, textColor, drawFocus) => PaintField(
e,
layout,
PaintDarkModeRender(e).Calculate(),
textColor,
drawFocus: false)
);

// Restore the original smoothing mode
e.Graphics.SmoothingMode = smoothingMode;
}

internal override void PaintOver(PaintEventArgs e, CheckState state)
{
// Set the smoothing mode to AntiAlias for better rendering quality
var smoothingMode = e.Graphics.SmoothingMode;
e.Graphics.SmoothingMode = Drawing.Drawing2D.SmoothingMode.AntiAlias;

LayoutData layout = CommonLayout().Layout();
ButtonDarkModeRenderer.RenderButton(
e.Graphics,
Control.ClientRectangle,
Control.FlatStyle,
PushButtonState.Hot,
Control.IsDefault,
Control.Focused,
Control.ShowFocusCues,
Control.Parent?.BackColor ?? Control.BackColor,
_ => PaintImage(e, layout),
(_, textColor, drawFocus) => PaintField(
e,
layout,
PaintDarkModeRender(e).Calculate(),
textColor,
drawFocus: false)
);

// Restore the original smoothing mode
e.Graphics.SmoothingMode = smoothingMode;
}

protected override LayoutOptions Layout(PaintEventArgs e) => CommonLayout();

private new LayoutOptions CommonLayout()
{
LayoutOptions layout = base.CommonLayout();
layout.FocusOddEvenFixup = false;
layout.ShadowedText = false;

return layout;
}

private ColorOptions PaintDarkModeRender(IDeviceContext deviceContext) =>
new(deviceContext, Control.ForeColor, Control.BackColor)
{
Enabled = Control.Enabled
};

private static PushButtonState ToPushButtonState(CheckState state, bool enabled)
{
return !enabled
? PushButtonState.Disabled
: state switch
{
CheckState.Unchecked => PushButtonState.Normal,
CheckState.Checked => PushButtonState.Pressed,
CheckState.Indeterminate => PushButtonState.Hot,
_ => PushButtonState.Normal
};
}
}
Loading