Visitar URL original
feat!: Reparent PasswordBox from TextBox to Control by Xiaoy312 · Pull Request #24038 · unoplatform/uno · GitHub
Skip to content

feat!: Reparent PasswordBox from TextBox to Control - #24038

Merged
Xiaoy312 merged 15 commits into
feature/breakingchangesfrom
dev/xygu/20260807/2138-passwordbox-reparent
Aug 12, 2026
Merged

Xiaoy312 merged 15 commits into
feature/breakingchangesfrom
dev/xygu/20260807/2138-passwordbox-reparent

Conversation

@Xiaoy312

@Xiaoy312 Xiaoy312 commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

GitHub Issue: internal — uno-private#2138 (the template allows "GitHub issue or internal"; there is no public issue for this. Note unoplatform/uno#2138 is an unrelated closed WASM issue, so closes #2138 is deliberately not used here.)

PR Type:

💬 Other... — breaking change: public API surface reduction on PasswordBox. (🔄 Refactoring does not fit; there are API changes.)

What changed? 🚀

Current behaviour. Uno's PasswordBox derives from TextBox. WinUI's derives from Control (runtimeclass PasswordBox : Control). Two consequences:

  1. The password is reachable as text. PasswordBox mirrored the password into the inherited Text dependency property, so passwordBox.Text returned cleartext and TextChanged fired per keystroke. WinUI has no such path.
  2. Leaked API. SelectedText, SelectionStart/Length, IsReadOnly, AcceptsReturn, TextWrapping, CanUndo/CanRedo, CopySelectionToClipboard, ProofingMenuFlyout … all appeared on PasswordBox, and is TextBox wrongly matched it. Uno carried ~16 new/override shadows to compensate — evidence the parent was wrong.

This PR makes both controls derive directly from Control and share the editing implementation by composition:

Control
 ├── TextBox      : Control, ITextBoxHost  ─┐  owns a TextBoxCore
 └── PasswordBox  : Control, ITextBoxHost  ─┤
                                            │
                    internal sealed TextBoxCore
                    selection · caret · undo history · IME · pointers · grippers
                    clipboard · selection flyout · proofing menu · TextBoxView

Why composition rather than a shared base class

WinUI's implementation does use inheritance (CPasswordBox : CTextBoxBase), but its projected surface does not. C++/WinRT can keep those separate; C# cannot, so the two goals conflict. Composition is the one that preserves what app developers observe:

Intermediate base class Composition (this PR)
PasswordBox.BaseType TextBoxBase Control ✅
TextBox.BaseType changes unchanged ✅
New public API one public type none — TextBoxCore and ITextBoxHost are both internal ✅

An internal shared base is not expressible: public class TextBox : InternalBase is CS0060 ("inconsistent accessibility"), verified empirically. So the issue's suggested "extract a shared internal text-input base" is not implementable as written; composition is how the internal part of that intent is honoured. The cost, stated plainly: every framework touchpoint the implementation needs (GetTemplateChild, VisualStateManager.GoToState, SetValue, dispatcher, focus) routes through ITextBoxHost.Owner, and each protected override input handler becomes a thin forward on both controls.

Notable behaviour corrections found on the way

  • PasswordBox used to spell-check its own masked text. It inherited IsSpellCheckEnabled (default true) and TextBoxView pushed that onto the display block. WinUI's PasswordBox has no such property and never spell-checks. This changes rendering (the squiggly underline is gone).
  • MaxLength now coerces Password directly. It previously reached a PasswordBox only through the Text mirror, and the two were desynced — Password accepted an over-long value while the mirrored Text rejected it.
  • TextBlock.OwningTextBox was TextBox-typed, so a reparented PasswordBox left it null: the display block reset its own selection on every text change, dropped the caret-thickness width adjustment, and stopped routing ContextRequested. Retyped to the engine.
  • Three Skia runtime projects (Android, WASM/Browser, iOS) did not compile partway through this work, and a four-build gate did not notice. The gate in this PR is ten builds. The native text-input path was TextBox-typed end to end; TextBoxView.TextBox is now deleted rather than retyped so the type system enforces it.
  • PasswordBox.SelectionFlyout's getter was silently emitted as virtual. Because the public property implicitly implemented ITextBoxHost.SelectionFlyout, the compiler marked get_SelectionFlyout virtual final newslot — a vtable change on an existing public member, which Uno.PackageDiff reports as a removed method. TextBox already forwards the interface member explicitly; PasswordBox now does too, so the public getter stays non-virtual and the package diff is clean with no PackageDiffIgnore.xml entry.

Out of scope, deliberately

PasswordChanging remains a [Uno.NotImplemented] stub, as TextReadingOrder and PreventKeyboardDisplayOnProgrammaticFocus already are.

PR Checklist ✅

Breaking change — impact and migration path

This is an intentional breaking change targeting feature/breakingchanges.

Impact. Any code treating a PasswordBox as a TextBox stops compiling or stops matching:

Was Now
passwordBox.Text passwordBox.Password
passwordBox.TextChanged passwordBox.PasswordChanged
passwordBox.BeforeTextChanging (cancellable) no replacement — see below
passwordBox.TextChanging (no equivalent; PasswordChanging is not implemented)
passwordBox.SelectedText / SelectionStart / SelectionLength removed (never in WinUI's PasswordBox)
passwordBox.IsReadOnly / AcceptsReturn / TextWrapping / TextAlignment removed
passwordBox.CanUndo / CanRedo / Undo() / Redo() removed
passwordBox.CopySelectionToClipboard() / CutSelectionToClipboard() removed
passwordBox.ProofingMenuFlyout removed
element is TextBox matching a PasswordBox now false — match PasswordBox explicitly

BeforeTextChanging loses its Cancel with no replacement. PasswordBoxPasswordChangingEventArgs has no Cancel member, so PasswordChanging would not substitute even once implemented. Apps that vetoed password input should use MaxLength for length limits and PasswordChanged for post-hoc validation.

All of these removals restore WinUI parity — none of the removed members exists on WinUI's PasswordBox — so code written against WinUI is unaffected.


Rebased twice onto a moving base — what reviewers should know

This branch forked at 47f9d020ac and has since been ported across two structural changes to feature/breakingchanges.

First port — #23999 (dev/mazi/referencedrop). Not additive drift: it folded the UI-layer Reference flavor into Skia (Uno.UI.Skia.csproj + Uno.UI.Reference.csproj → one Uno.UI.csproj) and merged every .skia.cs partial into its base file, so Uno.UI now has 0 of the 132 .skia.cs and 0 of the 26 .reference.cs files it had at the fork point. TextBox.cs went 1565 → 3498 lines. Item 5 of this change had git mv-ed TextBox.skia.cs out; #23999 merged it in — hence rename/rename, rename/delete and modify/delete across 14 files.

What made that port safe to do mechanically: only 3 base commits touched any of the 60 source files here, and all three were the restructure — zero semantic changes. Confirmed per-file with a statement-multiset audit before resolving anything: base's TextBox.cs vs the fork's TextBox.cs + TextBox.skia.cs differed by 1 statement (the merged class declaration combining both partials' interface lists) plus 21 deduplicated using directives; PasswordBox.cs by 0 plus 6.

Second port — the consolidated SamplesApp head + Mac Catalyst removal (80 commits). Far smaller blast radius: exactly one base commit touches any file this branch touches, and it is a docs commit. Measured before rebasing — the replay set (69 files across the branch's commits) intersected with base's 94 deletions and with its renames: both empty. The rebase replayed 23/23 with zero stops, and doc/articles/migrating-to-uno-7.md 3-way merged automatically; that file now differs from base by only this PR's 24-line bullet, with base's Mac Catalyst edits intact.

Two consequences worth flagging:

  • Item 5's "provably a move" property is gone and is not recoverable. Git rename detection needs the source deleted, and TextBox.skia.cs no longer exists to be renamed from. The extraction now reads as new files beside a surviving TextBox.cs. The three audits that property stood in for still hold and are stronger: statement-multiset base-vs-now, the override name-set diff (TextBox 35 / PasswordBox 32, gap = OnIsTabStopChanged/OnTapped/UpdateThemeBindings, all inert), and the partial void orphan check.
  • The engine file is TextBoxCore.Input.cs, not folded into TextBoxCore.cs. Folding would match what refactor(build)!: Fold UI-layer Reference flavor into Skia #23999 did to TextBox.skia.cs, but TextBoxCore.cs is #nullable enable while the engine is nullable-oblivious — merging them puts 1881 unannotated lines under an annotated context and produces 40 errors unrelated to this change. refactor(build)!: Fold UI-layer Reference flavor into Skia #23999 never hit this because both its files were oblivious. .reference.cs files are deleted outright, as refactor(build)!: Fold UI-layer Reference flavor into Skia #23999 did with TextBox.reference.cs.

Visual verification

Static rendering is covered, and was checked rather than assumed. Every PasswordBox_* sample is in the snapshot set (none sets IgnoreInSnapshotTests), and both Screenshot Comparison and Desktop Skia Windows Snapshot Tests are green on this commit — so there is no unintended pixel change against baseline. On top of that, all samples were rendered locally in light and dark themes via SamplesApp.Skia.Generic --auto-screenshots and inspected:

Sample Verified
PasswordBox_Simple All five boxes render. The one at :34 applies a custom ControlTemplate (PasswordBoxWithoutRevealButtonStyle) unconditionally, so a retemplated PasswordBox really does reach Skia desktop — it renders with its own border/fill, not blank. Auto-grow box is correctly narrow when empty; reveal buttons correctly absent while unfocused/empty
PasswordBox_Header_PlaceholderText Header renders above the box, placeholder inside it — both now route through ITextBoxHost rather than being inherited. This sample has no automated assertions, so this was the only check on it
PasswordBox_PasswordChar Masking correct for all four chars (●, *, ?, $), and no squiggly underline — confirming the intended loss of the inherited IsSpellCheckEnabled (a password box no longer spell-checks its own masked text)

What remains for a reviewer is narrow: interactive states only. Hover/pressed visual-state transitions, and selection-highlight rendering during a drag. Focus changes, the reveal button actually revealing, and the context-flyout command set are already covered by runtime tests (When_PasswordBox_Focus_Changes, When_PasswordBox_TextRevealed, When_PasswordBox_ContextFlyout_Commands_Available, plus three touch-flyout tests).

One note for anyone repeating this: Input_PasswordBox looks like it exercises the custom template but does not — its style is applied as android:Style=, so on desktop it renders with the default template.

Validation performed

Ten builds, all green. The gate was four builds for seven items and missed three broken runtimes, so it is now every project this change can reach. Uno.UI.Skia/Uno.UI.Reference collapse into one Uno.UI post-#23999.

Gate Result
Uno.UI, Uno.UI.UnitTests ✅
SamplesApp.Skia.Generic, SamplesApp.UITests ✅
Skia runtimes: Win32, X11, MacOS, Browser, AppleUIKit, Android ✅ (three were broken mid-work; see above)
Given_TextBox + Given_PasswordBox (Skia Desktop) ✅ 243/243, 4 skipped — identical to both pre-rebase runs
Blast-radius filter (AutoSuggestBox, ComboBox, TextBlock, ScrollViewer, CommandBarFlyout) ✅ 238/239, 9 skipped
generatepkgdiff vs Uno.WinUI 6.6.184 ✅ clean — reproduced the failure locally, then confirmed the fix clears it

The single test failure, Given_TextBlock.When_Inlines_Transitively_Change, is proven pre-existing: a worktree at the base commit with no branch changes gives the same 105/106 with the same test failing.

⚠️ One project in the solution filter is outside the local gate. The new consolidated SamplesApp head (src/SamplesApp/SamplesApp/SamplesApp.csproj) compiles this branch's Given_PasswordBox.cs, Given_TextBox.skia.cs and App.Assertions.cs, but every one of its TFMs is $(NetCurrent) = net11.0, and global.json sets allowPrerelease: false — so it cannot be built on the dev machine at all (NETSDK1045). CI is the only gate for it. Flagging this explicitly because the same class of blind spot is what let an IDE0051 dead-method error reach CI earlier in this PR.

Platform coverage — the earlier "compile-verified only" caveat is retired. On the post-rebase run (i.e. against current feature/breakingchanges), CI ran the full runtime-test suite green on WebAssembly (4/4 shards), Android (5/5 shards, plus NativeAOT), Desktop Skia Windows (+ Snapshot Tests), Desktop Skia Linux and Linux Framebuffer, WinAppSDK, and iOS shards 1–3. So the iOS/WASM/Android text-input paths this change retypes are runtime-validated, not merely compiled. Skia Desktop (Windows) is additionally runtime-validated locally at 243/243. Screenshot Comparison is green.

The reparent's own claims (base type, removed surface, password not reachable as text, MaxLength parity) are covered by tests that genuinely fail before the change; the remaining new tests are regression guards that also passed before.

Two infrastructure timeouts, neither caused by this PR. Both are the 60-minute agent cap, and in both cases the job result is canceled rather than a test failure:

  • Desktop Skia Linux (first run) — the task log shows a 51-minute stall inside a single test, Given_BitmapSource.When_MsAppData(), which awaits a TaskCompletionSource with no timeout after subscribing to ImageOpened following the Source assignment. That subscribe-after-assign race is tracked as When_MsAppData hangs on the Linux Framebuffer runtime-test host under .NET 11 Preview 6 #23967. This branch touches no imaging code; the test is excluded on SkiaFrameBuffer, which is why Linux Framebuffer passed in the same build. The re-run passed in 17m28s.
  • Desktop Skia macOS — not a stall: the last test began 2.1 s before cancellation with the suite still progressing, i.e. it simply exhausted the budget. That stage is chronically marginal — feature/breakingchanges itself runs it at 43 min and 58 min against the 60-minute cap, and an unrelated PR (Remove DependencyObjectStore #23702) timed out in the same window. On this very commit Windows passed in 5m19s and Linux in 17m28s, so the platform is the variable, not the change. Raising that job's timeout is an upstream concern.

🤖 Generated with Claude Code

@github-actions github-actions Bot added platform/wasm 🌐 Categorizes an issue or PR as relevant to the WebAssembly platform platform/android 🤖 Categorizes an issue or PR as relevant to the Android platform platform/macos 🍏 Categorizes an issue or PR as relevant to the macOS platform platform/ios 🍎 Categorizes an issue or PR as relevant to the iOS platform area/skia ✏️ Categorizes an issue or PR as relevant to Skia area/automation Categorizes an issue or PR as relevant to project automation kind/documentation platform/x11 🐧 Categorizes an issue or PR as relevant to X11 labels Aug 10, 2026
Xiaoy312 added a commit that referenced this pull request Aug 10, 2026
Follows the existing convention (see BC31): the PR number goes on the row
while the checkbox stays unticked until it merges.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Xiaoy312 added a commit that referenced this pull request Aug 11, 2026
Follows the existing convention (see BC31): the PR number goes on the row
while the checkbox stays unticked until it merges.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Xiaoy312
Xiaoy312 force-pushed the dev/xygu/20260807/2138-passwordbox-reparent branch from ffd9cb0 to 90d2ddb Compare August 11, 2026 00:10
@unodevops

Copy link
Copy Markdown
Contributor

🤖 Your WebAssembly Skia Sample App stage site is ready! Visit it here: https://unowasmprstaging.z20.web.core.windows.net/pr-24038/wasm-skia-net9/index.html

@unodevops

Copy link
Copy Markdown
Contributor

🤖 Your Docs stage site is ready! Visit it here: https://unodocsprstaging.z13.web.core.windows.net/pr-24038/docs/index.html

1 similar comment
@unodevops

Copy link
Copy Markdown
Contributor

🤖 Your Docs stage site is ready! Visit it here: https://unodocsprstaging.z13.web.core.windows.net/pr-24038/docs/index.html

@unodevops

Copy link
Copy Markdown
Contributor

🤖 Your WebAssembly Skia Sample App stage site is ready! Visit it here: https://unowasmprstaging.z20.web.core.windows.net/pr-24038/wasm-skia-net9/index.html

@nventive-devops

Copy link
Copy Markdown
Contributor

The build 226868 found UI Test snapshots differences: skia-linux-screenshots: 74, skia-windows-screenshots: 113

Details
  • skia-linux-screenshots: 74 changed over 2366

    🚨🚨 Comparison Details (first 20) 🚨🚨
    • ClipboardTests.png-dark
    • ClipboardTests.png
    • DamageRegion_ShadowChildPaintingCaster.png-dark
    • DisplayInformation.png-dark
    • DisplayInformation.png
    • Focus_FocusVisual_Properties.png-dark
    • Focus_FocusVisual_Properties.png
    • CalendarView_Theming.png-dark
    • ImageBrushInList.png-dark
    • ImageBrushInList.png
    • Buttons.png-dark
    • Buttons.png
    • DynamicFont.png-dark
    • Gamepad_CurrentReading.png-dark
    • Gamepad_CurrentReading.png
    • Gamepad_Enumeration.png-dark
    • Gamepad_Enumeration.png
    • ImageIconPage.png-dark
    • ButtonClippingTestsControl.png-dark
    • ButtonClippingTestsControl.png
  • skia-windows-screenshots: 113 changed over 2366

    🚨🚨 Comparison Details (first 20) 🚨🚨
    • Battery.png-dark
    • ButtonClippingTestsControl.png-dark
    • DynamicFont.png-dark
    • Buttons.png
    • DisplayInformation.png-dark
    • DisplayInformation.png
    • ElementLevelTheme.png-dark
    • ElementLevelTheme.png
    • ImageIconPage.png-dark
    • ImageIconPage.png
    • ImageBrushInList.png-dark
    • ImageBrushInList.png
    • Image_UseTargetSizeLate.png-dark
    • Attributed_text_FontSize_Changing.png-dark
    • Attributed_text_FontSize_Changing.png
    • CalendarView_Theming.png-dark
    • CalendarView_Theming.png
    • ExpanderColorValidationPage.png-dark
    • DropDownButtonPage.png-dark
    • DropDownButtonPage.png

Xiaoy312 and others added 2 commits August 11, 2026 13:43
Records the design for moving PasswordBox off TextBox, and three
corrections to the source issue:

- Its proposed "shared internal base" is illegal in C# (CS0060). WinUI
  does use a shared base (CTextBox/CPasswordBox/CRichEditBox all derive
  from CTextBoxBase, no DPs on it) but projects PasswordBox : Control,
  a split C++/WinRT can express and C# cannot. Uno takes composition so
  both controls keep WinUI's projected base and nothing new goes public.
- The scope cites PasswordBox.Android.cs / PasswordBox.Apple.cs and
  native partials that no longer exist; Uno.UI ships only Skia and
  Reference.
- It misses the four Skia platform runtimes whose internal TextBox-typed
  extension interfaces serve PasswordBox today purely by inheritance.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds the composed shared text-input implementation that PasswordBox will
come to own, alongside the existing internal TextBoxView. ITextBoxHost
carries the architecture rationale, including why composition is used
rather than the shared base class WinUI itself has.

No implementation moves yet. TextBox implements the host contract and
holds a core; CanShowButton reads TextBoxCore.IsEmpty, which routes
control -> core -> host -> Text and so exercises the full loop.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Xiaoy312
Xiaoy312 force-pushed the dev/xygu/20260807/2138-passwordbox-reparent branch from 336c01f to 9567818 Compare August 11, 2026 17:55
Xiaoy312 added a commit that referenced this pull request Aug 11, 2026
Follows the existing convention (see BC31): the PR number goes on the row
while the checkbox stays unticked until it merges.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Xiaoy312 and others added 9 commits August 11, 2026 14:01
Move the editing implementation out of TextBox into the internal
TextBoxCore engine, reached through ITextBoxHost. TextBox keeps its
public dependency properties, events and methods, plus the protected
override shells that forward to the core; everything else -- selection,
caret, undo history, IME, pointers, grippers, clipboard, the selection
flyout and the proofing menu -- now lives on the engine.

Retypes the internal consumers the move exposes: the TextBox-typed
notifications singleton, the IME extension, and the out-of-assembly
consumers in the Skia platform runtimes.

Audited for the two silent-failure modes this shape of refactor invites:
partial-method declarations left behind without their implementation
(calls are silently elided) and protected overrides converted to plain
internal methods without a forwarding override (the handler stops being
called). Both compile clean while dropping behaviour.

No behaviour change -- PasswordBox still derives from TextBox here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Matches WinUI's projected hierarchy: `runtimeclass PasswordBox : Control`.
PasswordBox now composes a TextBoxCore through ITextBoxHost instead of
inheriting the whole text-input surface.

This closes the confidentiality leak. `OnPasswordChanged` no longer mirrors the
password into an inherited `Text` DP, so `passwordBox.Text` is gone rather than
returning cleartext, `TextChanged` no longer fires per keystroke, and
`BeforeTextChanging` can no longer observe -- or `Cancel` -- password input.

Also drops the ~16 `new`/`override` shadows that existed only to hide inherited
TextBox API. PasswordBox now declares its own Password, PasswordChar,
PasswordRevealMode, IsPasswordRevealButtonEnabled, Description, Header,
HeaderTemplate, PlaceholderText, InputScope, MaxLength, SelectionHighlightColor,
SelectionFlyout, CanPasteClipboardContent, PasswordChanged, ContextMenuOpening,
Paste, SelectAll() and PasteFromClipboard() -- the WinUI IDL surface, nothing more.
SelectedText, SelectionStart/Length, IsReadOnly, AcceptsReturn, TextWrapping,
CanUndo/CanRedo, Copy/CutSelectionToClipboard and ProofingMenuFlyout are gone,
and `is TextBox` is now false for a PasswordBox.

Items 11 and 12 are one commit because they cannot be split: the mirroring
writes TextProperty, which stops existing the moment the base type changes.

Two consumer sweeps were forced rather than deferred:

- TextCommandBarFlyout's ten `and not PasswordBox` guards became dead patterns
  (CS8121). The guards and the comments explaining the Uno-specific hierarchy
  both go.
- TextBoxView read everything through its `TextBox` property, which returns null
  for a reparented PasswordBox. `UpdatePasswordMasking` in particular would have
  left the mask frozen at whatever was last rendered -- silently, since every
  one of those paths is a null-conditional. It now reads the core, and
  GetPasswordChar comes through a new `ITextBoxHost.PasswordChar` instead of
  type-testing its way to a PasswordBox. Resolves the standing TODO that
  predicted exactly this.

RaiseValueAutomationEvents is deliberately empty: the shared implementation only
raised those when the peer was a TextBoxAutomationPeer, and a password box's peer
never was, so raising them now would be new behaviour rather than parity.

When_Copy_Cut_Does_Not_Leak_Password is rewritten to drive Ctrl+C/Ctrl+X through
the keyboard -- it encodes a security invariant, and the accelerator is the path a
user actually has now that the TextBox-only clipboard methods are gone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The override audit (pre-move override set vs the control's current one) found
PasswordBox missing five of TextBox's 35 overrides. Three are inert -- their
bodies call `base` plus a partial method that has no implementation anywhere and
was already dead before this work. Two were real:

- `UpdateVisualState` is the serious one. Without it, PasswordBox's own calls
  from OnVisibilityChanged/OnPointerEntered/OnPointerExited/OnPointerCaptureLost/
  OnIsEnabledChanged reached Control's implementation instead of the core's, so
  every visual state -- Normal/PointerOver/Focused/Disabled and the reveal
  button's visibility -- silently stopped working.
- `GetPlainText` supplies the automation name from Header/PlaceholderText.

Also: PasswordBox spell-checked its own masked text before the reparent, because
it inherited TextBox's IsSpellCheckEnabled (default true) and TextBoxView pushes
that onto the DisplayBlock. WinUI's PasswordBox has no such property and never
spell-checks, so the host answers false -- which changes rendering, and
When_PasswordChar_Visual_Comparison caught it as a deterministic 0.0511 RSMD
against a 0.05 threshold. The test compares a PasswordBox against a TextBox
showing the same letters, so it was asserting the old squiggly-underline parity;
both comparisons now disable spell-check on the TextBox so they isolate the mask
character, which is what they mean to test.

Reverted PasswordBox's InputScope default to Default. Password would have been
the better value but it is not what the inherited property defaulted to, and
changing it is not part of reparenting.

Given_TextBox + Given_PasswordBox: 235/235 passed, 4 skipped -- unchanged from
the pre-refactor baseline, now with PasswordBox deriving from Control.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Item 20. Every gate so far was a no-regression check; nothing asserted the
change's actual claim. Adds 7 tests to Given_PasswordBox (235 -> 242).

Four genuinely fail before the reparent and pass after:

- When_Derives_From_Control_Not_TextBox -- BaseType was TextBox
- When_TextBox_Only_Api_Is_Not_Reachable -- Text, SelectedText, SelectionStart/
  Length, IsReadOnly, AcceptsReturn, TextWrapping, CanUndo/CanRedo,
  ProofingMenuFlyout, Select, Copy/CutSelectionToClipboard, Undo/Redo,
  TextChanged/TextChanging/BeforeTextChanging/SelectionChanged/SelectionChanging
- When_Password_Is_Not_Reachable_As_Text -- Text returned the cleartext. Written
  as a sweep over every readable string-valued member rather than a hand-picked
  list, so a member added later is covered too.
- When_MaxLength_Matches_TextBox -- catches the old desync, where Password took
  an over-long value while the mirrored Text rejected it.

Three are regression guards rather than proof, since they also passed before:
When_WinUI_Surface_Is_Declared (the IDL surface, which items 22/23 can silently
erode), When_Password_Changes_PasswordChanged_Is_Raised, and
When_Automation_Does_Not_Expose_Password.

The hierarchy and surface assertions are deliberately not platform-gated: they
are WinUI-parity claims that hold on native WinUI too.

MaxLength is asserted as parity with TextBox rather than against a hard-coded
result. My first attempt asserted truncation and failed -- Uno's CoerceText
rejects an over-long value outright. Comparing the two controls to each other
tests what "behaviour preserved" means and holds whichever semantic the platform
picks, while still catching MaxLength doing nothing.

Given_TextBox + Given_PasswordBox: 242/242 passed, 4 skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The reparent left ten sites inside Uno.UI type-testing or casting to TextBox,
so each silently stopped serving a PasswordBox. All compiled green.

TextBlock.OwningTextBox is the significant one: typed as TextBox, it was null
for a reparented PasswordBox, which meant the display block reset its own
selection on every text change, dropped the caret-thickness width adjustment,
and stopped routing ContextRequested. Retyped to TextBoxCore, which is what it
actually needs and what serves both controls.

The rest route through a new ITextBoxHost.Core seam so a FrameworkElement can
reach the engine without naming a concrete control: TextControlFlyoutHelper
(ForceFocusLoss), ScrollViewer's touch-caret snap guard and its cast together,
XamlRoot.InvalidateOverlays, UIElement.ContextRequested's menu position,
TextBoxExtensions.InputReturnType, and the UIA TextAdapter/TextRangeAdapter
selection members. GetAdjustedTextAlignment now asks the host whether alignment
was set explicitly, so a PasswordBox answers no by construction rather than
querying a dependency property not registered for it.

Verified no-ops, deliberately left alone: both FocusManager sites (bodies are
entirely commented out), AriaMapper (IsPassword wins first), AutomationProperties
(both branches return "textbox", and the PasswordBox branch is ungated by a
linker hint), and all ten TextCommandBarFlyout guards removed earlier - every
one was `and not PasswordBox`, so `is TextBox` now excludes it naturally.

Adds a fails-before test: selecting in a PasswordBox and then rewriting the
mask lost the selection (Range(0,0) instead of Range(2,7)).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three Skia runtime projects did not compile on this branch: Android (10
errors), Browser (6) and AppleUIKit (4). The four-build gate used through
items 5-12 never compiled any of them, so items 5 and 11 broke them silently.
Add all six runtimes to the gate.

The native overlay path was TextBox-typed end to end - IOverlayTextBoxView
took a TextBox, and TextBoxView.TextBox downcast the engine's owner - so a
reparented PasswordBox got null and every use was null-conditional, meaning
password entry through the invisible native text view would no-op rather than
throw. TextBoxView.TextBox is now deleted rather than retyped, so the type
system enforces this instead of a convention: IOverlayTextBoxView and
IInvisibleTextBoxView name TextBoxCore, and the iOS/Browser/Android
implementations follow.

Also fixed the a11y and IME consumers: WebAssemblyAccessibility's semantic
<input> value/selection sync, FocusSynchronizer, SemanticElementFactory,
Android's TextInputPlugin (its `textBox is PasswordBox` tests had become dead
patterns, which is what made the input type, autofill hint and sensitivity
flag wrong for a password field) and TextInputConnection's IME editable.

MacOSAccessibility's selection sync is the one site no build catches - that
project compiles green, so VoiceOver would just stop hearing about a password
box's selection changes.

ITextBoxHost gains PlaceholderText, IsPasswordRevealed and a SelectionChanged
notification, all of which existed as control-typed API the platform layers
were reaching for. Five core shims move from private to internal for the same
reason. Two latent nullable-reference errors surfaced in Android's autofill
path: TextBox.Text lives in a nullable-oblivious file, TextBoxCore.Text does
not.

Gate: Uno.UI.Skia, Uno.UI.Reference, Uno.UI.UnitTests, SamplesApp.Skia.Generic
and all six Skia runtimes build clean. Given_TextBox + Given_PasswordBox
243/243 (4 skipped); blast-radius filter 238/239 (9 skipped, the one failure
pre-existing). The platform-runtime changes are compile-verified only - no
runtime validation on iOS, WASM or Android.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PasswordShouldNotDuplicateWhenRegainingFocus asserted against a string-keyed
DP lookup for "Text". That name resolves to nothing once PasswordBox stops
deriving from TextBox, so the assertion would compare "123123" against null -
and being string-keyed, no compiler catches it.

The same suite already uses SetDependencyPropertyValue("Password", ...) on the
autoGrow password box, so "Password" is the established convention here.

Swept exhaustively rather than by name: cross-referenced every string-keyed
Get/Set/WaitForDependencyPropertyValue call in SamplesApp.UITests against all
16 <PasswordBox> element names in the samples. This was the only wrong one.

The samples themselves need no changes - none of the 37 PasswordBox elements
sets a TextBox-only property and none binds to Text - so xstyler is not run,
as reformatting untouched XAML would only add noise.

Adds SamplesApp.UITests to the standing gate; it is in Uno.UI-Skia-only.slnf
but was not in the ten builds, which is the same gap one level out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follows the existing convention (see BC31): the PR number goes on the row
while the checkbox stays unticked until it merges.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PR #23999 folded the UI-layer Reference flavor into Skia and merged every
`.skia.cs` partial into its base file, so `Uno.UI` now carries none of either
suffix. These files were added before that landed.

- `.skia.cs` dropped from the six new TextBox/PasswordBox/TextBoxCore partials;
  `PasswordBox.skia.cs` becomes `PasswordBox.Properties.cs`, mirroring
  `TextBox.Properties.cs`, since it holds public DPs rather than platform code
- the engine becomes `TextBoxCore.Input.cs` rather than folding into
  `TextBoxCore.cs`: the latter is `#nullable enable` and the engine is
  nullable-oblivious, so merging them would put 1881 unannotated lines under an
  annotated context and surface 40 latent warnings unrelated to this change
- `TextBoxCore.reference.cs` is deleted; `Uno.UI.csproj` is Skia-only, so a
  `.reference.cs` file no longer compiles anywhere, exactly as #23999 did with
  `TextBox.reference.cs`

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Xiaoy312 and others added 3 commits August 11, 2026 14:01
Adds PasswordBox to the type-hierarchy section alongside MediaPlayerPresenter,
ImageBrush and the theme animations.

Calls out the two things a migrating app cannot discover from the compiler:
BeforeTextChanging loses its Cancel with no replacement (PasswordChanging is
unimplemented and its args carry no Cancel either), and MaxLength now limits
Password directly rather than through the removed Text mirror, which the old
chain applied inconsistently.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CI's code-style analyzer flagged IDE0051 on TextBoxCore.OnKeyDownNonSkia.
It is genuinely unreachable: OnPostKeyDown only called it from a `#else`
branch, and Uno.UI.csproj is Skia-only since #23999, so `__SKIA__` is
defined for every target framework it builds.

Deletes the method and collapses OnPostKeyDown to forward directly. Also
drops a comment describing TextBoxCore.skia.cs and the Reference flavor,
neither of which exists after #23999.

Local builds could not have caught this: the ten-build gate passes
UnoFastDevBuild=true, which disables EnforceCodeStyleInBuild, and dropping
that flag is not sufficient either -- a net10.0 build with it removed still
emits zero IDE/CA diagnostics. CI's failing configuration is net11.0, which
global.json's allowPrerelease:false makes unselectable here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PasswordBox's public SelectionFlyout getter implicitly implemented
ITextBoxHost.SelectionFlyout, which emits it as virtual/final/newslot.
Uno.PackageDiff reports that vtable change as a removed method and fails
the Managed Binaries build. TextBox already forwards the member
explicitly, keeping its public getter non-virtual; do the same here.

Verified with generatepkgdiff against Uno.WinUI 6.6.184: the unmodified
package build reports "Removed method ... PasswordBox.get_SelectionFlyout()"
and exits with differences, while the same package carrying only this fix
compares clean. No PackageDiffIgnore.xml entry is required.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Xiaoy312
Xiaoy312 force-pushed the dev/xygu/20260807/2138-passwordbox-reparent branch from 9567818 to 5af55c8 Compare August 11, 2026 18:02
@unodevops

Copy link
Copy Markdown
Contributor

🤖 Your WebAssembly Skia Sample App stage site is ready! Visit it here: https://unowasmprstaging.z20.web.core.windows.net/pr-24038/wasm-skia-net9/index.html

@unodevops

Copy link
Copy Markdown
Contributor

🤖 Your Docs stage site is ready! Visit it here: https://unodocsprstaging.z13.web.core.windows.net/pr-24038/docs/index.html

@nventive-devops

Copy link
Copy Markdown
Contributor

The build 227059 found UI Test snapshots differences: skia-linux-screenshots: 74, skia-windows-screenshots: 120

Details
  • skia-linux-screenshots: 74 changed over 2366

    🚨🚨 Comparison Details (first 20) 🚨🚨
    • Buttons.png-dark
    • Buttons.png
    • DisplayInformation.png-dark
    • DisplayInformation.png
    • CalendarView_Theming.png-dark
    • ButtonClippingTestsControl.png-dark
    • ButtonClippingTestsControl.png
    • DamageRegion_ShadowChildPaintingCaster.png-dark
    • ElementLevelTheme.png-dark
    • ElementLevelTheme.png
    • DoubleImageBrushInList.png-dark
    • ImageBrushInList.png-dark
    • ImageBrushInList.png
    • ClipboardTests.png-dark
    • ClipboardTests.png
    • Focus_FocusVisual_Properties.png-dark
    • Focus_FocusVisual_Properties.png
    • Image_AnimatedWebP.png
    • Examples.png
    • DynamicFont.png-dark
  • skia-windows-screenshots: 120 changed over 2366

    🚨🚨 Comparison Details (first 20) 🚨🚨
    • Basics Pivot Test.png
    • Battery.png-dark
    • ExpanderColorValidationPage.png-dark
    • ExpanderColorValidationPage.png
    • Attributed_text_FontSize_Changing.png
    • Battery.png
    • CameraCaptureUISample.png-dark
    • CameraCaptureUISample.png
    • ButtonClippingTestsControl.png
    • ButtonClippingTestsControl.png-dark
    • Buttons.png-dark
    • CalendarView_Theming.png-dark
    • CalendarView_Theming.png
    • DisplayInformation.png-dark
    • DisplayInformation.png
    • ContentPresenter_NativeEmbedding_Android_FillType.png
    • ContextRequested.png-dark
    • ContextRequested.png
    • EllipsemaskingEllipseGrid.png-dark
    • ElementLevelTheme.png-dark

@Xiaoy312
Xiaoy312 marked this pull request as ready for review August 11, 2026 20:20
@unodevops

Copy link
Copy Markdown
Contributor

🤖 Your WebAssembly Skia Sample App stage site is ready! Visit it here: https://unowasmprstaging.z20.web.core.windows.net/pr-24038/wasm-skia-net9/index.html

@unodevops

Copy link
Copy Markdown
Contributor

🤖 Your Docs stage site is ready! Visit it here: https://unodocsprstaging.z13.web.core.windows.net/pr-24038/docs/index.html

@nventive-devops

Copy link
Copy Markdown
Contributor

The build 227096 found UI Test snapshots differences: skia-linux-screenshots: 76, skia-windows-screenshots: 119

Details
  • skia-linux-screenshots: 76 changed over 2366

    🚨🚨 Comparison Details (first 20) 🚨🚨
    • Attributed_text_FontSize_Changing.png
    • ButtonClippingTestsControl.png-dark
    • ButtonClippingTestsControl.png
    • ClipboardTests.png-dark
    • ClipboardTests.png
    • Examples.png
    • CalendarView_Theming.png-dark
    • CalendarView_Theming.png
    • ExpanderColorValidationPage.png-dark
    • DisplayInformation.png-dark
    • ImageBrushInList.png-dark
    • ImageBrushInList.png
    • Image_Margin_Large.png
    • ContextRequested.png
    • ElementLevelTheme.png-dark
    • ElementLevelTheme.png
    • Gamepad_CurrentReading.png-dark
    • Gamepad_CurrentReading.png
    • Gamepad_Enumeration.png-dark
    • Gamepad_Enumeration.png
  • skia-windows-screenshots: 119 changed over 2366

    🚨🚨 Comparison Details (first 20) 🚨🚨
    • Buttons.png-dark
    • Buttons.png
    • Examples.png
    • Contacts_Pick.png-dark
    • Contacts_Pick.png
    • Attributed_text_FontSize_Changing.png-dark
    • Basics.png-dark
    • ContentPresenter_Template.png-dark
    • ContentPresenter_TextProperties.png-dark
    • ContentPresenter_TextProperties.png
    • DisplayInformation.png-dark
    • DataTemplate_Update_Sample.png-dark
    • DataTemplate_Update_Sample.png
    • DataTransferManager.png-dark
    • DataTransferManager.png
    • ImageBrushInList.png-dark
    • ImageBrushInList.png
    • ImageBrushStretch.png-dark
    • EmailManagerTests.png-dark
    • EmailManagerTests.png

@mergify

mergify Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

The spec was written as item 1 of the work and never updated, so it still
described the pre-#23999 tree and an unstarted implementation. Corrections:

- The suffixed files it named (TextBox.skia.cs, .IME.skia.cs,
  .pointers.skia.cs, .reference.cs) no longer exist, and Uno.UI.Skia /
  Uno.UI.Reference are now a single Skia-only Uno.UI.
- Affected-files list replaced with what actually landed, including the
  six sites verified as no-ops and the samples that needed no change.
- PackageDiffIgnore.xml is NOT modified: the one reported break was
  PasswordBox.get_SelectionFlyout() being emitted virtual because the
  public property implicitly implemented ITextBoxHost. Forwarding the
  member explicitly restores binary compatibility, so no ignore entry.
- Records the behaviour changes found on the way (spell-check on masked
  text, MaxLength coercion, TextBlock.OwningTextBox), the partial void /
  override silent-failure audits, why the chrome/engine split was
  abandoned, and the visual verification.
- Validation status goes from "Not started" to the final gate results.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@unodevops

Copy link
Copy Markdown
Contributor

🤖 Your WebAssembly Skia Sample App stage site is ready! Visit it here: https://unowasmprstaging.z20.web.core.windows.net/pr-24038/wasm-skia-net9/index.html

@unodevops

Copy link
Copy Markdown
Contributor

🤖 Your Docs stage site is ready! Visit it here: https://unodocsprstaging.z13.web.core.windows.net/pr-24038/docs/index.html

@nventive-devops

Copy link
Copy Markdown
Contributor

The build 227245 found UI Test snapshots differences: skia-linux-screenshots: 76, skia-windows-screenshots: 149

Details
  • skia-linux-screenshots: 76 changed over 2366

    🚨🚨 Comparison Details (first 20) 🚨🚨
    • ImageBrushInList.png-dark
    • ImageBrushInList.png
    • ColorPickerSample.png
    • Examples.png
    • Buttons.png-dark
    • Buttons.png
    • ClipboardTests.png-dark
    • ClipboardTests.png
    • Image_Margin_Large.png
    • ButtonClippingTestsControl.png-dark
    • ButtonClippingTestsControl.png
    • CalendarView_Theming.png-dark
    • CalendarView_Theming.png
    • Attributed_text_FontSize_Changing.png
    • ElementLevelTheme.png-dark
    • Gamepad_CurrentReading.png-dark
    • ElementLevelTheme.png
    • Gamepad_CurrentReading.png
    • Gamepad_Enumeration.png-dark
    • Gamepad_Enumeration.png
  • skia-windows-screenshots: 149 changed over 2366

    🚨🚨 Comparison Details (first 20) 🚨🚨
    • AuthenticationBroker_Demo.png-dark
    • Battery.png
    • CustomTitleBar_Tests.png-dark
    • Examples.png
    • AuthenticationBroker_Demo.png
    • Attributed_text_FontSize_Changing.png
    • Battery.png-dark
    • CameraCaptureUISample.png-dark
    • CameraCaptureUISample.png
    • ButtonClippingTestsControl.png-dark
    • ButtonClippingTestsControl.png
    • Buttons.png
    • ClipboardTests.png-dark
    • ClipboardTests.png
    • Contacts_Pick.png-dark
    • Contacts_Pick.png
    • DataTransferManager.png-dark
    • ExpanderColorValidationPage.png-dark
    • DataTransferManager.png
    • ExpanderColorValidationPage.png

@Xiaoy312
Xiaoy312 merged commit 6bc0129 into feature/breakingchanges Aug 12, 2026
49 checks passed
@Xiaoy312
Xiaoy312 deleted the dev/xygu/20260807/2138-passwordbox-reparent branch August 12, 2026 19:54
@github-actions github-actions Bot mentioned this pull request Aug 20, 2026
4 of 5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/automation Categorizes an issue or PR as relevant to project automation area/skia ✏️ Categorizes an issue or PR as relevant to Skia kind/documentation platform/android 🤖 Categorizes an issue or PR as relevant to the Android platform platform/ios 🍎 Categorizes an issue or PR as relevant to the iOS platform platform/macos 🍏 Categorizes an issue or PR as relevant to the macOS platform platform/wasm 🌐 Categorizes an issue or PR as relevant to the WebAssembly platform platform/x11 🐧 Categorizes an issue or PR as relevant to X11

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants