Visitar URL original
fix(textbox): Paste and cut like WinUI with MaxLength, line breaks and undo by mikernet · Pull Request #26006 · unoplatform/uno · GitHub
Skip to content

fix(textbox): Paste and cut like WinUI with MaxLength, line breaks and undo - #26006

Open
mikernet wants to merge 10 commits into
unoplatform:masterfrom
mikernet:dev/mikernet/textbox-paste-surrogates
Open

mikernet wants to merge 10 commits into
unoplatform:masterfrom
mikernet:dev/mikernet/textbox-paste-surrogates

Conversation

@mikernet

@mikernet mikernet commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

GitHub Issue: closes #25998

Builds on #26005: it uses that PR's rule for when an edit counts as applied, its line-break helper, and its helper that applies input while keeping the undo history. Until that PR is merged, this one also shows its three commits.

PR Type:

🐞 Bugfix

What changed? 🚀

Pasting into a TextBox, and cutting from it, now behave the way they do on WinUI in these cases. Each fix is its own commit:

  • fix(textbox): Keep surrogate pairs whole in paste: when a paste doesn't fit under MaxLength, the shortened text no longer ends in half an emoji (a surrogate pair). A pair that doesn't fit is left out whole. Before, half of it was inserted: on 6.8.0-dev.222 that crashed text layout, and on master it left a broken character in the text. When the only room left is half a pair, nothing is inserted; with nothing selected, the text events don't fire either. As on WinUI, the text is shortened by code point, so a letter can still lose a combining mark that doesn't fit.
  • fix(textbox): Keep text after a single-line paste: pasting several lines into a single-line TextBox inserts the pasted text's first line and keeps the text after the caret. Before, everything after the first line break was dropped, including the text that was already after the caret ("XY" + "a↵b" at the caret gave "Xa"; it now gives "XaY").
  • fix(textbox): Count pasted line breaks as stored: in a multi-line TextBox, a pasted \r\n counts as one character against MaxLength, because that's how the TextBox stores it. Before, it counted as two, so less text fit. The paste uses the same line-break helper as the Text coercion.
  • fix(wasm): Ignore browser pastes without text: a browser paste with no text on the clipboard, such as an image, leaves a TextBox's text and selection alone, from the hidden input and, with accessibility on, from the semantic text element. Before, both deleted the selection. Such a paste no longer raises Paste on a TextBox, where WinUI does (see "Not changed" below); a RichEditBox still gets its Paste event. Both browser listeners read the text through one helper.
  • fix(textbox): Make cuts undoable: a cut is an undo step, and Undo puts the text back with its selection. Before, the cut set Text the way code does, which cleared the undo history, so a cut couldn't be undone at all.
  • fix(textbox): Record each applied paste and cut: a paste or a cut is its own undo step, also right after typing (before, the first Undo removed the typing and the paste together). It is recorded once it is applied, with the MaxLength PR's rule, so a paste or cut that MaxLength or a BeforeTextChanging handler refuses, or that changes nothing (no room left under MaxLength, nothing to paste and nothing selected), adds no undo step and keeps Redo. A paste that replaces a selection with the same text is still an undo step, as on WinUI. A cut with nothing selected only ends the typing run, as on WinUI.
  • fix(textbox): Set caret after redone paste or cut: after Redo, the caret is after the pasted text, or where the cut text was. Before, it went to the start of the paste, or to where the cut selection had ended.

Cost: for each paste, one line-break normalization of the pasted text; nothing when typing.

Not changed here:

  • The Paste event is still raised after the paste, and Handled doesn't cancel it (a separate issue). With this PR, a browser paste without text raises no Paste on a TextBox, like the other paste paths when the clipboard has no text; WinUI raises it.
  • The iOS edit menu's paste and the Android keyboard's paste command go through the native text view, not through the shared paste code, so they aren't covered.
  • Text longer than MaxLength: what a paste does there (delete the selection and insert nothing) comes from the MaxLength PR.
  • A same-text replacement: WinUI raises BeforeTextChanging, TextChanging and TextChanged when a paste replaces a selection with the same text, so a handler there can refuse it. Uno raises none, because the Text property doesn't change, and a handler can't refuse it.
  • A cancelled paste or cut: WinUI also clears the rest of the undo history then; Uno keeps it, for the reasons in fix(textbox): Limit only user input to MaxLength #26005.
  • An IME composition during a paste or a cut keeps a range that no longer matches the text, as before.

Validation:

  • Base: current master at 09b5f7a, which includes fix(textbox): Fix IME and touch editing on Android browsers #25804. Its browser input and composition tests still pass, among them the ones that rely on input past MaxLength being refused during a composition.
  • The expected values come from a WinUI 3 app (Windows App SDK 1.8) that drives a TextBox through its own APIs, called the WinUI recording app in the tests. It set the clipboard and called PasteFromClipboard(), typed through key messages posted to its own window, and called Undo, Redo and CutSelectionToClipboard. Tests say where a value wasn't recorded on WinUI.
  • New Given_TextBox_Paste runtime tests. They paste through the internal paste-with-text method and cut through a new internal CutSelection(), which does the cut without the copy, so they don't touch the clipboard. CutSelection() has the same guards as the real cut.
  • Skia Desktop (Windows, Win32 host): each commit's tests run on the commit before it, then with it. Before each commit, these fail: 8 of the 14 surrogate cases (the rest pin cases that already worked, such as a combining mark, which WinUI also cuts by code point), 4 of 4 single-line cases, 3 of 4 line-break cases, 2 of 2 cut cases, 10 of the 19 cases of the recording commit (the other nine pin behavior that already held, such as a cut right after a selection change, an empty paste over a selection, Undo of a paste into over-long text or over a backward selection, and an Undo() run from TextChanging during a cut), and 4 of 4 Redo cases. Before the cut commit, the tests' CutSelection() is a temporary stand-in that cuts the old way without the copy. All 47 desktop cases pass at the end; the browser cases run in WebAssembly only. The per-commit runs predate the rebase on 09b5f7a, which changed nothing in these commits' code. With the MaxLength PR underneath, Given_TextBox*, Given_PasswordBox, Given_ImeSessionCoordinator*, Given_AccessibleTextBox, Given_RichEditBox* and Given_BrowserKeyboardInputSource pass too: 1382 cases, 0 failures, without the clipboard tests.
  • WebAssembly (headless Chrome): before the WebAssembly commit, 2 of the 5 browser cases fail: a paste with no text deletes the selection, from the hidden input and from the semantic element. The other three check that text and empty text still paste and that a RichEditBox still raises Paste. With both PRs, the paste, MaxLength, IME, browser-input and semantic-input tests pass in the browser: 272 cases, 1 skipped (mobile only).
  • Tests that touch the clipboard: the 142 tests (147 cases) in those classes that use the system clipboard, which the 1382 leave out, ran on their own, in a separate window station with its own clipboard: 140 cases pass, 5 are skipped and 1 is inconclusive on this platform, and When_Touch_LongPress_Drag_Release_Does_Not_Open_Flyout_iOS fails, the same way as on master without these changes. Among them are When_Cut_Paste_Breaks_Typing, When_Cut_Paste, When_Paste_History_Remains_Intact, When_Paste_The_Same_Text, When_Paste_Does_Not_Change_Text and When_MaxLine_Paste, which all pass.

PR Checklist ✅

@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 kind/documentation labels Nov 3, 2026
@mikernet
mikernet force-pushed the dev/mikernet/textbox-paste-surrogates branch from 9c43b3f to ac50ccf Compare November 3, 2026 23:05
@mikernet
mikernet marked this pull request as ready for review November 3, 2026 23:34
@unodevops

Copy link
Copy Markdown
Contributor

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

@mergify

mergify Bot commented Nov 10, 2026

Copy link
Copy Markdown
Contributor

Tick the box to add this pull request to the merge queue (same as @mergifyio queue). If it does not match the queue conditions yet, it enters the queue once it does.

  • Queue this pull request

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TextBox paste can cut an emoji in half under MaxLength, and its undo steps are wrong

2 participants