From 112e4087e85be9fa3f1b71089b420cb71229654d Mon Sep 17 00:00:00 2001 From: Koji Wakamiya Date: Sat, 27 Jul 2024 04:55:49 +0900 Subject: [PATCH] Fix cursor position when Unicode Zs category is entered in TextField (#152215) Changed the cursor position to be the same as before flutter 3.22.0 when 17 character codes in the Unicode Zs category are entered into a TextField. Extend the support for https://github.com/flutter/flutter/pull/149698. As a result, https://github.com/flutter/flutter/issues/149099 is resolved. The code for the Unicode-Zs category is based on the following page. https://www.compart.com/en/unicode/category/Zs Fixes https://github.com/flutter/flutter/issues/149099 --- .../lib/src/painting/text_painter.dart | 14 ++-- .../test/painting/text_painter_test.dart | 81 ++++++++++++++++--- 2 files changed, 78 insertions(+), 17 deletions(-) diff --git a/packages/flutter/lib/src/painting/text_painter.dart b/packages/flutter/lib/src/painting/text_painter.dart index d769ed8cd1d..61d93d7b948 100644 --- a/packages/flutter/lib/src/painting/text_painter.dart +++ b/packages/flutter/lib/src/painting/text_painter.dart @@ -356,11 +356,15 @@ class _TextLayout { // Luckily they have the same bidi embedding level as the paragraph as per // https://unicode.org/reports/tr9/#L1, so we can anchor the caret to the // last logical trailing space. - final bool hasTrailingSpaces = switch (rawString.codeUnitAt(rawString.length - 1)) { - 0x9 || // horizontal tab - 0x3000 || // ideographic space - 0x20 => true, // space - _ => false, + // Whitespace character definitions refer to Java/ICU, not Unicode-Zs. + // https://github.com/unicode-org/icu/blob/23d9628f88a2d0127c564ad98297061c36d3ce77/icu4c/source/common/unicode/uchar.h#L3388-L3425 + final String lastCodeUnit = rawString[rawString.length - 1]; + final bool hasTrailingSpaces = switch (lastCodeUnit.codeUnitAt(0)) { + 0x0009 => true, // horizontal tab + 0x00A0 || // no-break space + 0x2007 || // figure space + 0x202F => false, // narrow no-break space + _ => RegExp(r'\p{Space_Separator}', unicode: true).hasMatch(lastCodeUnit), }; final double baseline = lineMetrics.baseline; diff --git a/packages/flutter/test/painting/text_painter_test.dart b/packages/flutter/test/painting/text_painter_test.dart index d946807d7b2..b8293455ae8 100644 --- a/packages/flutter/test/painting/text_painter_test.dart +++ b/packages/flutter/test/painting/text_painter_test.dart @@ -145,20 +145,77 @@ void main() { caretOffset = painter.getOffsetForCaret(ui.TextPosition(offset: text.length), ui.Rect.zero); expect(caretOffset.dx, painter.width); - // Test with trailing full-width space - const String textWithFullWidthSpace = 'A\u{3000}'; - checkCaretOffsetsLtr(textWithFullWidthSpace); - painter.text = const TextSpan(text: textWithFullWidthSpace); - painter.layout(); - caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 0), ui.Rect.zero); - expect(caretOffset.dx, 0); - caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 1), ui.Rect.zero); - expect(caretOffset.dx, painter.width / 2); - caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: textWithFullWidthSpace.length), ui.Rect.zero); - expect(caretOffset.dx, painter.width); + /// Verify the handling of spaces by SkParagraph and TextPainter. + /// + /// Test characters that are in the Unicode-Zs category but are not treated as whitespace characters by SkParagraph. + /// The following character codes are intentionally excluded from the test target. + /// * '\u{00A0}' (no-break space) + /// * '\u{2007}' (figure space) + /// * '\u{202F}' (narrow no-break space) + void verifyCharacterIsConsideredTrailingSpace(String character) { + final String reason = 'character: ${character.codeUnitAt(0).toRadixString(16)}'; + + text = 'A$character'; + checkCaretOffsetsLtr(text); + painter.text = TextSpan(text: text); + painter.layout(); + caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 0), ui.Rect.zero); + expect(caretOffset.dx, 0.0, reason: reason); + caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 1), ui.Rect.zero); + expect(caretOffset.dx, 14.0, reason: reason); + caretOffset = painter.getOffsetForCaret(ui.TextPosition(offset: text.length), ui.Rect.zero); + expect(caretOffset.dx, painter.width, reason: reason); + + painter.layout(maxWidth: 14.0); + final List lines = painter.computeLineMetrics(); + expect(lines.length, 1, reason: reason); + expect(lines.first.width, 14.0, reason: reason); + } + + // Test with trailing space. + verifyCharacterIsConsideredTrailingSpace('\u{0020}'); + + // Test with trailing full-width space. + verifyCharacterIsConsideredTrailingSpace('\u{3000}'); + + // Test with trailing ogham space mark. + verifyCharacterIsConsideredTrailingSpace('\u{1680}'); + + // Test with trailing en quad. + verifyCharacterIsConsideredTrailingSpace('\u{2000}'); + + // Test with trailing em quad. + verifyCharacterIsConsideredTrailingSpace('\u{2001}'); + + // Test with trailing en space. + verifyCharacterIsConsideredTrailingSpace('\u{2002}'); + + // Test with trailing em space. + verifyCharacterIsConsideredTrailingSpace('\u{2003}'); + + // Test with trailing three-per-em space. + verifyCharacterIsConsideredTrailingSpace('\u{2004}'); + + // Test with trailing four-per-em space. + verifyCharacterIsConsideredTrailingSpace('\u{2005}'); + + // Test with trailing six-per-em space. + verifyCharacterIsConsideredTrailingSpace('\u{2006}'); + + // Test with trailing punctuation space. + verifyCharacterIsConsideredTrailingSpace('\u{2008}'); + + // Test with trailing thin space. + verifyCharacterIsConsideredTrailingSpace('\u{2009}'); + + // Test with trailing hair space. + verifyCharacterIsConsideredTrailingSpace('\u{200A}'); + + // Test with trailing medium mathematical space(MMSP). + verifyCharacterIsConsideredTrailingSpace('\u{205F}'); painter.dispose(); - }); + }, skip: isBrowser && !isSkiaWeb); // https://github.com/flutter/flutter/issues/56308 test('TextPainter caret test with WidgetSpan', () { // Regression test for https://github.com/flutter/flutter/issues/98458.