Add a per-state stroke model for Net11 editable-control borders - #14919
Add a per-state stroke model for Net11 editable-control borders#14919ricardobossan wants to merge 16 commits into
Conversation
This comment was marked as outdated.
This comment was marked as outdated.
641ef68 to
7fb6b99
Compare
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.
7fb6b99 to
9fab207
Compare
There was a problem hiding this comment.
🟡 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) plusModernFieldStrokeResolver.GetStroke()with explicit precedence for Disabled/Focused/ReadOnly/Hover/Rest. - Adds linear-light overlay compositing utilities to
ModernControlColorMathfor modern stroke/surface colors derived from effective background (notForeColor). - Updates
TextBoxBaseNC 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, 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? |
Codecov Report✅ All modified and coverable lines are covered by tests.
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
Flags with carried forward coverage won't be shown. Click here to find out more. 🚀 New features to boost your workflow:
|
@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. |
|
|
||
| private static float SrgbToLinear(byte channel) | ||
| { | ||
| float value = channel / 255f; |
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
Wonderful. Can't wait to demo that on stage!
|
Please merge as soon as you get a second, so we can test all of the latest changes together. @Olina-Zhang FYI! |
KlausLoeffelmann
left a comment
There was a problem hiding this comment.
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!
Fixes #14906
Proposed changes
For
VisualStylesMode.Net11, replace the simpleForeColor-based border and basic focus ring used by editable controls (TextBox,MaskedTextBox, andRichTextBoxthroughTextBoxBase) with a state-resolved stroke design.ModernFieldStrokeStatewith Rest, Hover, Focused, Disabled, and ReadOnly values; theModernFieldStrokerecord holds the side/top and bottom colors, surface, DIP widths, and focus-indicator setting.ModernFieldStrokeResolver.GetStrokeis the single resolution point, applying Disabled > Focused > ReadOnly > Hover > Rest. Rendering consumes its resolved stroke rather than selecting colors or states itself.ModernControlColorMathbecause the semi-transparent WinUI control-stroke overlays require it.PopupButtonColorMath.Blendcannot serve this purpose because it blends encoded sRGB values. Stroke colors use the effective background and do not depend onForeColor.TextBoxBase.OnNcPaintrender every state over the control's ownBackColor, 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,CreateVisualStylesBottomEdgePathcreates 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.FixedSingleandNoneretain a straight focus underline. Pointer tracking inOnMouseEnterandOnMouseLeaverepaints the non-client frame for hover, which is distinguished only through border color and contrast.0x12to near-invisible0x03, matching the subtle light and classic treatment. Both themes use the visible bottom edge to define the field.< Net11rendering path remains as it was.The work covers
TextBoxBase.ComboBox, which uses aWM_PAINTclient-area adapter, andUpDownremain 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.Net11is active, everyTextBoxBasecontrol receives a modern border that is unaffected byForeColor, 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
Screenshots
The Classic control shown in both recordings demonstrates that the legacy
< Net11branch 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.