Skip to content

Fix DarkMode (a) StatusStrip background renderer A11Y Regression. #13360

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

Conversation

KlausLoeffelmann
Copy link
Member

@KlausLoeffelmann KlausLoeffelmann commented Apr 23, 2025

image

Fixes #12909.
Fixes #12663.
Fixes #12041.
Fixes #12040.
Fixes #12027.

As for the approach.
We already have a series of Renderers, where we need to avoid passing rendering down to the visual styles. In fact, for the ToolStrip and everything around it, we do most of the render work ourselves.

That also means, we're in many cases no longer compatible with subtle details. The rounded corners of the Windows 11 windows for example, can obscure the symbolic Grip, depending on the Window border color and the resulting Antialias, and leave not enough distance for a good contrast:

image

And then in addition, since we have WAY higher resolutions since back then, we also needed a bigger area for better contrast and visibility. This change addresses that, and also reposition the grip pixel to follow the shape of the rounded corners:

image
image
image

Microsoft Reviewers: Open in CodeFlow

Copy link

codecov bot commented Apr 23, 2025

Codecov Report

Attention: Patch coverage is 24.91639% with 449 lines in your changes missing coverage. Please review.

Project coverage is 76.56041%. Comparing base (380e851) to head (19fc0c4).
Report is 21 commits behind head on main.

Additional details and impacted files
@@                 Coverage Diff                 @@
##                main      #13360         +/-   ##
===================================================
- Coverage   76.60904%   76.56041%   -0.04863%     
===================================================
  Files           3236        3237          +1     
  Lines         639448      639896        +448     
  Branches       47318       47380         +62     
===================================================
+ Hits          489875      489907         +32     
- Misses        146048      146455        +407     
- Partials        3525        3534          +9     
Flag Coverage Δ
Debug 76.56041% <24.91639%> (-0.04863%) ⬇️
integration 18.50252% <9.69900%> (-0.02305%) ⬇️
production 50.94548% <24.91639%> (-0.06844%) ⬇️
test 97.41283% <ø> (ø)
unit 48.35455% <24.91639%> (-0.06719%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@KlausLoeffelmann KlausLoeffelmann force-pushed the DarkMode_a_FixStatusStripRegression branch 3 times, most recently from 8e9ff57 to 5ea89b6 Compare May 6, 2025 13:27
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR addresses several A11Y regressions related to DarkMode rendering by removing experimental diagnostic attributes and refining related rendering logic. Key changes include the removal of experimental DarkMode attributes in form APIs, the introduction and integration of a dedicated dark mode renderer in ToolStripSystemRenderer, and various rendering improvements in ToolStripRenderer and StatusStrip.

Reviewed Changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated no comments.

Show a summary per file
File Description
FormCornerPreference.cs Removed experimental diagnostic attribute for DarkMode support.
Form.cs Removed experimental attribute on DarkMode–related properties and events.
ToolStripSystemRenderer.cs Introduced a dedicated dark mode renderer and updated RendererOverride logic.
ToolStripRenderer.cs Updated rendering paths and optimized array initializations for DarkMode disabled images.
ToolStrip.cs Modified background painting to use renderer overrides when available.
StatusStrip.cs Updated comment spelling for clarity.
PublicAPI.Unshipped.txt Updated API documentation entries for FormCornerPreference and related events.
Comments suppressed due to low confidence (6)

src/System.Windows.Forms/System/Windows/Forms/FormCornerPreference.cs:11

  • Verify that the removal of the experimental DarkMode diagnostic attribute is intended and that any related tooling or documentation has been updated accordingly.
-[Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)]

src/System.Windows.Forms/System/Windows/Forms/Form.cs:2185

  • Ensure that removing the experimental attribute from the Form properties aligns with production readiness for DarkMode, and update any related diagnostic handling as needed.
-    [Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)]

src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/StatusStrip.cs:121

  • Correct the spelling of 'accomodate' to 'accommodate' in the comment for clarity.
// we do some custom stuff with padding to accomodate size grip.

src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemRenderer.cs:18

  • Confirm that prioritizing high contrast mode over dark mode in the RendererOverride is intended and that the newly introduced dark mode renderer meets accessibility guidelines.
+    private ToolStripRenderer? _toolStripDarkModeRenderer;

src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripRenderer.cs:855

  • Review the logic change for rendering item check images to ensure that returning early when the image rectangle is empty or the image is null does not inadvertently skip intended rendering in edge cases.
+        if (imageRect == Rectangle.Empty || image is null)

src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStrip.cs:3651

  • [nitpick] Ensure that using the renderer override to draw the ToolStrip background and returning early does not conflict with any custom rendering requirements that downstream consumers might have.
+            if (Renderer.RendererOverride is ToolStripRenderer renderer)

@KlausLoeffelmann KlausLoeffelmann force-pushed the DarkMode_a_FixStatusStripRegression branch 2 times, most recently from 3a1b788 to 031811f Compare May 8, 2025 07:02
@KlausLoeffelmann KlausLoeffelmann added this to the 10 Preview5 milestone May 8, 2025
@KlausLoeffelmann KlausLoeffelmann marked this pull request as ready for review May 9, 2025 15:30
@KlausLoeffelmann KlausLoeffelmann requested a review from a team as a code owner May 9, 2025 15:30
@dotnet-policy-service dotnet-policy-service bot removed the draft draft PR label May 9, 2025
@KlausLoeffelmann KlausLoeffelmann force-pushed the DarkMode_a_FixStatusStripRegression branch from e78aef4 to f65907a Compare May 9, 2025 19:55
Copy link
Member

@JeremyKuhne JeremyKuhne left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added comments. Please describe what changes you've made in your PR, not just what it will fix.

@@ -546,24 +546,12 @@ public static bool VisualStylesEnabled

internal static ToolStripRenderer CreateRenderer(ToolStripManagerRenderMode renderMode)
{
switch (renderMode)
return renderMode switch
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: you can turn this into => now.

@@ -6,47 +6,83 @@

namespace System.Windows.Forms;

/// <summary>
/// Provides system rendering for ToolStrip controls with support for dark mode.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please indent all XML comments by a space.

}
else if (toolStrip.IsDropDown)
{
FillBackground(g, bounds, (!ToolStripManager.VisualStylesEnabled) ?
e.BackColor : SystemColors.Menu);
FillBackground(g, bounds, (!ToolStripManager.VisualStylesEnabled)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reversing the ! would be clearer and the parenthesis isn't needed.

new(8, 0, 2, 2),
private static readonly Rectangle[] s_baseSizeGripRectangles =
[
new(12, 0, 2, 2),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did these change?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See added screenshots for explanaition.

@dotnet-policy-service dotnet-policy-service bot added waiting-author-feedback The team requires more information from the author and removed waiting-author-feedback The team requires more information from the author labels May 9, 2025
@KlausLoeffelmann KlausLoeffelmann force-pushed the DarkMode_a_FixStatusStripRegression branch from f65907a to 82e62d9 Compare May 9, 2025 22:12
@KlausLoeffelmann
Copy link
Member Author

Added comments.

@merriemcgaw, @Tanya-Solyanik, @Shyam-Gupta:

First of all, @jeremy, thanks for the review!

While reading @JeremyKuhne review comments, I got the idea, that many of his comments can be in fact generelized Copilot instructions, which we could just "run" before we submit a PR. I think that would drastically reduce the workload, both for review and for keeping all those little optinization details in mind, and we could actually concentrate more on the domain-specific work.

I created #13441, and my suggestion would be, that if someone sees an optimization potential, which would also work in a generic way, that they not only comment, but also add those to a list of general CoPilot instructions. This list can grow over time, and will exponentially save time, with this kind of work.

What do y'all think?

I have experimented over the weekend with a few approaches, and I think this looks promesing.
I have in this case created a document based on Jeremie's comments and will spend one or two hours tomorrow to see, if I can address half of the issues "in one go" so to say.

@RussKie

This comment was marked as off-topic.

Copy link

@memoarfaa memoarfaa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recording.2025-05-13.030723.mp4

@KlausLoeffelmann
@JeremyKuhne
Why not just to be

    private static void RenderStatusStripBackground(ToolStripRenderEventArgs e)
    {
        if (Application.RenderWithVisualStyles)
        {
            VisualStyleRenderer vsRenderer = VisualStyleRenderer!;
#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.
            VisualStyleElement visualStyleElement = Application.IsDarkModeEnabled
                ? VisualStyleElement.CreateElement("DarkMode::ExplorerStatusBar", 0, 0)
                : VisualStyleElement.Status.Bar.Normal;
#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.
            ;

            vsRenderer.SetParameters(visualStyleElement);
            vsRenderer.DrawBackground(e.Graphics, new Rectangle(0, 0, e.ToolStrip.Width - 1, e.ToolStrip.Height - 1));
        }
        else
        {
            if (!SystemInformation.InLockedTerminalSession())
            {
                e.Graphics.Clear(e.BackColor);
            }
        }
    }

Can you use Dark Mode Render instead of accessibility drawing please?

@dotnet-policy-service dotnet-policy-service bot added the waiting-author-feedback The team requires more information from the author label May 13, 2025
@KlausLoeffelmann
Copy link
Member Author

KlausLoeffelmann commented May 13, 2025

Images aren't very a11y-friendly, the text could have been copied and pasted in.

Hey @RussKie,

I cannot see the screenshot you assumingly posted - can you fix that?

Also, the plan is:
we want to get the Renderer quickly out for Preview 5, so we can all test and refine.

@dotnet-policy-service dotnet-policy-service bot added untriaged The team needs to look at this issue in the next triage and removed waiting-author-feedback The team requires more information from the author labels May 13, 2025
@KlausLoeffelmann KlausLoeffelmann force-pushed the DarkMode_a_FixStatusStripRegression branch 3 times, most recently from 82a94d1 to 5619f0b Compare June 9, 2025 00:09
@KlausLoeffelmann KlausLoeffelmann changed the base branch from main to klaus/preview6/0 June 10, 2025 04:35
@KlausLoeffelmann KlausLoeffelmann force-pushed the DarkMode_a_FixStatusStripRegression branch 4 times, most recently from 921d7fe to a1bb6dd Compare June 13, 2025 05:42
@KlausLoeffelmann KlausLoeffelmann force-pushed the DarkMode_a_FixStatusStripRegression branch from 73a6b0f to 74272fd Compare June 14, 2025 16:59
@KlausLoeffelmann KlausLoeffelmann changed the base branch from klaus/preview6/0 to main June 15, 2025 04:33
@KlausLoeffelmann KlausLoeffelmann force-pushed the DarkMode_a_FixStatusStripRegression branch from 74272fd to 933b263 Compare June 15, 2025 04:38
@KlausLoeffelmann KlausLoeffelmann added the waiting-review This item is waiting on review by one or more members of team label Jun 16, 2025
@LeafShi1 LeafShi1 modified the milestones: 10 Preview6, 10 Preview7 Jun 16, 2025
Copy link
Member

@JeremyKuhne JeremyKuhne left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is more feedback to address (maybe an hour or so of work), but nothing that blocks getting this in for the next preview. Approving with the understanding that we'll address with a follow up issue to track.

Color arrowColor)
{
ArgumentNullException.ThrowIfNull(e);

Graphics g = e.Graphics;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: I've created an issue to re-do the GraphicsInternal stuff: #13604

// 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I know this was this way, but as you did change the code here, this would be better with pattern matching. Also, why are we dropping the comment about design time?

greyRectangles[i] = baseRect;
}
// Apply scaling
g.ScaleTransform(heightScale, heightScale);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a general comment here. In the future, if we want to fine tune for performance, we should consider scaling the rectangles and points directly. This is particularly true when it comes to us trying to directly GDI render on the Graphics object as we'll be able to assert that we don't need to extract state. (See GraphicsInternal discussions)

// Default offset
int offset = 2;

if (Environment.OSVersion.Version >= new Version(10, 0, 22000)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This warrants a comment about what version this is and what changed exactly. Also, what will the impact be to user perceived rendering be if the OS changes again?

{
return (control.RawBackColor == Color.Empty && control.BackgroundImage is null);
}
// Only paint background effects if no BackColor has been set
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Part of the reason for this is that comments on the outside become the IntelliSense docs for the method. ShouldPaintBackground is pretty self-explanatory, but if there was a <summary> comment it shouldn't explain the implementation details. Not a huge deal, but you did change the code here for style, so it was worth pointing out.

/// and overrides necessary methods to provide dark-themed rendering.
/// </para>
/// </remarks>
internal class ToolStripSystemDarkModeRenderer : ToolStripRenderer
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it would not be a binary breaking change. It does, however, have an impact on performance to leave it unsealed. The JIT can better de-virtualize when classes are sealed.

private static Color GetDarkModeColor(Color color) =>
color switch
{
Color c when c == SystemColors.Control => Color.FromArgb(45, 45, 45),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be much more efficient to switch on color.ToKnownColor().

Graphics g = e.Graphics;
Rectangle bounds = e.AffectedBounds;

if (!ShouldPaintBackground(toolStrip))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've created an issue on GraphicsInternal optimization and mentioned it elsewhere.

@KlausLoeffelmann KlausLoeffelmann merged commit 12c763e into dotnet:main Jun 16, 2025
8 checks passed
@dotnet-policy-service dotnet-policy-service bot removed the waiting-review This item is waiting on review by one or more members of team label Jun 16, 2025
@KlausLoeffelmann
Copy link
Member Author

@JeremyKuhne, @merriemcgaw:
Tracking the important parts here at one central point, so they do not get lost.

#13605

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment