diff --git a/lib/ui/dart_ui.cc b/lib/ui/dart_ui.cc index 77b126d797346..efb5af0a30a69 100644 --- a/lib/ui/dart_ui.cc +++ b/lib/ui/dart_ui.cc @@ -78,7 +78,7 @@ typedef CanvasPath Path; V(Gradient::Create, 1) \ V(ImageFilter::Create, 1) \ V(ImageShader::Create, 1) \ - V(ParagraphBuilder::Create, 9) \ + V(ParagraphBuilder::Create, 10) \ V(PathMeasure::Create, 3) \ V(Path::Create, 1) \ V(PictureRecorder::Create, 1) \ diff --git a/lib/ui/text.dart b/lib/ui/text.dart index 313630e98017b..89b3be4662fed 100644 --- a/lib/ui/text.dart +++ b/lib/ui/text.dart @@ -2798,7 +2798,7 @@ abstract class Paragraph { /// This only returns a valid value if asserts are enabled, and must not be /// used otherwise. bool get debugDisposed; - } +} @pragma('vm:entry-point') base class _NativeParagraph extends NativeFieldWrapperClass1 implements Paragraph { @@ -3015,6 +3015,28 @@ abstract class ParagraphBuilder { /// [Paragraph]. factory ParagraphBuilder(ParagraphStyle style) = _NativeParagraphBuilder; + /// Whether the rounding hack enabled by default in SkParagraph and TextPainter + /// is disabled. + /// + /// Do not rely on this getter as it exists for migration purposes only and + /// will soon be removed. + static bool get shouldDisableRoundingHack { + return const bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') + || _roundingHackDisabledInDebugMode; + } + static bool _roundingHackDisabledInDebugMode = false; + + /// Only works in debug mode. Do not call this method as it is for migration + /// purposes only and will soon be removed. + static void setDisableRoundingHack(bool disableRoundingHack) { + // bool.hasEnvironment does not work in internal tests so an additional flag + // is needed for tests. + assert(() { + _roundingHackDisabledInDebugMode = disableRoundingHack; + return true; + }()); + } + /// The number of placeholders currently in the paragraph. int get placeholderCount; @@ -3132,11 +3154,12 @@ base class _NativeParagraphBuilder extends NativeFieldWrapperClass1 implements P style._fontSize ?? 0, style._height ?? 0, style._ellipsis ?? '', - _encodeLocale(style._locale) + _encodeLocale(style._locale), + !ParagraphBuilder.shouldDisableRoundingHack, ); } - @Native(symbol: 'ParagraphBuilder::Create') + @Native(symbol: 'ParagraphBuilder::Create') external void _constructor( Int32List encoded, ByteData? strutData, @@ -3145,7 +3168,8 @@ base class _NativeParagraphBuilder extends NativeFieldWrapperClass1 implements P double fontSize, double height, String ellipsis, - String locale); + String locale, + bool applyRoundingHack); @override int get placeholderCount => _placeholderCount; diff --git a/lib/ui/text/paragraph_builder.cc b/lib/ui/text/paragraph_builder.cc index 3857a10572d62..eb01511772d49 100644 --- a/lib/ui/text/paragraph_builder.cc +++ b/lib/ui/text/paragraph_builder.cc @@ -151,11 +151,12 @@ void ParagraphBuilder::Create(Dart_Handle wrapper, double fontSize, double height, const std::u16string& ellipsis, - const std::string& locale) { + const std::string& locale, + bool applyRoundingHack) { UIDartState::ThrowIfUIOperationsProhibited(); auto res = fml::MakeRefCounted( encoded_handle, strutData, fontFamily, strutFontFamilies, fontSize, - height, ellipsis, locale); + height, ellipsis, locale, applyRoundingHack); res->AssociateWithDartWrapper(wrapper); } @@ -230,7 +231,8 @@ ParagraphBuilder::ParagraphBuilder( double fontSize, double height, const std::u16string& ellipsis, - const std::string& locale) { + const std::string& locale, + bool applyRoundingHack) { int32_t mask = 0; txt::ParagraphStyle style; { @@ -291,6 +293,7 @@ ParagraphBuilder::ParagraphBuilder( if (mask & kPSLocaleMask) { style.locale = locale; } + style.apply_rounding_hack = applyRoundingHack; FontCollection& font_collection = UIDartState::Current() ->platform_configuration() diff --git a/lib/ui/text/paragraph_builder.h b/lib/ui/text/paragraph_builder.h index 0018f969da68f..6fc7c58ad97aa 100644 --- a/lib/ui/text/paragraph_builder.h +++ b/lib/ui/text/paragraph_builder.h @@ -30,7 +30,8 @@ class ParagraphBuilder : public RefCountedDartWrappable { double fontSize, double height, const std::u16string& ellipsis, - const std::string& locale); + const std::string& locale, + bool applyRoundingHack); ~ParagraphBuilder() override; @@ -76,7 +77,8 @@ class ParagraphBuilder : public RefCountedDartWrappable { double fontSize, double height, const std::u16string& ellipsis, - const std::string& locale); + const std::string& locale, + bool applyRoundingHack); std::unique_ptr m_paragraphBuilder; }; diff --git a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart index cf18d8664c8ae..c6c0d2ebe5c1a 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart @@ -2722,6 +2722,8 @@ extension SkParagraphStylePropertiesExtension on SkParagraphStyleProperties { @JS('replaceTabCharacters') external set _replaceTabCharacters(JSBoolean? bool); set replaceTabCharacters(bool? bool) => _replaceTabCharacters = bool?.toJS; + + external set applyRoundingHack(bool applyRoundingHack); } @JS() diff --git a/lib/web_ui/lib/src/engine/canvaskit/renderer.dart b/lib/web_ui/lib/src/engine/canvaskit/renderer.dart index 834446ef26d82..96da491f69af4 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/renderer.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/renderer.dart @@ -328,6 +328,7 @@ class CanvasKitRenderer implements Renderer { strutStyle: strutStyle, ellipsis: ellipsis, locale: locale, + applyRoundingHack: !ui.ParagraphBuilder.shouldDisableRoundingHack, ); @override diff --git a/lib/web_ui/lib/src/engine/canvaskit/text.dart b/lib/web_ui/lib/src/engine/canvaskit/text.dart index 6fe5a29a48db3..0948916b5d799 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/text.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/text.dart @@ -33,6 +33,7 @@ class CkParagraphStyle implements ui.ParagraphStyle { ui.StrutStyle? strutStyle, String? ellipsis, ui.Locale? locale, + bool applyRoundingHack = true, }) : skParagraphStyle = toSkParagraphStyle( textAlign, textDirection, @@ -46,6 +47,7 @@ class CkParagraphStyle implements ui.ParagraphStyle { strutStyle, ellipsis, locale, + applyRoundingHack, ), _fontFamily = _effectiveFontFamily(fontFamily), _fontSize = fontSize, @@ -145,6 +147,7 @@ class CkParagraphStyle implements ui.ParagraphStyle { ui.StrutStyle? strutStyle, String? ellipsis, ui.Locale? locale, + bool applyRoundingHack, ) { final SkParagraphStyleProperties properties = SkParagraphStyleProperties(); @@ -181,6 +184,7 @@ class CkParagraphStyle implements ui.ParagraphStyle { properties.replaceTabCharacters = true; properties.textStyle = toSkTextStyleProperties( fontFamily, fontSize, height, fontWeight, fontStyle); + properties.applyRoundingHack = applyRoundingHack; return canvasKit.ParagraphStyle(properties); } diff --git a/lib/web_ui/lib/text.dart b/lib/web_ui/lib/text.dart index 491966a0f88c2..0835cb9e2ced9 100644 --- a/lib/web_ui/lib/text.dart +++ b/lib/web_ui/lib/text.dart @@ -685,6 +685,19 @@ abstract class Paragraph { abstract class ParagraphBuilder { factory ParagraphBuilder(ParagraphStyle style) => engine.renderer.createParagraphBuilder(style); + + static bool get shouldDisableRoundingHack { + return const bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') + || _roundingHackDisabledInDebugMode; + } + static bool _roundingHackDisabledInDebugMode = false; + static void setDisableRoundingHack(bool disableRoundingHack) { + assert(() { + _roundingHackDisabledInDebugMode = disableRoundingHack; + return true; + }()); + } + void pushStyle(TextStyle style); void pop(); void addText(String text); diff --git a/lib/web_ui/test/canvaskit/text_test.dart b/lib/web_ui/test/canvaskit/text_test.dart index 8d4af676480fe..547155df75ac7 100644 --- a/lib/web_ui/test/canvaskit/text_test.dart +++ b/lib/web_ui/test/canvaskit/text_test.dart @@ -123,7 +123,51 @@ void testMain() { } }); }); + + test('applyRoundingHack works', () { + bool assertsEnabled = false; + assert(() { + assertsEnabled = true; + return true; + }()); + if (!assertsEnabled){ + return; + } + + const double fontSize = 1.25; + const String text = '12345'; + assert((fontSize * text.length).truncate() != fontSize * text.length); + final bool roundingHackWasDisabled = ui.ParagraphBuilder.shouldDisableRoundingHack; + ui.ParagraphBuilder.setDisableRoundingHack(true); + final ui.ParagraphBuilder builder = ui.ParagraphBuilder( + ui.ParagraphStyle(fontSize: fontSize, fontFamily: 'FlutterTest'), + ); + builder.addText(text); + final ui.Paragraph paragraph = builder.build() + ..layout(const ui.ParagraphConstraints(width: text.length * fontSize)); + + expect(paragraph.maxIntrinsicWidth, text.length * fontSize); + switch (paragraph.computeLineMetrics()) { + case [ui.LineMetrics(width: final double width)]: + expect(width, text.length * fontSize); + case final List metrics: + expect(metrics, hasLength(1)); + } + ui.ParagraphBuilder.setDisableRoundingHack(roundingHackWasDisabled); + }); + + test('rounding hack applied by default', () { + const double fontSize = 1.25; + const String text = '12345'; + assert((fontSize * text.length).truncate() != fontSize * text.length); + expect(ui.ParagraphBuilder.shouldDisableRoundingHack, isFalse); + final ui.ParagraphBuilder builder = ui.ParagraphBuilder(ui.ParagraphStyle(fontSize: fontSize, fontFamily: 'FlutterTest')); + builder.addText(text); + final ui.Paragraph paragraph = builder.build() + ..layout(const ui.ParagraphConstraints(width: text.length * fontSize)); + expect(paragraph.computeLineMetrics().length, greaterThan(1)); + }); + // TODO(hterkelsen): https://github.com/flutter/flutter/issues/71520 }, skip: isSafari || isFirefox); - } diff --git a/testing/dart/paragraph_test.dart b/testing/dart/paragraph_test.dart index 8c8e865770c59..eec1c571360c5 100644 --- a/testing/dart/paragraph_test.dart +++ b/testing/dart/paragraph_test.dart @@ -233,4 +233,45 @@ void main() { expect(callback, throwsStateError); } }); + + test('disableRoundingHack works in tests', () { + bool assertsEnabled = false; + assert(() { + assertsEnabled = true; + return true; + }()); + if (!assertsEnabled){ + return; + } + const double fontSize = 1.25; + const String text = '12345'; + assert((fontSize * text.length).truncate() != fontSize * text.length); + final bool roundingHackWasDisabled = ParagraphBuilder.shouldDisableRoundingHack; + ParagraphBuilder.setDisableRoundingHack(true); + final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle(fontSize: fontSize)); + builder.addText(text); + final Paragraph paragraph = builder.build() + ..layout(const ParagraphConstraints(width: text.length * fontSize)); + + expect(paragraph.maxIntrinsicWidth, text.length * fontSize); + switch (paragraph.computeLineMetrics()) { + case [LineMetrics(width: final double width)]: + expect(width, text.length * fontSize); + case final List metrics: + expect(metrics, hasLength(1)); + } + ParagraphBuilder.setDisableRoundingHack(roundingHackWasDisabled); + }); + + test('rounding hack applied by default', () { + const double fontSize = 1.25; + const String text = '12345'; + assert((fontSize * text.length).truncate() != fontSize * text.length); + expect(ParagraphBuilder.shouldDisableRoundingHack, isFalse); + final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle(fontSize: fontSize)); + builder.addText(text); + final Paragraph paragraph = builder.build() + ..layout(const ParagraphConstraints(width: text.length * fontSize)); + expect(paragraph.computeLineMetrics().length, greaterThan(1)); + }); } diff --git a/third_party/txt/src/skia/paragraph_builder_skia.cc b/third_party/txt/src/skia/paragraph_builder_skia.cc index 96d98f3f4c29f..641725ec6ccfb 100644 --- a/third_party/txt/src/skia/paragraph_builder_skia.cc +++ b/third_party/txt/src/skia/paragraph_builder_skia.cc @@ -138,6 +138,7 @@ skt::ParagraphStyle ParagraphBuilderSkia::TxtToSkia(const ParagraphStyle& txt) { skia.turnHintingOff(); skia.setReplaceTabCharacters(true); + skia.setApplyRoundingHack(txt.apply_rounding_hack); return skia; } diff --git a/third_party/txt/src/txt/paragraph_style.h b/third_party/txt/src/txt/paragraph_style.h index 7c3043b8ad899..e1915323a0bd3 100644 --- a/third_party/txt/src/txt/paragraph_style.h +++ b/third_party/txt/src/txt/paragraph_style.h @@ -95,6 +95,14 @@ class ParagraphStyle { std::u16string ellipsis; std::string locale; + // Temporary flag that indicates whether the Paragraph should report its + // metrics with rounding hacks applied. + // + // This flag currently defaults to true and will be flipped to false once the + // migration is complete. + // TODO(LongCatIsLooong): https://github.com/flutter/flutter/issues/31707 + bool apply_rounding_hack = true; + TextStyle GetTextStyle() const; bool unlimited_lines() const;