Add a per-state stroke model for Net11 editable-control borders - #14919
Open
ricardobossan wants to merge 13 commits into
Open
Add a per-state stroke model for Net11 editable-control borders#14919ricardobossan wants to merge 13 commits into
ricardobossan wants to merge 13 commits into
Conversation
This comment was marked as outdated.
This comment was marked as outdated.
ricardobossan
marked this pull request as ready for review
August 24, 2026 20:12
ricardobossan
force-pushed
the
Issue_14906_Add_Per_State_Stroke_Net11
branch
from
August 24, 2026 22:52
641ef68 to
7fb6b99
Compare
This was referenced Aug 24, 2026
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
force-pushed
the
Issue_14906_Add_Per_State_Stroke_Net11
branch
from
August 26, 2026 02:49
7fb6b99 to
9fab207
Compare
LeafShi1
reviewed
Aug 26, 2026
LeafShi1
reviewed
Aug 26, 2026
LeafShi1
reviewed
Aug 26, 2026
LeafShi1
self-requested a review
August 27, 2026 01:54
Contributor
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.
Comment on lines
1696
to
1700
| protected override void OnHandleDestroyed(EventArgs e) | ||
| { | ||
| _focusIndicatorRenderer?.Dispose(); | ||
| _focusIndicatorRenderer = null; | ||
| _textBoxFlags[s_modified] = Modified; | ||
| _textBoxFlags[s_setSelectionOnHandleCreated] = true; | ||
| // Update text selection cached values to be restored when recreating the handle. |
Comment on lines
+19
to
+22
| // WinUI control-stroke overlay alphas over the black (light mode) / white (dark mode) pole, | ||
| // verified against Common_themeresources_any.xaml, except light-mode Strong, which is raised | ||
| // above WinUI so the visible bottom edge meets WCAG 1.4.11 (#14906). Composited in linear light. | ||
| private const int StrokeDefaultAlphaLight = 0x0F; // ControlStrokeColorDefault |
Comment on lines
+2678
to
+2679
| int sideThickness = Math.Max(1, (int)MathF.Round(stroke.SideTopThicknessDip * DeviceDpi / 96f)); | ||
| int bottomThickness = Math.Max(1, (int)MathF.Round(stroke.BottomThicknessDip * DeviceDpi / 96f)); |
Comment on lines
2664
to
+2668
| int borderThickness = Math.Max(focusBorderMetrics.Width, focusBorderMetrics.Height); | ||
| int focusBandHeight = GetVisualStylesFocusBandHeight(); | ||
|
|
||
| Color clientBackColor = BackColor; | ||
| ModernFieldStrokeContext strokeContext = new( | ||
| BackColor: BackColor, |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #14906
Proposed changes
Replace the flat, ForeColor-driven border and the primitive focus ring on VisualStylesMode.Net11 editable controls (TextBox, MaskedTextBox, RichTextBox, all via TextBoxBase) with a proper per-state stroke model.
ModernFieldStrokeState(Rest, Hover, Focused, Disabled, ReadOnly), aModernFieldStrokerecord (side/top color, bottom color, surface, and DIP thicknesses), and a singleModernFieldStrokeResolver.GetStrokechokepoint with precedence Disabled > Focused > ReadOnly > Hover > Rest. Paint code receives only a completed stroke; all state and color selection stays behind the resolver.ModernControlColorMath. WinUI control-stroke overlays are semi-transparent and must be composited in linear light. The existingPopupButtonColorMath.Blendworks in encoded sRGB and cannot, so this adds the correct compositing. Colors derive from the effective background and are independent ofForeColor.TextBoxBase.OnNcPaintnow draws from the resolved stroke: a light side/top, a stronger darker bottom elevation edge, and a per-state surface. Focus is expressed by an accent bottom edge at 4 DIP (bottom only), which replaces the former rounded focus ring. Hover tracks pointer-over viaOnMouseEnter/OnMouseLeaveand repaints the non-client frame the same way focus does.Scope is TextBoxBase. ComboBox (a WM_PAINT client-area adapter) and UpDown are out of scope and tracked as follow-ups in #14906. Design direction and the per-state values come from the discussion in #14906.
Customer Impact
Under VisualStylesMode.Net11, editable-control borders no longer inherit
ForeColor(issue #14847, fixed here generally for all TextBoxBase controls), and they gain distinct rest, hover, focus, disabled, and read-only treatments consistent with the modern visual style.Regression?
No. The change is gated on Net11; Classic, Disabled, and High Contrast (which opts out of VisualStyles) paint exactly as before.
Risk
Low to medium. It changes the appearance of Net11 editable-control borders (a new feature surface), behind the Net11 gate, and is covered by resolver unit tests.
Test methodology
Known follow-ups (not in this PR)
Screenshots
The Classic-mode control visible in both confirms the legacy (< Net11) path is unchanged. DPI scaling was verified manually (the 2 DIP and 4 DIP strokes scale up proportionally); it is absent from the recordings only because changing the host display scale stopped the screen recorder.