Skip to content

Add a per-state stroke model for Net11 editable-control borders - #14919

Open
ricardobossan wants to merge 16 commits into
dotnet:mainfrom
ricardobossan:Issue_14906_Add_Per_State_Stroke_Net11
Open

Add a per-state stroke model for Net11 editable-control borders#14919
ricardobossan wants to merge 16 commits into
dotnet:mainfrom
ricardobossan:Issue_14906_Add_Per_State_Stroke_Net11

Conversation

@ricardobossan

@ricardobossan ricardobossan commented Aug 22, 2026

Copy link
Copy Markdown
Member

Fixes #14906

Proposed changes

For VisualStylesMode.Net11, replace the simple ForeColor-based border and basic focus ring used by editable controls (TextBox, MaskedTextBox, and RichTextBox through TextBoxBase) with a state-resolved stroke design.

  • Introduce ModernFieldStrokeState with Rest, Hover, Focused, Disabled, and ReadOnly values; the ModernFieldStroke record holds the side/top and bottom colors, surface, DIP widths, and focus-indicator setting. ModernFieldStrokeResolver.GetStroke is the single resolution point, applying Disabled > Focused > ReadOnly > Hover > Rest. Rendering consumes its resolved stroke rather than selecting colors or states itself.
  • Add linear-light compositing to ModernControlColorMath because the semi-transparent WinUI control-stroke overlays require it. PopupButtonColorMath.Blend cannot serve this purpose because it blends encoded sRGB values. Stroke colors use the effective background and do not depend on ForeColor.
  • Have TextBoxBase.OnNcPaint render every state over the control's own BackColor, eliminating state-specific interior fills so a padded custom background remains seamless. State changes are communicated by border color or weight and, when focused, the accent. For rounded borders, CreateVisualStylesBottomEdgePath creates the tapered bottom edge so it joins the corner rounds without extending up their arcs. The common bottom-edge shape is used for every rounded state, with only its color and thickness varying. Focus makes that edge accent-colored and heavier at about 3 DIP while keeping the other sides at the subtle resting color, rather than drawing a heavy surround. FixedSingle and None retain a straight focus underline. Pointer tracking in OnMouseEnter and OnMouseLeave repaints the non-client frame for hover, which is distinguished only through border color and contrast.
  • Keep ReadOnly in the resolver's precedence model, but give it the Rest appearance, including the same surface and strokes. The visually differentiated appearances are Rest, Hover, Focused, and Disabled.
  • Since High Contrast disables Net11 VisualStyles automatically, this drawing route is never selected in HC. The pre-existing system-color legacy border handles that case, leaving no High Contrast logic in the new model.
  • In light mode, the resting lower edge delivers roughly 3:1 WCAG 1.4.11 non-text contrast on white, unlike the approximately 1.74:1 WinUI measurement that Accessibility Insights rejected. Retaining that 3:1 minimum also keeps it less prominent than the accent focus edge. In dark mode, the side/top overlay alpha is reduced from the WinUI approximately 0x12 to near-invisible 0x03, matching the subtle light and classic treatment. Both themes use the visible bottom edge to define the field.
  • Limit this behavior to Net11; the < Net11 rendering path remains as it was.

The work covers TextBoxBase. ComboBox, which uses a WM_PAINT client-area adapter, and UpDown remain follow-up items under #14906. Leaf also identified the Windows 11 flat scrollbar behavior where its up/down arrows appear only on hover under the Net11 appearance. That scrollbar change is deliberately deferred to a separate follow-up so these border-rendering changes can merge first. The design direction and state values are from the #14906 discussion.

Customer Impact

When VisualStylesMode.Net11 is active, every TextBoxBase control receives a modern border that is unaffected by ForeColor, addressing #14847 across the control family. Rest, hover, focus, and disabled have their respective treatments, while read-only intentionally looks like rest.

Regression?

No regressions are expected. This is a Net11-gated change, leaving Classic and the legacy fallback, including the High Contrast VisualStyles opt-out path, unchanged.

Risk

The risk level is low to medium. It affects the newly introduced Net11 editable-control border surface only, is contained behind that mode gate, and has resolver-level unit coverage.

Test methodology

  • Unit coverage for the resolver, including its precedence rules, state colors, shared surface behavior, and focus-indicator setting
  • Hands-on checks of all five states for each of the three control types in light and dark themes, across accent-color updates and DPI scaling, including confirmation that High Contrast chooses the legacy border through the VisualStyles opt-out
  • QA test-team validation (Testing result of Pull Request #14919: Add a per-state stroke model for Net11 editable-control borders #14997), whose feedback informed the resting-bottom and focus-corner weights and the hover border contrast

Screenshots

  1. Light mode: interact with each control in every state:
14906_20260904_light
  1. Dark mode: interact with each control in every state:
14906_20260904_dark

The Classic control shown in both recordings demonstrates that the legacy < Net11 branch is unaffected. Manual DPI testing confirmed proportional scaling of the 2 DIP and 4 DIP strokes; the recordings omit it because changing the host display scale halted the screen recorder.

@ricardobossan

This comment was marked as outdated.

Ricardo Bossan (BEYONDSOFT CONSULTING INC) (from Dev Box) added 10 commits August 25, 2026 22:53
OnNcPaint now builds a ModernFieldStrokeContext and draws the resolved
ModernFieldStroke (per-state side/top color, bottom color, surface, and
DIP thicknesses) instead of deriving the border from ForeColor, so text
color and border are finally independent.

x Focus is expressed by the accent bottom edge alone; the former rounded
focus ring is removed. The bottom-edge band is clamped so the accent
stays on the bottom and its corners rather than wrapping up the sides.
Track pointer-over via OnMouseEnter/OnMouseLeave under the Net11 guard,
repainting the non-client frame through InvalidateVisualStylesFrame (the
same path focus uses), and feed the hovered flag into the stroke context
so the resolver applies the Hover treatment.
@ricardobossan
ricardobossan force-pushed the Issue_14906_Add_Per_State_Stroke_Net11 branch from 7fb6b99 to 9fab207 Compare August 26, 2026 02:49
Comment thread src/System.Windows.Forms/System/Windows/Forms/Controls/TextBox/TextBoxBase.cs Outdated
Comment thread src/System.Windows.Forms/System/Windows/Forms/Controls/TextBox/TextBoxBase.cs Outdated
@LeafShi1
LeafShi1 self-requested a review August 27, 2026 01:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR updates the Net11 (modern visual styles) non-client rendering for editable text controls (via TextBoxBase) by introducing a per-state stroke model and routing border/focus rendering through a single resolver, including new linear-light compositing for WinUI-style overlays.

Changes:

  • Introduces a ModernFieldStroke* model (State, Context, Stroke) plus ModernFieldStrokeResolver.GetStroke() with explicit precedence for Disabled/Focused/ReadOnly/Hover/Rest.
  • Adds linear-light overlay compositing utilities to ModernControlColorMath for modern stroke/surface colors derived from effective background (not ForeColor).
  • Updates TextBoxBase NC painting to use the resolved stroke (including hover tracking) and removes the old focus-indicator animation path; adds resolver unit tests.
File summaries
File Description
src/System.Windows.Forms/System/Windows/Forms/Controls/TextBox/TextBoxBase.cs Replaces ForeColor-driven/animated focus rendering with resolved per-state stroke + hover tracking; updates NC paint focus/bottom edge logic.
src/System.Windows.Forms/System/Windows/Forms/Rendering/ModernControlColorMath.cs Adds linear-light compositing helpers and new field-stroke/surface color APIs (default/hover/strong/read-only + disabled strong border).
src/System.Windows.Forms/System/Windows/Forms/Rendering/ModernFieldStroke.cs Adds resolved stroke record used by paint paths.
src/System.Windows.Forms/System/Windows/Forms/Rendering/ModernFieldStrokeContext.cs Adds resolver input context record (enabled/read-only/focused/hovered/dark/accent/etc.).
src/System.Windows.Forms/System/Windows/Forms/Rendering/ModernFieldStrokeResolver.cs Adds the single chokepoint resolver to compute strokes from context + precedence rules.
src/System.Windows.Forms/System/Windows/Forms/Rendering/ModernFieldStrokeState.cs Adds internal enum describing stroke interaction states.
src/test/unit/System.Windows.Forms/System/Windows/Forms/Rendering/ModernFieldStrokeResolverTests.cs Adds unit tests covering precedence, thickness DIPs, opacity, and color-math invariants.
src/test/unit/System.Windows.Forms/TextBoxBaseTests.cs Removes tests tied to the old AnimatedFocusIndicatorRenderer behavior that no longer applies.
Review details

Suppressed comments (1)

src/System.Windows.Forms/System/Windows/Forms/Controls/TextBox/TextBoxBase.cs:2789

  • PaintRoundedBorderRegionMitigation still uses borderThickness from focus metrics, but the border now being drawn is sideThickness. If these differ, the mitigation can trace too wide/narrow and either leave artifacts or overpaint. Pass sideThickness to keep mitigation aligned with the actual stroke width.
        // Bottom (elevation and focus) edge. The rounded focus indicator grows out of the bottom border
        // as a tapered fill, leaving the left, top, and right of the rounded frame untouched, so the
        // corners do not become heavy (#14997). Non-focus states keep a resting bottom edge clipped to a
        // band; flat styles draw a straight focus underline (see #14906).
        if (BorderStyle == BorderStyle.Fixed3D && canRenderRoundedChrome)
  • Files reviewed: 8/8 changed files
  • Comments generated: 4
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@ricardobossan
ricardobossan marked this pull request as draft September 4, 2026 00:30
@KlausLoeffelmann

Copy link
Copy Markdown
Member

@ricardobossan, do you feel confident to take this out of draft and get it merged, after Olina tested this?

Or is there still work to do form your perspective?

@dotnet-policy-service dotnet-policy-service Bot added the draft draft PR label Sep 4, 2026
@ricardobossan
ricardobossan marked this pull request as ready for review September 4, 2026 18:08
@codecov

codecov Bot commented Sep 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 37.24166%. Comparing base (1dfc02d) to head (d8f0650).
⚠️ Report is 200 commits behind head on main.

❗ There is a different number of reports uploaded between BASE (1dfc02d) and HEAD (d8f0650). Click for more details.

HEAD has 3 uploads less than BASE
Flag BASE (1dfc02d) HEAD (d8f0650)
Debug 3 2
production 2 1
integration 1 0
Additional details and impacted files
@@                 Coverage Diff                  @@
##                main      #14919          +/-   ##
====================================================
- Coverage   77.31435%   37.24166%   -40.07269%     
====================================================
  Files           3265         246        -3019     
  Lines         644447        9774      -634673     
  Branches       47631        1029       -46602     
====================================================
- Hits          498250        3640      -494610     
+ Misses        142509        5970      -136539     
+ Partials        3688         164        -3524     
Flag Coverage Δ
Debug 37.24166% <ø> (-40.07270%) ⬇️
integration ?
production 39.36526% <ø> (-12.91627%) ⬇️
test 20.64923% <ø> (-76.76994%) ⬇️
unit 39.36526% <ø> (-10.26895%) ⬇️

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.

@ricardobossan

Copy link
Copy Markdown
Member Author

@ricardobossan, do you feel confident to take this out of draft and get it merged, after Olina tested this?

Or is there still work to do form your perspective?

@KlausLoeffelmann Hi Klaus, the border drawing itself is finished and solid. A few minor cosmetic refinements were the only reason the PR sat in draft, and I have deliberately parked those for a later follow-up rather than let them delay finished work, so the PR is now out of draft and ready for review and merge. Two rounds of QA feedback have already been addressed, tracked in #14997. There is also a final re-test pass on the latest commit running with QA for completeness, which should not hold anything up. Thanks for checking in.

@KlausLoeffelmann KlausLoeffelmann removed the draft draft PR label Sep 4, 2026
@KlausLoeffelmann KlausLoeffelmann added this to the 11.0-rc2 milestone Sep 4, 2026

private static float SrgbToLinear(byte channel)
{
float value = channel / 255f;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

As a follow-up suggestion - non-blocking for now:
Let's resolve the magic numbers a bit over-all, except here:
Here, I would rather very briefly explain the approach (have Copilot write a comment), so that the world which comes after us still know, what this magic does.

@KlausLoeffelmann KlausLoeffelmann left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Wonderful. Can't wait to demo that on stage!

@KlausLoeffelmann

Copy link
Copy Markdown
Member

Please merge as soon as you get a second, so we can test all of the latest changes together.

@Olina-Zhang FYI!

@KlausLoeffelmann KlausLoeffelmann left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Hey Ricardo,

I want to take a closer look at something I just saw, so I am revoking my previous review for a moment.

I reapprove, when I tested.

Sorry for the delay!

@dotnet-policy-service dotnet-policy-service Bot added the waiting-author-feedback The team requires more information from the author label Sep 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

waiting-author-feedback The team requires more information from the author

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Tracking] Modern field stroke state model for Net11 editable controls (TextBoxBase)

4 participants