diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 82b2e704efec9..37f64f8c92b61 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -1867,21 +1867,17 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_canvas.da ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/color_filter.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/embedded_views_diff.dart + ../../../flutter/LICENSE -ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/font_fallback_data.dart + ../../../flutter/LICENSE -ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/fonts.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/image.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/image_filter.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/image_wasm_codecs.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/image_web_codecs.dart + ../../../flutter/LICENSE -ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/interval_tree.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer_scene_builder.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer_tree.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/mask_filter.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/n_way_canvas.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/native_memory.dart + ../../../flutter/LICENSE -ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/noto_font.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/painting.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/path.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/path_metrics.dart + ../../../flutter/LICENSE @@ -1905,6 +1901,8 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/dom.dart + ../../../flutter/L ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/embedder.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/engine_canvas.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/font_change_util.dart + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/font_fallback_data.dart + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/font_fallbacks.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/fonts.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/frame_reference.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/global_styles.dart + ../../../flutter/LICENSE @@ -1948,6 +1946,7 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/html/surface_stats.dart + ../ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/html/transform.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/html_image_codec.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/initialization.dart + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/interval_tree.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/js_interop/js_loader.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/js_interop/js_promise.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/js_interop/js_typed_data.dart + ../../../flutter/LICENSE @@ -1958,6 +1957,7 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/navigation.dart + ../../../fl ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/navigation/history.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/navigation/js_url_strategy.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/navigation/url_strategy.dart + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/noto_font.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/onscreen_logging.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/picture.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart + ../../../flutter/LICENSE @@ -2016,6 +2016,12 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_sk ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_skstring.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_surface.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/skwasm_module.dart + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/text/raw_line_metrics.dart + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/text/raw_paragraph.dart + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/text/raw_paragraph_builder.dart + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/text/raw_paragraph_style.dart + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/text/raw_strut_style.dart + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/text/raw_text_style.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/scene_builder.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/shaders.dart + ../../../flutter/LICENSE @@ -2075,6 +2081,12 @@ ORIGIN: ../../../flutter/lib/web_ui/skwasm/picture.cpp + ../../../flutter/LICENS ORIGIN: ../../../flutter/lib/web_ui/skwasm/shaders.cpp + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/skwasm/string.cpp + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/skwasm/surface.cpp + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/web_ui/skwasm/text/line_metrics.cpp + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/web_ui/skwasm/text/paragraph.cpp + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/web_ui/skwasm/text/paragraph_builder.cpp + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/web_ui/skwasm/text/paragraph_style.cpp + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/web_ui/skwasm/text/strut_style.cpp + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/web_ui/skwasm/text/text_style.cpp + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/skwasm/wrappers.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/runtime/dart_isolate.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/runtime/dart_isolate.h + ../../../flutter/LICENSE @@ -4470,21 +4482,17 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/color_filter.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/embedded_views_diff.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/font_fallback_data.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/fonts.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/image.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/image_filter.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/image_wasm_codecs.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/image_web_codecs.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/interval_tree.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer_scene_builder.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer_tree.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/mask_filter.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/n_way_canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/native_memory.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/noto_font.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/painting.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/path.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/path_metrics.dart @@ -4508,6 +4516,8 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/dom.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/embedder.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/engine_canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/font_change_util.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/font_fallback_data.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/font_fallbacks.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/fonts.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/frame_reference.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/global_styles.dart @@ -4551,6 +4561,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/surface_stats.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/transform.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html_image_codec.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/initialization.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/interval_tree.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/js_interop/js_loader.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/js_interop/js_promise.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/js_interop/js_typed_data.dart @@ -4561,6 +4572,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/navigation.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/navigation/history.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/navigation/js_url_strategy.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/navigation/url_strategy.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/noto_font.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/onscreen_logging.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/picture.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart @@ -4619,6 +4631,12 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_skda FILE: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_skstring.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_surface.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/skwasm_module.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/text/raw_line_metrics.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/text/raw_paragraph.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/text/raw_paragraph_builder.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/text/raw_paragraph_style.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/text/raw_strut_style.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/text/raw_text_style.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/scene_builder.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/shaders.dart @@ -4678,6 +4696,12 @@ FILE: ../../../flutter/lib/web_ui/skwasm/picture.cpp FILE: ../../../flutter/lib/web_ui/skwasm/shaders.cpp FILE: ../../../flutter/lib/web_ui/skwasm/string.cpp FILE: ../../../flutter/lib/web_ui/skwasm/surface.cpp +FILE: ../../../flutter/lib/web_ui/skwasm/text/line_metrics.cpp +FILE: ../../../flutter/lib/web_ui/skwasm/text/paragraph.cpp +FILE: ../../../flutter/lib/web_ui/skwasm/text/paragraph_builder.cpp +FILE: ../../../flutter/lib/web_ui/skwasm/text/paragraph_style.cpp +FILE: ../../../flutter/lib/web_ui/skwasm/text/strut_style.cpp +FILE: ../../../flutter/lib/web_ui/skwasm/text/text_style.cpp FILE: ../../../flutter/lib/web_ui/skwasm/wrappers.h FILE: ../../../flutter/runtime/dart_isolate.cc FILE: ../../../flutter/runtime/dart_isolate.h diff --git a/lib/web_ui/dev/roll_fallback_fonts.dart b/lib/web_ui/dev/roll_fallback_fonts.dart index 7cf9f5ad6eaa0..18db6efd9a7b6 100644 --- a/lib/web_ui/dev/roll_fallback_fonts.dart +++ b/lib/web_ui/dev/roll_fallback_fonts.dart @@ -182,7 +182,6 @@ class RollFallbackFontsCommand extends Command 'lib', 'src', 'engine', - 'canvaskit', 'font_fallback_data.dart', )); await fontDataFile.writeAsString(sb.toString()); diff --git a/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart index a6c832c188754..f49c42cce499d 100644 --- a/lib/web_ui/lib/src/engine.dart +++ b/lib/web_ui/lib/src/engine.dart @@ -26,21 +26,17 @@ export 'engine/canvaskit/canvaskit_canvas.dart'; export 'engine/canvaskit/color_filter.dart'; export 'engine/canvaskit/embedded_views.dart'; export 'engine/canvaskit/embedded_views_diff.dart'; -export 'engine/canvaskit/font_fallback_data.dart'; -export 'engine/canvaskit/font_fallbacks.dart'; export 'engine/canvaskit/fonts.dart'; export 'engine/canvaskit/image.dart'; export 'engine/canvaskit/image_filter.dart'; export 'engine/canvaskit/image_wasm_codecs.dart'; export 'engine/canvaskit/image_web_codecs.dart'; -export 'engine/canvaskit/interval_tree.dart'; export 'engine/canvaskit/layer.dart'; export 'engine/canvaskit/layer_scene_builder.dart'; export 'engine/canvaskit/layer_tree.dart'; export 'engine/canvaskit/mask_filter.dart'; export 'engine/canvaskit/n_way_canvas.dart'; export 'engine/canvaskit/native_memory.dart'; -export 'engine/canvaskit/noto_font.dart'; export 'engine/canvaskit/painting.dart'; export 'engine/canvaskit/path.dart'; export 'engine/canvaskit/path_metrics.dart'; @@ -63,6 +59,8 @@ export 'engine/dom.dart'; export 'engine/embedder.dart'; export 'engine/engine_canvas.dart'; export 'engine/font_change_util.dart'; +export 'engine/font_fallback_data.dart'; +export 'engine/font_fallbacks.dart'; export 'engine/fonts.dart'; export 'engine/frame_reference.dart'; export 'engine/global_styles.dart'; @@ -106,6 +104,7 @@ export 'engine/html/surface_stats.dart'; export 'engine/html/transform.dart'; export 'engine/html_image_codec.dart'; export 'engine/initialization.dart'; +export 'engine/interval_tree.dart'; export 'engine/js_interop/js_loader.dart'; export 'engine/js_interop/js_promise.dart'; export 'engine/js_interop/js_typed_data.dart'; @@ -115,6 +114,7 @@ export 'engine/mouse_cursor.dart'; export 'engine/navigation/history.dart'; export 'engine/navigation/js_url_strategy.dart'; export 'engine/navigation/url_strategy.dart'; +export 'engine/noto_font.dart'; export 'engine/onscreen_logging.dart'; export 'engine/picture.dart'; export 'engine/platform_dispatcher.dart'; diff --git a/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart b/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart deleted file mode 100644 index 63827fcaeb1b6..0000000000000 --- a/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart +++ /dev/null @@ -1,534 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:typed_data'; - -import 'package:ui/src/engine.dart'; - -/// Global static font fallback data. -class FontFallbackData { - - factory FontFallbackData() => - FontFallbackData._(getFallbackFontData(configuration.useColorEmoji)); - - FontFallbackData._(this.fallbackFonts) : - _notoSansSC = fallbackFonts.singleWhere((NotoFont font) => font.name == 'Noto Sans SC'), - _notoSansTC = fallbackFonts.singleWhere((NotoFont font) => font.name == 'Noto Sans TC'), - _notoSansHK = fallbackFonts.singleWhere((NotoFont font) => font.name == 'Noto Sans HK'), - _notoSansJP = fallbackFonts.singleWhere((NotoFont font) => font.name == 'Noto Sans JP'), - _notoSansKR = fallbackFonts.singleWhere((NotoFont font) => font.name == 'Noto Sans KR'), - _notoSymbols = fallbackFonts.singleWhere((NotoFont font) => font.name == 'Noto Sans Symbols'), - notoTree = createNotoFontTree(fallbackFonts); - - static FontFallbackData get instance => _instance; - static FontFallbackData _instance = FontFallbackData(); - - /// Resets the fallback font data. - /// - /// After calling this method fallback fonts will be loaded from scratch. - /// - /// Used for tests. - static void debugReset() { - _instance = FontFallbackData(); - notoDownloadQueue = FallbackFontDownloadQueue(); - } - - /// Code units that no known font has a glyph for. - final Set codeUnitsWithNoKnownFont = {}; - - /// Code units which are known to be covered by at least one fallback font. - final Set knownCoveredCodeUnits = {}; - - final List fallbackFonts; - - /// Index of all font families by code unit range. - final IntervalTree notoTree; - - final NotoFont _notoSansSC; - final NotoFont _notoSansTC; - final NotoFont _notoSansHK; - final NotoFont _notoSansJP; - final NotoFont _notoSansKR; - - final NotoFont _notoSymbols; - - static IntervalTree createNotoFontTree(List fallbackFonts) { - final Map> ranges = - >{}; - - for (final NotoFont font in fallbackFonts) { - // ignore: prefer_foreach - for (final CodeunitRange range in font.computeUnicodeRanges()) { - ranges.putIfAbsent(font, () => []).add(range); - } - } - - return IntervalTree.createFromRanges(ranges); - } - - /// Fallback fonts which have been registered and loaded. - final List registeredFallbackFonts = []; - - final List globalFontFallbacks = ['Roboto']; - - /// A list of code units to check against the global fallback fonts. - final Set _codeUnitsToCheckAgainstFallbackFonts = {}; - - /// This is [true] if we have scheduled a check for missing code units. - /// - /// We only do this once a frame, since checking if a font supports certain - /// code units is very expensive. - bool _scheduledCodeUnitCheck = false; - - /// Determines if the given [text] contains any code points which are not - /// supported by the current set of fonts. - void ensureFontsSupportText(String text, List fontFamilies) { - // TODO(hterkelsen): Make this faster for the common case where the text - // is supported by the given fonts. - if (debugDisableFontFallbacks) { - return; - } - - // If the text is ASCII, then skip this check. - bool isAscii = true; - for (int i = 0; i < text.length; i++) { - if (text.codeUnitAt(i) >= 160) { - isAscii = false; - break; - } - } - if (isAscii) { - return; - } - - // We have a cache of code units which are known to be covered by at least - // one of our fallback fonts, and a cache of code units which are known not - // to be covered by any fallback font. From the given text, construct a set - // of code units which need to be checked. - final Set runesToCheck = {}; - for (final int rune in text.runes) { - // Filter out code units which ASCII, known to be covered, or known not - // to be covered. - if (!(rune < 160 || - knownCoveredCodeUnits.contains(rune) || - codeUnitsWithNoKnownFont.contains(rune))) { - runesToCheck.add(rune); - } - } - if (runesToCheck.isEmpty) { - return; - } - - final List codeUnits = runesToCheck.toList(); - - final List fonts = []; - for (final String font in fontFamilies) { - final List? typefacesForFamily = - CanvasKitRenderer.instance.fontCollection.familyToFontMap[font]; - if (typefacesForFamily != null) { - fonts.addAll(typefacesForFamily); - } - } - final List codeUnitsSupported = - List.filled(codeUnits.length, false); - final String testString = String.fromCharCodes(codeUnits); - for (final SkFont font in fonts) { - final Uint16List glyphs = font.getGlyphIDs(testString); - assert(glyphs.length == codeUnitsSupported.length); - for (int i = 0; i < glyphs.length; i++) { - codeUnitsSupported[i] |= glyphs[i] != 0 || _isControlCode(codeUnits[i]); - } - } - - if (codeUnitsSupported.any((bool x) => !x)) { - final List missingCodeUnits = []; - for (int i = 0; i < codeUnitsSupported.length; i++) { - if (!codeUnitsSupported[i]) { - missingCodeUnits.add(codeUnits[i]); - } - } - _codeUnitsToCheckAgainstFallbackFonts.addAll(missingCodeUnits); - if (!_scheduledCodeUnitCheck) { - _scheduledCodeUnitCheck = true; - CanvasKitRenderer.instance.rasterizer.addPostFrameCallback(_ensureFallbackFonts); - } - } - } - - /// Returns [true] if [codepoint] is a Unicode control code. - bool _isControlCode(int codepoint) { - return codepoint < 32 || (codepoint > 127 && codepoint < 160); - } - - /// Checks the missing code units against the current set of fallback fonts - /// and starts downloading new fallback fonts if the current set can't cover - /// the code units. - void _ensureFallbackFonts() { - _scheduledCodeUnitCheck = false; - // We don't know if the remaining code units are covered by our fallback - // fonts. Check them and update the cache. - if (_codeUnitsToCheckAgainstFallbackFonts.isEmpty) { - return; - } - final List codeUnits = _codeUnitsToCheckAgainstFallbackFonts.toList(); - _codeUnitsToCheckAgainstFallbackFonts.clear(); - final List codeUnitsSupported = - List.filled(codeUnits.length, false); - final String testString = String.fromCharCodes(codeUnits); - - for (final String font in globalFontFallbacks) { - final List? fontsForFamily = - CanvasKitRenderer.instance.fontCollection.familyToFontMap[font]; - if (fontsForFamily == null) { - printWarning('A fallback font was registered but we ' - 'cannot retrieve the typeface for it.'); - continue; - } - for (final SkFont font in fontsForFamily) { - final Uint16List glyphs = font.getGlyphIDs(testString); - assert(glyphs.length == codeUnitsSupported.length); - for (int i = 0; i < glyphs.length; i++) { - final bool codeUnitSupported = glyphs[i] != 0; - if (codeUnitSupported) { - knownCoveredCodeUnits.add(codeUnits[i]); - } - codeUnitsSupported[i] |= - codeUnitSupported || _isControlCode(codeUnits[i]); - } - } - - // Once we've checked every typeface for this family, check to see if - // every code unit has been covered in order to avoid unnecessary checks. - bool keepGoing = false; - for (final bool supported in codeUnitsSupported) { - if (!supported) { - keepGoing = true; - break; - } - } - - if (!keepGoing) { - return; - } - } - - // If we reached here, then there are some code units which aren't covered - // by the global fallback fonts. Remove the ones which were covered and - // try to find fallback fonts which cover them. - for (int i = codeUnits.length - 1; i >= 0; i--) { - if (codeUnitsSupported[i]) { - codeUnits.removeAt(i); - } - } - findFontsForMissingCodeunits(codeUnits); - } - - void registerFallbackFont(String family, Uint8List bytes) { - final SkTypeface? typeface = - canvasKit.Typeface.MakeFreeTypeFaceFromData(bytes.buffer); - if (typeface == null) { - printWarning('Failed to parse fallback font $family as a font.'); - return; - } - // Insert emoji font before all other fallback fonts so we use the emoji - // whenever it's available. - registeredFallbackFonts.add(RegisteredFont(bytes, family, typeface)); - // Insert emoji font before all other fallback fonts so we use the emoji - // whenever it's available. - if (family == 'Noto Color Emoji' || family == 'Noto Emoji') { - if (globalFontFallbacks.first == 'Roboto') { - globalFontFallbacks.insert(1, family); - } else { - globalFontFallbacks.insert(0, family); - } - } else { - globalFontFallbacks.add(family); - } - } - - Future findFontsForMissingCodeunits(List codeUnits) async { - final FontFallbackData data = FontFallbackData.instance; - - Set fonts = {}; - final Set coveredCodeUnits = {}; - final Set missingCodeUnits = {}; - for (final int codeUnit in codeUnits) { - final List fontsForUnit = data.notoTree.intersections(codeUnit); - fonts.addAll(fontsForUnit); - if (fontsForUnit.isNotEmpty) { - coveredCodeUnits.add(codeUnit); - } else { - missingCodeUnits.add(codeUnit); - } - } - - // The call to `findMinimumFontsForCodeUnits` will remove all code units that - // were matched by `fonts` from `unmatchedCodeUnits`. - final Set unmatchedCodeUnits = Set.from(coveredCodeUnits); - fonts = findMinimumFontsForCodeUnits(unmatchedCodeUnits, fonts); - - fonts.forEach(notoDownloadQueue.add); - - // We looked through the Noto font tree and didn't find any font families - // covering some code units. - if (missingCodeUnits.isNotEmpty || unmatchedCodeUnits.isNotEmpty) { - if (!notoDownloadQueue.isPending) { - printWarning('Could not find a set of Noto fonts to display all missing ' - 'characters. Please add a font asset for the missing characters.' - ' See: https://flutter.dev/docs/cookbook/design/fonts'); - data.codeUnitsWithNoKnownFont.addAll(missingCodeUnits); - } - } - } - - /// Finds the minimum set of fonts which covers all of the [codeUnits]. - /// - /// Removes all code units covered by [fonts] from [codeUnits]. The code - /// units remaining in the [codeUnits] set after calling this function do not - /// have a font that covers them and can be omitted next time to avoid - /// searching for fonts unnecessarily. - /// - /// Since set cover is NP-complete, we approximate using a greedy algorithm - /// which finds the font which covers the most code units. If multiple CJK - /// fonts match the same number of code units, we choose one based on the user's - /// locale. - Set findMinimumFontsForCodeUnits( - Set codeUnits, Set fonts) { - assert(fonts.isNotEmpty || codeUnits.isEmpty); - final Set minimumFonts = {}; - final List bestFonts = []; - - final String language = domWindow.navigator.language; - - while (codeUnits.isNotEmpty) { - int maxCodeUnitsCovered = 0; - bestFonts.clear(); - for (final NotoFont font in fonts) { - int codeUnitsCovered = 0; - for (final int codeUnit in codeUnits) { - if (font.contains(codeUnit)) { - codeUnitsCovered++; - } - } - if (codeUnitsCovered > maxCodeUnitsCovered) { - bestFonts.clear(); - bestFonts.add(font); - maxCodeUnitsCovered = codeUnitsCovered; - } else if (codeUnitsCovered == maxCodeUnitsCovered) { - bestFonts.add(font); - } - } - if (maxCodeUnitsCovered == 0) { - // Fonts cannot cover remaining unmatched characters. - break; - } - // If the list of best fonts are all CJK fonts, choose the best one based - // on locale. Otherwise just choose the first font. - NotoFont bestFont = bestFonts.first; - if (bestFonts.length > 1) { - if (bestFonts.every((NotoFont font) => - font == _notoSansSC || - font == _notoSansTC || - font == _notoSansHK || - font == _notoSansJP || - font == _notoSansKR - )) { - if (language == 'zh-Hans' || - language == 'zh-CN' || - language == 'zh-SG' || - language == 'zh-MY') { - if (bestFonts.contains(_notoSansSC)) { - bestFont = _notoSansSC; - } - } else if (language == 'zh-Hant' || - language == 'zh-TW' || - language == 'zh-MO') { - if (bestFonts.contains(_notoSansTC)) { - bestFont = _notoSansTC; - } - } else if (language == 'zh-HK') { - if (bestFonts.contains(_notoSansHK)) { - bestFont = _notoSansHK; - } - } else if (language == 'ja') { - if (bestFonts.contains(_notoSansJP)) { - bestFont = _notoSansJP; - } - } else if (language == 'ko') { - if (bestFonts.contains(_notoSansKR)) { - bestFont = _notoSansKR; - } - } else if (bestFonts.contains(_notoSansSC)) { - bestFont = _notoSansSC; - } - } else { - // To be predictable, if there is a tie for best font, choose a font - // from this list first, then just choose the first font. - if (bestFonts.contains(_notoSymbols)) { - bestFont = _notoSymbols; - } else if (bestFonts.contains(_notoSansSC)) { - bestFont = _notoSansSC; - } - } - } - codeUnits.removeWhere((int codeUnit) { - return bestFont.contains(codeUnit); - }); - minimumFonts.add(bestFont); - } - return minimumFonts; - } -} - -class FallbackFontDownloadQueue { - NotoDownloader downloader = NotoDownloader(); - - final Set downloadedFonts = {}; - final Map pendingFonts = {}; - - bool get isPending => pendingFonts.isNotEmpty || _fontsLoading != null; - - Future? _fontsLoading; - bool get debugIsLoadingFonts => _fontsLoading != null; - - Future debugWhenIdle() async { - if (assertionsEnabled) { - await Future.delayed(Duration.zero); - while (isPending) { - if (_fontsLoading != null) { - await _fontsLoading; - } - if (pendingFonts.isNotEmpty) { - await Future.delayed(const Duration(milliseconds: 100)); - if (pendingFonts.isEmpty) { - await Future.delayed(const Duration(milliseconds: 100)); - } - } - } - } else { - throw UnimplementedError(); - } - } - - void add(NotoFont font) { - if (downloadedFonts.contains(font) || - pendingFonts.containsKey(font.url)) { - return; - } - final bool firstInBatch = pendingFonts.isEmpty; - pendingFonts[font.url] = font; - if (firstInBatch) { - Timer.run(startDownloads); - } - } - - Future startDownloads() async { - final Map> downloads = >{}; - final Map downloadedData = {}; - for (final NotoFont font in pendingFonts.values) { - downloads[font.url] = Future(() async { - ByteBuffer buffer; - try { - buffer = await downloader.downloadAsBytes(font.url, - debugDescription: font.name); - } catch (e) { - pendingFonts.remove(font.url); - printWarning('Failed to load font ${font.name} at ${font.url}'); - printWarning(e.toString()); - return; - } - downloadedFonts.add(font); - downloadedData[font.url] = buffer.asUint8List(); - }); - } - - await Future.wait(downloads.values); - - // Register fallback fonts in a predictable order. Otherwise, the fonts - // change their precedence depending on the download order causing - // visual differences between app reloads. - final List downloadOrder = - (downloadedData.keys.toList()..sort()).reversed.toList(); - for (final String url in downloadOrder) { - final NotoFont font = pendingFonts.remove(url)!; - final Uint8List bytes = downloadedData[url]!; - FontFallbackData.instance.registerFallbackFont(font.name, bytes); - if (pendingFonts.isEmpty) { - (renderer.fontCollection as SkiaFontCollection).registerDownloadedFonts(); - sendFontChangeMessage(); - } - } - - if (pendingFonts.isNotEmpty) { - await startDownloads(); - } - } -} - -class NotoDownloader { - int get debugActiveDownloadCount => _debugActiveDownloadCount; - int _debugActiveDownloadCount = 0; - - static const String _defaultFallbackFontsUrlPrefix = 'https://fonts.gstatic.com/s/'; - String? fallbackFontUrlPrefixOverride; - String get fallbackFontUrlPrefix => fallbackFontUrlPrefixOverride ?? _defaultFallbackFontsUrlPrefix; - - /// Returns a future that resolves when there are no pending downloads. - /// - /// Useful in tests to make sure that fonts are loaded before working with - /// text. - Future debugWhenIdle() async { - if (assertionsEnabled) { - // Some downloads begin asynchronously in a microtask or in a Timer.run. - // Let those run before waiting for downloads to finish. - await Future.delayed(Duration.zero); - while (_debugActiveDownloadCount > 0) { - await Future.delayed(const Duration(milliseconds: 100)); - // If we started with a non-zero count and hit zero while waiting, wait a - // little more to make sure another download doesn't get chained after - // the last one (e.g. font file download after font CSS download). - if (_debugActiveDownloadCount == 0) { - await Future.delayed(const Duration(milliseconds: 100)); - } - } - } else { - throw UnimplementedError(); - } - } - - /// Downloads the [url] and returns it as a [ByteBuffer]. - /// - /// Override this for testing. - Future downloadAsBytes(String url, {String? debugDescription}) async { - if (assertionsEnabled) { - _debugActiveDownloadCount += 1; - } - final Future data = httpFetchByteBuffer('$fallbackFontUrlPrefix$url'); - if (assertionsEnabled) { - unawaited(data.whenComplete(() { - _debugActiveDownloadCount -= 1; - })); - } - return data; - } - - /// Downloads the [url] and returns is as a [String]. - /// - /// Override this for testing. - Future downloadAsString(String url, {String? debugDescription}) async { - if (assertionsEnabled) { - _debugActiveDownloadCount += 1; - } - final Future data = httpFetchText('$fallbackFontUrlPrefix$url'); - if (assertionsEnabled) { - unawaited(data.whenComplete(() { - _debugActiveDownloadCount -= 1; - })); - } - return data; - } -} - -FallbackFontDownloadQueue notoDownloadQueue = FallbackFontDownloadQueue(); diff --git a/lib/web_ui/lib/src/engine/canvaskit/fonts.dart b/lib/web_ui/lib/src/engine/canvaskit/fonts.dart index 5f4fbabb0a723..79ee8fc4aafbf 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/fonts.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/fonts.dart @@ -19,6 +19,10 @@ const String _robotoUrl = class SkiaFontCollection implements FlutterFontCollection { final Set _downloadedFontFamilies = {}; + @override + late FontFallbackManager fontFallbackManager = + FontFallbackManager(SkiaFallbackRegistry(this)); + /// Fonts that started the download process, but are not yet registered. /// /// /// Once downloaded successfully, this map is cleared and the resulting @@ -26,6 +30,7 @@ class SkiaFontCollection implements FlutterFontCollection { final List _unregisteredFonts = []; final List _registeredFonts = []; + final List registeredFallbackFonts = []; /// Returns fonts that have been downloaded, registered, and parsed. /// @@ -59,8 +64,7 @@ class SkiaFontCollection implements FlutterFontCollection { .add(SkFont(font.typeface)); } - for (final RegisteredFont font - in FontFallbackData.instance.registeredFallbackFonts) { + for (final RegisteredFont font in registeredFallbackFonts) { _fontProvider!.registerFont(font.bytes, font.family); familyToFontMap .putIfAbsent(font.family, () => []) @@ -108,8 +112,8 @@ class SkiaFontCollection implements FlutterFontCollection { } } - /// We need a default fallback font for CanvasKit, in order to - /// avoid crashing while laying out text with an unregistered font. We chose + /// We need a default fallback font for CanvasKit, in order to avoid + /// crashing while laying out text with an unregistered font. We chose /// Roboto to match Android. if (!loadedRoboto) { // Download Roboto and add it to the font buffers. @@ -216,6 +220,12 @@ class SkiaFontCollection implements FlutterFontCollection { @override void clear() {} + + @override + void debugResetFallbackFonts() { + fontFallbackManager = FontFallbackManager(SkiaFallbackRegistry(this)); + registeredFallbackFonts.clear(); + } } /// Represents a font that has been registered. @@ -254,3 +264,57 @@ class FontDownloadResult { final UnregisteredFont? font; final FontLoadError? error; } + +class SkiaFallbackRegistry implements FallbackFontRegistry { + SkiaFallbackRegistry(this.fontCollection); + + SkiaFontCollection fontCollection; + + @override + List getMissingCodePoints(List codeUnits, List fontFamilies) { + final List fonts = []; + for (final String font in fontFamilies) { + final List? typefacesForFamily = fontCollection.familyToFontMap[font]; + if (typefacesForFamily != null) { + fonts.addAll(typefacesForFamily); + } + } + final List codePointsSupported = + List.filled(codeUnits.length, false); + final String testString = String.fromCharCodes(codeUnits); + for (final SkFont font in fonts) { + final Uint16List glyphs = font.getGlyphIDs(testString); + assert(glyphs.length == codePointsSupported.length); + for (int i = 0; i < glyphs.length; i++) { + codePointsSupported[i] |= glyphs[i] != 0; + } + } + + final List missingCodeUnits = []; + for (int i = 0; i < codePointsSupported.length; i++) { + if (!codePointsSupported[i]) { + missingCodeUnits.add(codeUnits[i]); + } + } + return missingCodeUnits; + } + + @override + Future loadFallbackFont(String familyName, String url) async { + final ByteBuffer buffer = await httpFetchByteBuffer(url); + final SkTypeface? typeface = + canvasKit.Typeface.MakeFreeTypeFaceFromData(buffer); + if (typeface == null) { + printWarning('Failed to parse fallback font $familyName as a font.'); + return; + } + fontCollection.registeredFallbackFonts.add( + RegisteredFont(buffer.asUint8List(), familyName, typeface) + ); + } + + @override + void updateFallbackFontFamilies(List families) { + fontCollection.registerDownloadedFonts(); + } +} diff --git a/lib/web_ui/lib/src/engine/canvaskit/renderer.dart b/lib/web_ui/lib/src/engine/canvaskit/renderer.dart index cec642f014188..45f4df4b70f95 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/renderer.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/renderer.dart @@ -6,28 +6,9 @@ import 'dart:async'; import 'dart:math' as math; import 'dart:typed_data'; +import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; -import '../dom.dart'; -import '../embedder.dart'; -import '../html_image_codec.dart'; -import '../initialization.dart'; -import '../profiler.dart'; -import '../renderer.dart'; -import 'canvaskit_api.dart'; -import 'canvaskit_canvas.dart'; -import 'fonts.dart'; -import 'image.dart'; -import 'image_filter.dart'; -import 'layer_scene_builder.dart'; -import 'painting.dart'; -import 'path.dart'; -import 'picture_recorder.dart'; -import 'rasterizer.dart'; -import 'shader.dart'; -import 'text.dart'; -import 'vertices.dart'; - enum CanvasKitVariant { /// The appropriate variant is chosen based on the browser. /// @@ -406,4 +387,27 @@ class CanvasKitRenderer implements Renderer { return CkFragmentProgram.fromBytes(assetKey, data.buffer.asUint8List()); }); } + + @override + ui.LineMetrics createLineMetrics({ + required bool hardBreak, + required double ascent, + required double descent, + required double unscaledAscent, + required double height, + required double width, + required double left, + required double baseline, + required int lineNumber + }) => EngineLineMetrics( + hardBreak: hardBreak, + ascent: ascent, + descent: descent, + unscaledAscent: unscaledAscent, + height: height, + width: width, + left: left, + baseline: baseline, + lineNumber: lineNumber + ); } diff --git a/lib/web_ui/lib/src/engine/canvaskit/text.dart b/lib/web_ui/lib/src/engine/canvaskit/text.dart index 815012dcd84ef..fa2dec9a3f999 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/text.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/text.dart @@ -5,17 +5,9 @@ import 'dart:typed_data'; import 'package:meta/meta.dart'; +import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; -import '../util.dart'; -import 'canvaskit_api.dart'; -import 'font_fallbacks.dart'; -import 'native_memory.dart'; -import 'painting.dart'; -import 'renderer.dart'; -import 'text_fragmenter.dart'; -import 'util.dart'; - final bool _ckRequiresClientICU = canvasKit.ParagraphBuilder.RequiresClientICU(); final List _testFonts = ['FlutterTest', 'Ahem']; @@ -866,7 +858,7 @@ class CkParagraphBuilder implements ui.ParagraphBuilder { if (style.fontFamilyFallback != null) { fontFamilies.addAll(style.fontFamilyFallback!); } - FontFallbackData.instance.ensureFontsSupportText(text, fontFamilies); + renderer.fontCollection.fontFallbackManager!.ensureFontsSupportText(text, fontFamilies); _paragraphBuilder.addText(text); } @@ -975,6 +967,8 @@ List _getEffectiveFontFamilies(String? fontFamily, !fontFamilyFallback.every((String font) => fontFamily == font)) { fontFamilies.addAll(fontFamilyFallback); } - fontFamilies.addAll(FontFallbackData.instance.globalFontFallbacks); + fontFamilies.addAll( + renderer.fontCollection.fontFallbackManager!.globalFontFallbacks + ); return fontFamilies; } diff --git a/lib/web_ui/lib/src/engine/canvaskit/font_fallback_data.dart b/lib/web_ui/lib/src/engine/font_fallback_data.dart similarity index 100% rename from lib/web_ui/lib/src/engine/canvaskit/font_fallback_data.dart rename to lib/web_ui/lib/src/engine/font_fallback_data.dart diff --git a/lib/web_ui/lib/src/engine/font_fallbacks.dart b/lib/web_ui/lib/src/engine/font_fallbacks.dart new file mode 100644 index 0000000000000..27b7f2269be36 --- /dev/null +++ b/lib/web_ui/lib/src/engine/font_fallbacks.dart @@ -0,0 +1,380 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:ui/src/engine.dart'; + +abstract class FallbackFontRegistry { + List getMissingCodePoints(List codePoints, List fontFamilies); + Future loadFallbackFont(String familyName, String string); + void updateFallbackFontFamilies(List families); +} + +/// Global static font fallback data. +class FontFallbackManager { + factory FontFallbackManager(FallbackFontRegistry registry) => + FontFallbackManager._( + registry, + getFallbackFontData(configuration.useColorEmoji) + ); + + FontFallbackManager._(this.registry, this.fallbackFonts) : + _notoSansSC = fallbackFonts.singleWhere((NotoFont font) => font.name == 'Noto Sans SC'), + _notoSansTC = fallbackFonts.singleWhere((NotoFont font) => font.name == 'Noto Sans TC'), + _notoSansHK = fallbackFonts.singleWhere((NotoFont font) => font.name == 'Noto Sans HK'), + _notoSansJP = fallbackFonts.singleWhere((NotoFont font) => font.name == 'Noto Sans JP'), + _notoSansKR = fallbackFonts.singleWhere((NotoFont font) => font.name == 'Noto Sans KR'), + _notoSymbols = fallbackFonts.singleWhere((NotoFont font) => font.name == 'Noto Sans Symbols'), + notoTree = createNotoFontTree(fallbackFonts) { + downloadQueue = FallbackFontDownloadQueue(this); + } + + final FallbackFontRegistry registry; + + late final FallbackFontDownloadQueue downloadQueue; + + /// Code points that no known font has a glyph for. + final Set codePointsWithNoKnownFont = {}; + + /// Code points which are known to be covered by at least one fallback font. + final Set knownCoveredCodePoints = {}; + + final List fallbackFonts; + + /// Index of all font families by code point range. + final IntervalTree notoTree; + + final NotoFont _notoSansSC; + final NotoFont _notoSansTC; + final NotoFont _notoSansHK; + final NotoFont _notoSansJP; + final NotoFont _notoSansKR; + + final NotoFont _notoSymbols; + + Future _idleFuture = Future.value(); + + static IntervalTree createNotoFontTree(List fallbackFonts) { + final Map> ranges = + >{}; + + for (final NotoFont font in fallbackFonts) { + final List fontRanges = + ranges.putIfAbsent(font, () => []); + fontRanges.addAll(font.computeUnicodeRanges()); + } + + return IntervalTree.createFromRanges(ranges); + } + + final List globalFontFallbacks = ['Roboto']; + + /// A list of code points to check against the global fallback fonts. + final Set _codePointsToCheckAgainstFallbackFonts = {}; + + /// This is [true] if we have scheduled a check for missing code points. + /// + /// We only do this once a frame, since checking if a font supports certain + /// code points is very expensive. + bool _scheduledCodePointCheck = false; + + Future debugWhenIdle() { + if (assertionsEnabled) { + return _idleFuture; + } else { + throw UnimplementedError(); + } + } + + /// Determines if the given [text] contains any code points which are not + /// supported by the current set of fonts. + void ensureFontsSupportText(String text, List fontFamilies) { + // TODO(hterkelsen): Make this faster for the common case where the text + // is supported by the given fonts. + if (debugDisableFontFallbacks) { + return; + } + + // We have a cache of code points which are known to be covered by at least + // one of our fallback fonts, and a cache of code points which are known not + // to be covered by any fallback font. From the given text, construct a set + // of code points which need to be checked. + final Set runesToCheck = {}; + for (final int rune in text.runes) { + // Filter out code points that don't need checking. + if (!(rune < 160 || // ASCII and Unicode control points. + knownCoveredCodePoints.contains(rune) || // Points we've already covered + codePointsWithNoKnownFont.contains(rune)) // Points that don't have a fallback font + ) { + runesToCheck.add(rune); + } + } + if (runesToCheck.isEmpty) { + return; + } + + final List codePoints = runesToCheck.toList(); + final List missingCodePoints = + registry.getMissingCodePoints(codePoints, fontFamilies); + + if (missingCodePoints.isNotEmpty) { + _codePointsToCheckAgainstFallbackFonts.addAll(missingCodePoints); + if (!_scheduledCodePointCheck) { + _scheduledCodePointCheck = true; + _idleFuture = Future.delayed(Duration.zero, () async { + _ensureFallbackFonts(); + _scheduledCodePointCheck = false; + await downloadQueue.waitForIdle(); + }); + } + } + } + + /// Checks the missing code points against the current set of fallback fonts + /// and starts downloading new fallback fonts if the current set can't cover + /// the code points. + void _ensureFallbackFonts() { + _scheduledCodePointCheck = false; + // We don't know if the remaining code points are covered by our fallback + // fonts. Check them and update the cache. + if (_codePointsToCheckAgainstFallbackFonts.isEmpty) { + return; + } + final List codePoints = _codePointsToCheckAgainstFallbackFonts.toList(); + _codePointsToCheckAgainstFallbackFonts.clear(); + final List missingCodePoints = registry.getMissingCodePoints(codePoints, globalFontFallbacks); + findFontsForMissingCodePoints(missingCodePoints); + } + + void registerFallbackFont(String family) { + // Insert emoji font before all other fallback fonts so we use the emoji + // whenever it's available. + if (family == 'Noto Color Emoji' || family == 'Noto Emoji') { + if (globalFontFallbacks.first == 'Roboto') { + globalFontFallbacks.insert(1, family); + } else { + globalFontFallbacks.insert(0, family); + } + } else { + globalFontFallbacks.add(family); + } + } + + void findFontsForMissingCodePoints(List codePoints) { + Set fonts = {}; + final Set coveredCodePoints = {}; + final Set missingCodePoints = {}; + for (final int codePoint in codePoints) { + final List fontsForPoint = notoTree.intersections(codePoint); + fonts.addAll(fontsForPoint); + if (fontsForPoint.isNotEmpty) { + coveredCodePoints.add(codePoint); + } else { + missingCodePoints.add(codePoint); + } + } + + // The call to `findMinimumFontsForCodePoints` will remove all code points that + // were matched by `fonts` from `unmatchedCodePoints`. + final Set unmatchedCodePoints = Set.from(coveredCodePoints); + fonts = findMinimumFontsForCodePoints(unmatchedCodePoints, fonts); + + fonts.forEach(downloadQueue.add); + + // We looked through the Noto font tree and didn't find any font families + // covering some code points. + if (missingCodePoints.isNotEmpty || unmatchedCodePoints.isNotEmpty) { + if (!downloadQueue.isPending) { + printWarning('Could not find a set of Noto fonts to display all missing ' + 'characters. Please add a font asset for the missing characters.' + ' See: https://flutter.dev/docs/cookbook/design/fonts'); + codePointsWithNoKnownFont.addAll(missingCodePoints); + } + } + } + + /// Finds the minimum set of fonts which covers all of the [codePoints]. + /// + /// Removes all code points covered by [fonts] from [codePoints]. The code + /// points remaining in the [codePoints] set after calling this function do not + /// have a font that covers them and can be omitted next time to avoid + /// searching for fonts unnecessarily. + /// + /// Since set cover is NP-complete, we approximate using a greedy algorithm + /// which finds the font which covers the most code points. If multiple CJK + /// fonts match the same number of code points, we choose one based on the user's + /// locale. + Set findMinimumFontsForCodePoints( + Set codePoints, Set fonts) { + assert(fonts.isNotEmpty || codePoints.isEmpty); + final Set minimumFonts = {}; + final List bestFonts = []; + + final String language = domWindow.navigator.language; + + while (codePoints.isNotEmpty) { + int maxCodePointsCovered = 0; + bestFonts.clear(); + for (final NotoFont font in fonts) { + int codePointsCovered = 0; + for (final int codePoint in codePoints) { + if (font.contains(codePoint)) { + codePointsCovered++; + } + } + if (codePointsCovered > maxCodePointsCovered) { + bestFonts.clear(); + bestFonts.add(font); + maxCodePointsCovered = codePointsCovered; + } else if (codePointsCovered == maxCodePointsCovered) { + bestFonts.add(font); + } + } + if (maxCodePointsCovered == 0) { + // Fonts cannot cover remaining unmatched characters. + break; + } + // If the list of best fonts are all CJK fonts, choose the best one based + // on locale. Otherwise just choose the first font. + NotoFont bestFont = bestFonts.first; + if (bestFonts.length > 1) { + if (bestFonts.every((NotoFont font) => + font == _notoSansSC || + font == _notoSansTC || + font == _notoSansHK || + font == _notoSansJP || + font == _notoSansKR + )) { + if (language == 'zh-Hans' || + language == 'zh-CN' || + language == 'zh-SG' || + language == 'zh-MY') { + if (bestFonts.contains(_notoSansSC)) { + bestFont = _notoSansSC; + } + } else if (language == 'zh-Hant' || + language == 'zh-TW' || + language == 'zh-MO') { + if (bestFonts.contains(_notoSansTC)) { + bestFont = _notoSansTC; + } + } else if (language == 'zh-HK') { + if (bestFonts.contains(_notoSansHK)) { + bestFont = _notoSansHK; + } + } else if (language == 'ja') { + if (bestFonts.contains(_notoSansJP)) { + bestFont = _notoSansJP; + } + } else if (language == 'ko') { + if (bestFonts.contains(_notoSansKR)) { + bestFont = _notoSansKR; + } + } else if (bestFonts.contains(_notoSansSC)) { + bestFont = _notoSansSC; + } + } else { + // To be predictable, if there is a tie for best font, choose a font + // from this list first, then just choose the first font. + if (bestFonts.contains(_notoSymbols)) { + bestFont = _notoSymbols; + } else if (bestFonts.contains(_notoSansSC)) { + bestFont = _notoSansSC; + } + } + } + codePoints.removeWhere((int codePoint) { + return bestFont.contains(codePoint); + }); + minimumFonts.add(bestFont); + } + return minimumFonts; + } +} + +class FallbackFontDownloadQueue { + FallbackFontDownloadQueue(this.fallbackManager); + + final FontFallbackManager fallbackManager; + + static const String _defaultFallbackFontsUrlPrefix = 'https://fonts.gstatic.com/s/'; + String? fallbackFontUrlPrefixOverride; + String get fallbackFontUrlPrefix => fallbackFontUrlPrefixOverride ?? _defaultFallbackFontsUrlPrefix; + + final Set downloadedFonts = {}; + final Map pendingFonts = {}; + + bool get isPending => pendingFonts.isNotEmpty; + + void Function(String family)? debugOnLoadFontFamily; + + Completer? _idleCompleter; + + Future waitForIdle() { + if (_idleCompleter == null) { + // We're already idle + return Future.value(); + } else { + return _idleCompleter!.future; + } + } + + void add(NotoFont font) { + if (downloadedFonts.contains(font) || + pendingFonts.containsKey(font.url)) { + return; + } + final bool firstInBatch = pendingFonts.isEmpty; + pendingFonts[font.url] = font; + _idleCompleter ??= Completer(); + if (firstInBatch) { + Timer.run(startDownloads); + } + } + + Future startDownloads() async { + final Map> downloads = >{}; + final List downloadedFontFamilies = []; + for (final NotoFont font in pendingFonts.values) { + downloads[font.url] = Future(() async { + try { + final String url = '$fallbackFontUrlPrefix${font.url}'; + debugOnLoadFontFamily?.call(font.name); + await fallbackManager.registry.loadFallbackFont(font.name, url); + downloadedFontFamilies.add(font.url); + } catch (e) { + pendingFonts.remove(font.url); + printWarning('Failed to load font ${font.name} at ${font.url}'); + printWarning(e.toString()); + return; + } + downloadedFonts.add(font); + }); + } + + await Future.wait(downloads.values); + + // Register fallback fonts in a predictable order. Otherwise, the fonts + // change their precedence depending on the download order causing + // visual differences between app reloads. + downloadedFontFamilies.sort(); + for (final String url in downloadedFontFamilies) { + final NotoFont font = pendingFonts.remove(url)!; + fallbackManager.registerFallbackFont(font.name); + } + + if (pendingFonts.isEmpty) { + fallbackManager.registry.updateFallbackFontFamilies( + fallbackManager.globalFontFallbacks + ); + sendFontChangeMessage(); + final Completer idleCompleter = _idleCompleter!; + _idleCompleter = null; + idleCompleter.complete(); + } else { + await startDownloads(); + } + } +} diff --git a/lib/web_ui/lib/src/engine/fonts.dart b/lib/web_ui/lib/src/engine/fonts.dart index 0e401d6fa84fb..07c4154891f2b 100644 --- a/lib/web_ui/lib/src/engine/fonts.dart +++ b/lib/web_ui/lib/src/engine/fonts.dart @@ -124,6 +124,14 @@ abstract class FlutterFontCollection { /// Completes when fonts from FontManifest.json have been loaded. Future loadAssetFonts(FontManifest manifest); + // The font fallback manager for this font collection. HTML renderer doesn't + // have a font fallback manager and just relies on the browser to fall back + // properly. + FontFallbackManager? get fontFallbackManager; + + // Reset the state of font fallbacks. Only to be used in testing. + void debugResetFallbackFonts(); + // Unregisters all fonts. void clear(); } diff --git a/lib/web_ui/lib/src/engine/html/renderer.dart b/lib/web_ui/lib/src/engine/html/renderer.dart index 21de71dd1baee..728c8bc367ce2 100644 --- a/lib/web_ui/lib/src/engine/html/renderer.dart +++ b/lib/web_ui/lib/src/engine/html/renderer.dart @@ -339,4 +339,27 @@ class HtmlRenderer implements Renderer { Future createFragmentProgram(String assetKey) { return Future.value(HtmlFragmentProgram()); } + + @override + ui.LineMetrics createLineMetrics({ + required bool hardBreak, + required double ascent, + required double descent, + required double unscaledAscent, + required double height, + required double width, + required double left, + required double baseline, + required int lineNumber + }) => EngineLineMetrics( + hardBreak: hardBreak, + ascent: ascent, + descent: descent, + unscaledAscent: unscaledAscent, + height: height, + width: width, + left: left, + baseline: baseline, + lineNumber: lineNumber + ); } diff --git a/lib/web_ui/lib/src/engine/canvaskit/interval_tree.dart b/lib/web_ui/lib/src/engine/interval_tree.dart similarity index 94% rename from lib/web_ui/lib/src/engine/canvaskit/interval_tree.dart rename to lib/web_ui/lib/src/engine/interval_tree.dart index 59657e7032ee3..f037ae277815f 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/interval_tree.dart +++ b/lib/web_ui/lib/src/engine/interval_tree.dart @@ -4,7 +4,7 @@ import 'dart:math' as math; -import 'noto_font.dart' show CodeunitRange; +import 'noto_font.dart' show CodePointRange; /// A tree which stores a set of intervals that can be queried for intersection. class IntervalTree { @@ -14,12 +14,12 @@ class IntervalTree { /// /// When the interval tree is queried, it will return a list of [T]s which /// have a range which contains the point. - factory IntervalTree.createFromRanges(Map> rangesMap) { + factory IntervalTree.createFromRanges(Map> rangesMap) { assert(rangesMap.isNotEmpty); // Get a list of all the ranges ordered by start index. final List> intervals = >[]; - rangesMap.forEach((T key, List rangeList) { - for (final CodeunitRange range in rangeList) { + rangesMap.forEach((T key, List rangeList) { + for (final CodePointRange range in rangeList) { intervals.add(IntervalTreeNode(key, range.start, range.end)); } }); diff --git a/lib/web_ui/lib/src/engine/canvaskit/noto_font.dart b/lib/web_ui/lib/src/engine/noto_font.dart similarity index 81% rename from lib/web_ui/lib/src/engine/canvaskit/noto_font.dart rename to lib/web_ui/lib/src/engine/noto_font.dart index f58c2dd0efbfa..737e758f4895b 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/noto_font.dart +++ b/lib/web_ui/lib/src/engine/noto_font.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import '../text/unicode_range.dart'; +import 'text/unicode_range.dart'; class NotoFont { NotoFont(this.name, this.url, this._packedRanges); @@ -11,9 +11,9 @@ class NotoFont { final String url; final String _packedRanges; // A sorted list of Unicode ranges. - late final List _ranges = _unpackFontRange(_packedRanges); + late final List _ranges = _unpackFontRange(_packedRanges); - List computeUnicodeRanges() => _ranges; + List computeUnicodeRanges() => _ranges; // Returns `true` if this font has a glyph for the given [codeunit]. bool contains(int codeUnit) { @@ -23,7 +23,7 @@ class NotoFont { int max = _ranges.length - 1; while (min <= max) { final int mid = (min + max) ~/ 2; - final CodeunitRange range = _ranges[mid]; + final CodePointRange range = _ranges[mid]; if (range.start > codeUnit) { max = mid - 1; } else { @@ -38,8 +38,8 @@ class NotoFont { } } -class CodeunitRange { - const CodeunitRange(this.start, this.end); +class CodePointRange { + const CodePointRange(this.start, this.end); final int start; final int end; @@ -50,10 +50,10 @@ class CodeunitRange { @override bool operator ==(Object other) { - if (other is! CodeunitRange) { + if (other is! CodePointRange) { return false; } - final CodeunitRange range = other; + final CodePointRange range = other; return range.start == start && range.end == end; } @@ -73,15 +73,15 @@ class MutableInt { int value; } -List _unpackFontRange(String packedRange) { +List _unpackFontRange(String packedRange) { final MutableInt i = MutableInt(0); - final List ranges = []; + final List ranges = []; while (i.value < packedRange.length) { final int rangeStart = _consumeInt36(packedRange, i, until: _kCharPipe); final int rangeLength = _consumeInt36(packedRange, i, until: _kCharSemicolon); final int rangeEnd = rangeStart + rangeLength; - ranges.add(CodeunitRange(rangeStart, rangeEnd)); + ranges.add(CodePointRange(rangeStart, rangeEnd)); } return ranges; } diff --git a/lib/web_ui/lib/src/engine/renderer.dart b/lib/web_ui/lib/src/engine/renderer.dart index 5dbaaad432bfb..20fd055b1c13d 100644 --- a/lib/web_ui/lib/src/engine/renderer.dart +++ b/lib/web_ui/lib/src/engine/renderer.dart @@ -161,6 +161,18 @@ abstract class Renderer { ui.Path copyPath(ui.Path src); ui.Path combinePaths(ui.PathOperation op, ui.Path path1, ui.Path path2); + ui.LineMetrics createLineMetrics({ + required bool hardBreak, + required double ascent, + required double descent, + required double unscaledAscent, + required double height, + required double width, + required double left, + required double baseline, + required int lineNumber, + }); + ui.TextStyle createTextStyle({ required ui.Color? color, required ui.TextDecoration? decoration, diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl.dart index 13d223e640888..c1c5eade1687e 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl.dart +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl.dart @@ -31,6 +31,12 @@ export 'skwasm_impl/raw/raw_skdata.dart'; export 'skwasm_impl/raw/raw_skstring.dart'; export 'skwasm_impl/raw/raw_surface.dart'; export 'skwasm_impl/raw/skwasm_module.dart'; +export 'skwasm_impl/raw/text/raw_line_metrics.dart'; +export 'skwasm_impl/raw/text/raw_paragraph.dart'; +export 'skwasm_impl/raw/text/raw_paragraph_builder.dart'; +export 'skwasm_impl/raw/text/raw_paragraph_style.dart'; +export 'skwasm_impl/raw/text/raw_strut_style.dart'; +export 'skwasm_impl/raw/text/raw_text_style.dart'; export 'skwasm_impl/renderer.dart'; export 'skwasm_impl/scene_builder.dart'; export 'skwasm_impl/shaders.dart'; diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/canvas.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/canvas.dart index e886b6985d6de..ddf1326effef2 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/canvas.dart +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/canvas.dart @@ -205,8 +205,13 @@ class SkwasmCanvas implements ui.Canvas { } @override - void drawParagraph(ui.Paragraph uiParagraph, ui.Offset offset) { - // TODO(jacksongardner): implement this + void drawParagraph(ui.Paragraph paragraph, ui.Offset offset) { + canvasDrawParagraph( + _handle, + (paragraph as SkwasmParagraph).handle, + offset.dx, + offset.dy, + ); } @override diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/font_collection.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/font_collection.dart index f99c01f484e65..979da4ac2aff4 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/font_collection.dart +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/font_collection.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:convert'; import 'dart:ffi'; import 'dart:js_interop'; @@ -12,15 +11,59 @@ import 'dart:typed_data'; import 'package:ui/src/engine.dart'; import 'package:ui/src/engine/skwasm/skwasm_impl.dart'; +// This URL was found by using the Google Fonts Developer API to find the URL +// for Roboto. The API warns that this URL is not stable. In order to update +// this, list out all of the fonts and find the URL for the regular +// Roboto font. The API reference is here: +// https://developers.google.com/fonts/docs/developer_api +const String _robotoUrl = + 'https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Me5WZLCzYlKw.ttf'; + +class SkwasmTypeface { + SkwasmTypeface(SkDataHandle data) : handle = typefaceCreate(data); + + bool _isDisposed = false; + + void dispose() { + if (!_isDisposed) { + _isDisposed = true; + typefaceDispose(handle); + } + } + + TypefaceHandle handle; +} + class SkwasmFontCollection implements FlutterFontCollection { - SkwasmFontCollection() : _handle = fontCollectionCreate(); + SkwasmFontCollection() { + setDefaultFontFamilies(['Roboto']); + } + + FontCollectionHandle handle = fontCollectionCreate(); + TextStyleHandle defaultTextStyle = textStyleCreate(); + final Map> registeredTypefaces = >{}; - FontCollectionHandle _handle; + void setDefaultFontFamilies(List families) => withStackScope((StackScope scope) { + final Pointer familyPointers = + scope.allocPointerArray(families.length).cast(); + for (int i = 0; i < families.length; i++) { + familyPointers[i] = skStringFromDartString(families[i]); + } + textStyleClearFontFamilies(defaultTextStyle); + textStyleAddFontFamilies(defaultTextStyle, familyPointers, families.length); + for (int i = 0; i < families.length; i++) { + skStringFree(familyPointers[i]); + } + }); + + @override + late FontFallbackManager fontFallbackManager = + FontFallbackManager(SkwasmFallbackRegistry(this)); @override void clear() { - fontCollectionDispose(_handle); - _handle = fontCollectionCreate(); + fontCollectionDispose(handle); + handle = fontCollectionCreate(); } @override @@ -29,20 +72,19 @@ class SkwasmFontCollection implements FlutterFontCollection { final List loadedFonts = []; final Map fontFailures = {}; - // We can't restore the pointers directly due to a bug in dart2wasm - // https://github.com/dart-lang/sdk/issues/52142 - final List familyHandles = []; + /// We need a default fallback font for Skwasm, in order to avoid crashing + /// while laying out text with an unregistered font. We chose Roboto to + /// match Android. + if (!manifest.families.any((FontFamily family) => family.name == 'Roboto')) { + manifest.families.add( + FontFamily('Roboto', [FontAsset(_robotoUrl, {})]) + ); + } + for (final FontFamily family in manifest.families) { - final List rawUtf8Bytes = utf8.encode(family.name); - final SkStringHandle stringHandle = skStringAllocate(rawUtf8Bytes.length); - final Pointer stringDataPointer = skStringGetData(stringHandle); - for (int i = 0; i < rawUtf8Bytes.length; i++) { - stringDataPointer[i] = rawUtf8Bytes[i]; - } - familyHandles.add(stringHandle.address); for (final FontAsset fontAsset in family.fontAssets) { fontFutures.add(() async { - final FontLoadError? error = await _downloadFontAsset(fontAsset, stringHandle); + final FontLoadError? error = await _downloadFontAsset(fontAsset, family.name); if (error == null) { loadedFonts.add(fontAsset.asset); } else { @@ -51,17 +93,12 @@ class SkwasmFontCollection implements FlutterFontCollection { }()); } } - await Future.wait(fontFutures); - // Wait until all the downloading and registering is complete before - // freeing the handles to the family name strings. - familyHandles - .map((int address) => SkStringHandle.fromAddress(address)) - .forEach(skStringFree); + await Future.wait(fontFutures); return AssetFontsResult(loadedFonts, fontFailures); } - Future _downloadFontAsset(FontAsset asset, SkStringHandle familyNameHandle) async { + Future _downloadFontAsset(FontAsset asset, String family) async { final HttpFetchResponse response; try { response = await assetManager.loadAsset(asset.asset); @@ -84,12 +121,45 @@ class SkwasmFontCollection implements FlutterFontCollection { wasmMemory.set(chunk, dataAddress.toJS); dataAddress += chunk.length.toDart.toInt(); } - final bool result = fontCollectionRegisterFont(_handle, fontData, familyNameHandle); + final SkwasmTypeface typeface = SkwasmTypeface(fontData); skDataDispose(fontData); - if (!result) { + if (typeface.handle != nullptr) { + final SkStringHandle familyNameHandle = skStringFromDartString(family); + fontCollectionRegisterTypeface(handle, typeface.handle, familyNameHandle); + registeredTypefaces.putIfAbsent(family, () => []).add(typeface); + skStringFree(familyNameHandle); + return null; + } else { return FontInvalidDataError(assetManager.getAssetUrl(asset.asset)); } - return null; + } + + Future loadFontFromUrl(String familyName, String url) async { + final HttpFetchResponse response = await httpFetch(url); + int length = 0; + final List chunks = []; + await response.read((JSUint8Array1 chunk) { + length += chunk.length.toDart.toInt(); + chunks.add(chunk); + }); + final SkDataHandle fontData = skDataCreate(length); + int dataAddress = skDataGetPointer(fontData).cast().address; + final JSUint8Array1 wasmMemory = createUint8ArrayFromBuffer(skwasmInstance.wasmMemory.buffer); + for (final JSUint8Array1 chunk in chunks) { + wasmMemory.set(chunk, dataAddress.toJS); + dataAddress += chunk.length.toDart.toInt(); + } + + final SkwasmTypeface typeface = SkwasmTypeface(fontData); + skDataDispose(fontData); + if (typeface.handle == nullptr) { + return false; + } + final SkStringHandle familyNameHandle = skStringFromDartString(familyName); + fontCollectionRegisterTypeface(handle, typeface.handle, familyNameHandle); + registeredTypefaces.putIfAbsent(familyName, () => []).add(typeface); + skStringFree(familyNameHandle); + return true; } @override @@ -99,20 +169,64 @@ class SkwasmFontCollection implements FlutterFontCollection { for (int i = 0; i < list.length; i++) { dataPointer[i] = list[i]; } - bool success; + final SkwasmTypeface typeface = SkwasmTypeface(dataHandle); + skDataDispose(dataHandle); + if (typeface.handle == nullptr) { + return false; + } + if (fontFamily != null) { - final List rawUtf8Bytes = utf8.encode(fontFamily); - final SkStringHandle stringHandle = skStringAllocate(rawUtf8Bytes.length); - final Pointer stringDataPointer = skStringGetData(stringHandle); - for (int i = 0; i < rawUtf8Bytes.length; i++) { - stringDataPointer[i] = rawUtf8Bytes[i]; - } - success = fontCollectionRegisterFont(_handle, dataHandle, stringHandle); - skStringFree(stringHandle); + final SkStringHandle familyHandle = skStringFromDartString(fontFamily); + fontCollectionRegisterTypeface(handle, typeface.handle, familyHandle); + skStringFree(familyHandle); } else { - success = fontCollectionRegisterFont(_handle, dataHandle, nullptr); + fontCollectionRegisterTypeface(handle, typeface.handle, nullptr); } - skDataDispose(dataHandle); - return success; + return true; } + + @override + void debugResetFallbackFonts() { + setDefaultFontFamilies([]); + fontFallbackManager = FontFallbackManager(SkwasmFallbackRegistry(this)); + } +} + +class SkwasmFallbackRegistry implements FallbackFontRegistry { + SkwasmFallbackRegistry(this.fontCollection); + + final SkwasmFontCollection fontCollection; + + @override + List getMissingCodePoints(List codePoints, List fontFamilies) + => withStackScope((StackScope scope) { + final List typefaces = fontFamilies + .map((String family) => fontCollection.registeredTypefaces[family]) + .fold(const Iterable.empty(), + (Iterable accumulated, List? typefaces) => + typefaces == null ? accumulated : accumulated.followedBy(typefaces)).toList(); + final Pointer typefaceBuffer = scope.allocPointerArray(typefaces.length).cast(); + for (int i = 0; i < typefaces.length; i++) { + typefaceBuffer[i] = typefaces[i].handle; + } + final Pointer codePointBuffer = scope.allocInt32Array(codePoints.length); + for (int i = 0; i < codePoints.length; i++) { + codePointBuffer[i] = codePoints[i]; + } + final int missingCodePointCount = typefacesFilterCoveredCodePoints( + typefaceBuffer, + typefaces.length, + codePointBuffer, + codePoints.length + ); + return List.generate(missingCodePointCount, (int index) => codePointBuffer[index]); + }); + + @override + Future loadFallbackFont(String familyName, String url) => + fontCollection.loadFontFromUrl(familyName, url); + + @override + void updateFallbackFontFamilies(List families) => + fontCollection.setDefaultFontFamilies(families); } diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/paragraph.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/paragraph.dart index a76aa6c275985..4d0d027aa409a 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/paragraph.dart +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/paragraph.dart @@ -2,11 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// ignore_for_file: avoid_unused_constructor_parameters +import 'dart:ffi'; +import 'package:ui/src/engine.dart'; +import 'package:ui/src/engine/skwasm/skwasm_impl.dart'; import 'package:ui/ui.dart' as ui; -// TODO(jacksongardner): implement everything in this file class SkwasmLineMetrics implements ui.LineMetrics { factory SkwasmLineMetrics({ required bool hardBreak, @@ -18,148 +19,529 @@ class SkwasmLineMetrics implements ui.LineMetrics { required double left, required double baseline, required int lineNumber, - }) { - throw UnimplementedError(); - } + }) => SkwasmLineMetrics._(lineMetricsCreate( + hardBreak, + ascent, + descent, + unscaledAscent, + height, + width, + left, + baseline, + lineNumber, + )); + + SkwasmLineMetrics._(this.handle); + + final LineMetricsHandle handle; + bool _isDisposed = false; @override - bool get hardBreak { - throw UnimplementedError(); - } + bool get hardBreak => lineMetricsGetHardBreak(handle); @override - double get ascent { - throw UnimplementedError(); - } + double get ascent => lineMetricsGetAscent(handle); @override - double get descent { - throw UnimplementedError(); - } + double get descent => lineMetricsGetDescent(handle); @override - double get unscaledAscent { - throw UnimplementedError(); - } + double get unscaledAscent => lineMetricsGetUnscaledAscent(handle); @override - double get height { - throw UnimplementedError(); - } + double get height => lineMetricsGetHeight(handle); @override - double get width { - throw UnimplementedError(); - } + double get width => lineMetricsGetWidth(handle); @override - double get left { - throw UnimplementedError(); - } + double get left => lineMetricsGetLeft(handle); @override - double get baseline { - throw UnimplementedError(); - } + double get baseline => lineMetricsGetBaseline(handle); @override - int get lineNumber { - throw UnimplementedError(); + int get lineNumber => lineMetricsGetLineNumber(handle); + + void dispose() { + if (_isDisposed) { + lineMetricsDispose(handle); + _isDisposed = true; + } } } class SkwasmParagraph implements ui.Paragraph { + SkwasmParagraph(this.handle); + + ParagraphHandle handle; + bool _isDisposed = false; + @override - double get width { - return 0.0; - } + double get width => paragraphGetWidth(handle); @override - double get height { - return 0.0; - } + double get height => paragraphGetHeight(handle); @override - double get longestLine { - return 0.0; - } + double get longestLine => paragraphGetLongestLine(handle); @override - double get minIntrinsicWidth { - return 0.0; - } + double get minIntrinsicWidth => paragraphGetMinIntrinsicWidth(handle); @override - double get maxIntrinsicWidth { - return 0.0; - } + double get maxIntrinsicWidth => paragraphGetMaxIntrinsicWidth(handle); @override - double get alphabeticBaseline { - return 0.0; - } + double get alphabeticBaseline => paragraphGetAlphabeticBaseline(handle); @override - double get ideographicBaseline { - return 0.0; - } + double get ideographicBaseline => paragraphGetIdeographicBaseline(handle); @override - bool get didExceedMaxLines { - return false; - } + bool get didExceedMaxLines => paragraphGetDidExceedMaxLines(handle); @override - void layout(ui.ParagraphConstraints constraints) { + void layout(ui.ParagraphConstraints constraints) => + paragraphLayout(handle, constraints.width); + + List _convertTextBoxList(TextBoxListHandle listHandle) { + final int length = textBoxListGetLength(listHandle); + return withStackScope((StackScope scope) { + final RawRect tempRect = scope.allocFloatArray(4); + return List.generate(length, (int index) { + final int textDirectionIndex = + textBoxListGetBoxAtIndex(listHandle, index, tempRect); + return ui.TextBox.fromLTRBD( + tempRect[0], + tempRect[1], + tempRect[2], + tempRect[3], + ui.TextDirection.values[textDirectionIndex], + ); + }); + }); } @override - List getBoxesForRange(int start, int end, - {ui.BoxHeightStyle boxHeightStyle = ui.BoxHeightStyle.tight, - ui.BoxWidthStyle boxWidthStyle = ui.BoxWidthStyle.tight}) { - return []; + List getBoxesForRange( + int start, + int end, { + ui.BoxHeightStyle boxHeightStyle = ui.BoxHeightStyle.tight, + ui.BoxWidthStyle boxWidthStyle = ui.BoxWidthStyle.tight + }) { + final TextBoxListHandle listHandle = paragraphGetBoxesForRange( + handle, + start, + end, + boxHeightStyle.index, + boxWidthStyle.index + ); + final List boxes = _convertTextBoxList(listHandle); + textBoxListDispose(listHandle); + return boxes; } @override - ui.TextPosition getPositionForOffset(ui.Offset offset) { - return const ui.TextPosition(offset: 0); - } + ui.TextPosition getPositionForOffset(ui.Offset offset) => withStackScope((StackScope scope) { + final Pointer outAffinity = scope.allocInt32Array(1); + final int position = paragraphGetPositionForOffset( + handle, + offset.dx, + offset.dy, + outAffinity + ); + return ui.TextPosition( + offset: position, + affinity: ui.TextAffinity.values[outAffinity[0]], + ); + }); @override - ui.TextRange getWordBoundary(ui.TextPosition position) { - return const ui.TextRange(start: 0, end: 0); - } + ui.TextRange getWordBoundary(ui.TextPosition position) => withStackScope((StackScope scope) { + final Pointer outRange = scope.allocInt32Array(2); + paragraphGetWordBoundary(handle, position.offset, outRange); + return ui.TextRange(start: outRange[0], end: outRange[1]); + }); @override ui.TextRange getLineBoundary(ui.TextPosition position) { - return const ui.TextRange(start: 0, end: 0); + final int lineNumber = paragraphGetLineNumberAt(handle, position.offset); + final LineMetricsHandle metricsHandle = + paragraphGetLineMetricsAtIndex(handle, lineNumber); + final ui.TextRange range = ui.TextRange( + start: lineMetricsGetStartIndex(metricsHandle), + end: lineMetricsGetEndIndex(metricsHandle), + ); + lineMetricsDispose(metricsHandle); + return range; } @override List getBoxesForPlaceholders() { - return []; + final TextBoxListHandle listHandle = paragraphGetBoxesForPlaceholders(handle); + final List boxes = _convertTextBoxList(listHandle); + textBoxListDispose(listHandle); + return boxes; } @override List computeLineMetrics() { - return []; + final int lineCount = paragraphGetLineCount(handle); + return List.generate(lineCount, + (int index) => SkwasmLineMetrics._(paragraphGetLineMetricsAtIndex(handle, index)) + ); } @override - bool get debugDisposed => false; + bool get debugDisposed => _isDisposed; @override void dispose() { + if (!_isDisposed) { + paragraphDispose(handle); + _isDisposed = true; + } } } -class SkwasmParagraphStyle implements ui.ParagraphStyle { +void withScopedFontList( + List fontFamilies, + void Function(Pointer, int) callback) { + withStackScope((StackScope scope) { + final Pointer familiesPtr = + scope.allocPointerArray(fontFamilies.length).cast(); + int nativeIndex = 0; + for (int i = 0; i < fontFamilies.length; i++) { + familiesPtr[nativeIndex] = skStringFromDartString(fontFamilies[i]); + nativeIndex++; + } + callback(familiesPtr, fontFamilies.length); + for (int i = 0; i < fontFamilies.length; i++) { + skStringFree(familiesPtr[i]); + } + }); } class SkwasmTextStyle implements ui.TextStyle { + SkwasmTextStyle({ + this.color, + this.decoration, + this.decorationColor, + this.decorationStyle, + this.decorationThickness, + this.fontWeight, + this.fontStyle, + this.textBaseline, + this.fontFamily, + this.fontFamilyFallback, + this.fontSize, + this.letterSpacing, + this.wordSpacing, + this.height, + this.leadingDistribution, + this.locale, + this.background, + this.foreground, + this.shadows, + this.fontFeatures, + this.fontVariations, + }); + + void applyToHandle(TextStyleHandle handle) { + if (color != null) { + textStyleSetColor(handle, color!.value); + } + if (decoration != null) { + textStyleSetDecoration(handle, decoration!.maskValue); + } + if (decorationColor != null) { + textStyleSetDecorationColor(handle, decorationColor!.value); + } + if (decorationStyle != null) { + textStyleSetDecorationStyle(handle, decorationStyle!.index); + } + if (decorationThickness != null) { + textStyleSetDecorationThickness(handle, decorationThickness!); + } + if (fontWeight != null || fontStyle != null) { + textStyleSetFontStyle( + handle, + (fontWeight ?? ui.FontWeight.normal).value, + (fontStyle ?? ui.FontStyle.normal).index + ); + } + if (textBaseline != null) { + textStyleSetTextBaseline(handle, textBaseline!.index); + } + + final List effectiveFontFamilies = fontFamilies; + if (effectiveFontFamilies.isNotEmpty) { + withScopedFontList(effectiveFontFamilies, + (Pointer families, int count) => + textStyleAddFontFamilies(handle, families, count)); + } + + if (fontSize != null) { + textStyleSetFontSize(handle, fontSize!); + } + if (letterSpacing != null) { + textStyleSetLetterSpacing(handle, letterSpacing!); + } + if (wordSpacing != null) { + textStyleSetWordSpacing(handle, wordSpacing!); + } + if (height != null) { + textStyleSetHeight(handle, height!); + } + if (leadingDistribution != null) { + textStyleSetHalfLeading( + handle, + leadingDistribution == ui.TextLeadingDistribution.even + ); + } + if (locale != null) { + final SkStringHandle localeHandle = + skStringFromDartString(locale!.toLanguageTag()); + textStyleSetLocale(handle, localeHandle); + skStringFree(localeHandle); + } + if (background != null) { + textStyleSetBackground(handle, (background! as SkwasmPaint).handle); + } + if (foreground != null) { + textStyleSetForeground(handle, (foreground! as SkwasmPaint).handle); + } + if (shadows != null) { + for (final ui.Shadow shadow in shadows!) { + textStyleAddShadow( + handle, + shadow.color.value, + shadow.offset.dx, + shadow.offset.dy, + shadow.blurSigma, + ); + } + } + if (fontFeatures != null) { + for (final ui.FontFeature feature in fontFeatures!) { + final SkStringHandle featureName = skStringFromDartString(feature.feature); + textStyleAddFontFeature(handle, featureName, feature.value); + skStringFree(featureName); + } + } + + if (fontVariations != null && fontVariations!.isNotEmpty) { + final int variationCount = fontVariations!.length; + withStackScope((StackScope scope) { + final Pointer axisBuffer = scope.allocUint32Array(variationCount); + final Pointer valueBuffer = scope.allocFloatArray(variationCount); + for (int i = 0; i < variationCount; i++) { + final ui.FontVariation variation = fontVariations![i]; + final String axis = variation.axis; + assert(axis.length == 4); // 4 byte code + final int axisNumber = + axis.codeUnitAt(0) << 24 | + axis.codeUnitAt(1) << 16 | + axis.codeUnitAt(2) << 8 | + axis.codeUnitAt(3); + axisBuffer[i] = axisNumber; + valueBuffer[i] = variation.value; + } + textStyleSetFontVariations(handle, axisBuffer, valueBuffer, variationCount); + }); + } + } + + List get fontFamilies => [ + if (fontFamily != null) fontFamily!, + if (fontFamilyFallback != null) ...fontFamilyFallback!, + ]; + + final ui.Color? color; + final ui.TextDecoration? decoration; + final ui.Color? decorationColor; + final ui.TextDecorationStyle? decorationStyle; + final double? decorationThickness; + final ui.FontWeight? fontWeight; + final ui.FontStyle? fontStyle; + final ui.TextBaseline? textBaseline; + final String? fontFamily; + final List? fontFamilyFallback; + final double? fontSize; + final double? letterSpacing; + final double? wordSpacing; + final double? height; + final ui.TextLeadingDistribution? leadingDistribution; + final ui.Locale? locale; + final ui.Paint? background; + final ui.Paint? foreground; + final List? shadows; + final List? fontFeatures; + final List? fontVariations; +} + +class SkwasmStrutStyle implements ui.StrutStyle { + factory SkwasmStrutStyle({ + String? fontFamily, + List? fontFamilyFallback, + double? fontSize, + double? height, + ui.TextLeadingDistribution? leadingDistribution, + double? leading, + ui.FontWeight? fontWeight, + ui.FontStyle? fontStyle, + bool? forceStrutHeight, + }) { + final StrutStyleHandle handle = strutStyleCreate(); + if (fontFamily != null || fontFamilyFallback != null) { + final List fontFamilies = [ + if (fontFamily != null) fontFamily, + if (fontFamilyFallback != null) ...fontFamilyFallback, + ]; + if (fontFamilies.isNotEmpty) { + withScopedFontList(fontFamilies, + (Pointer families, int count) => + strutStyleSetFontFamilies(handle, families, count)); + } + } + if (fontSize != null) { + strutStyleSetFontSize(handle, fontSize); + } + if (height != null) { + strutStyleSetHeight(handle, height); + } + if (leadingDistribution != null) { + strutStyleSetHalfLeading( + handle, + leadingDistribution == ui.TextLeadingDistribution.even); + } + if (leading != null) { + strutStyleSetLeading(handle, leading); + } + if (fontWeight != null || fontStyle != null) { + fontWeight ??= ui.FontWeight.normal; + fontStyle ??= ui.FontStyle.normal; + strutStyleSetFontStyle(handle, fontWeight.value, fontStyle.index); + } + if (forceStrutHeight != null) { + strutStyleSetForceStrutHeight(handle, forceStrutHeight); + } + return SkwasmStrutStyle._(handle); + } + + SkwasmStrutStyle._(this.handle); + + final StrutStyleHandle handle; +} + +class SkwasmParagraphStyle implements ui.ParagraphStyle { + factory SkwasmParagraphStyle({ + ui.TextAlign? textAlign, + ui.TextDirection? textDirection, + int? maxLines, + String? fontFamily, + double? fontSize, + double? height, + ui.TextHeightBehavior? textHeightBehavior, + ui.FontWeight? fontWeight, + ui.FontStyle? fontStyle, + ui.StrutStyle? strutStyle, + String? ellipsis, + ui.Locale? locale, + }) { + final ParagraphStyleHandle handle = paragraphStyleCreate(); + if (textAlign != null) { + paragraphStyleSetTextAlign(handle, textAlign.index); + } + if (textDirection != null) { + paragraphStyleSetTextDirection(handle, textDirection.index); + } + if (maxLines != null) { + paragraphStyleSetMaxLines(handle, maxLines); + } + if (height != null) { + paragraphStyleSetHeight(handle, height); + } + if (textHeightBehavior != null) { + paragraphStyleSetTextHeightBehavior( + handle, + textHeightBehavior.applyHeightToFirstAscent, + textHeightBehavior.applyHeightToLastDescent, + ); + } + if (ellipsis != null) { + final SkStringHandle ellipsisHandle = skStringFromDartString(ellipsis); + paragraphStyleSetEllipsis(handle, ellipsisHandle); + skStringFree(ellipsisHandle); + } + if (strutStyle != null) { + strutStyle as SkwasmStrutStyle; + paragraphStyleSetStrutStyle(handle, strutStyle.handle); + } + final TextStyleHandle textStyleHandle = textStyleCopy( + (renderer.fontCollection as SkwasmFontCollection).defaultTextStyle, + ); + if (fontFamily != null) { + withScopedFontList([fontFamily], + (Pointer families, int count) => + textStyleAddFontFamilies(textStyleHandle, families, count)); + } + if (fontSize != null) { + textStyleSetFontSize(textStyleHandle, fontSize); + } + if (fontWeight != null || fontStyle != null) { + fontWeight ??= ui.FontWeight.normal; + fontStyle ??= ui.FontStyle.normal; + textStyleSetFontStyle(textStyleHandle, fontWeight.value, fontStyle.index); + } + if (textHeightBehavior != null) { + textStyleSetHalfLeading( + textStyleHandle, + textHeightBehavior.leadingDistribution == ui.TextLeadingDistribution.even, + ); + } + if (locale != null) { + final SkStringHandle localeHandle = + skStringFromDartString(locale.toLanguageTag()); + textStyleSetLocale(textStyleHandle, localeHandle); + skStringFree(localeHandle); + } + paragraphStyleSetTextStyle(handle, textStyleHandle); + return SkwasmParagraphStyle._(handle, textStyleHandle, fontFamily); + } + + SkwasmParagraphStyle._(this.handle, this.textStyleHandle, this.defaultFontFamily); + + final ParagraphStyleHandle handle; + final TextStyleHandle textStyleHandle; + final String? defaultFontFamily; +} + +class _TextStyleStackEntry { + _TextStyleStackEntry(this.style, this.handle); + + SkwasmTextStyle style; + TextStyleHandle handle; } class SkwasmParagraphBuilder implements ui.ParagraphBuilder { + factory SkwasmParagraphBuilder( + SkwasmParagraphStyle style, + SkwasmFontCollection collection, + ) => SkwasmParagraphBuilder._(paragraphBuilderCreate( + style.handle, + collection.handle, + ), style); + + SkwasmParagraphBuilder._(this.handle, this.style); + final ParagraphBuilderHandle handle; + final SkwasmParagraphStyle style; + final List<_TextStyleStackEntry> textStyleStack = <_TextStyleStackEntry>[]; + + @override + List placeholderScales = []; + @override void addPlaceholder( double width, @@ -169,28 +551,78 @@ class SkwasmParagraphBuilder implements ui.ParagraphBuilder { double? baselineOffset, ui.TextBaseline? baseline }) { + paragraphBuilderAddPlaceholder( + handle, + width * scale, + height * scale, + alignment.index, + (baselineOffset ?? height) * scale, + (baseline ?? ui.TextBaseline.alphabetic).index, + ); + placeholderScales.add(scale); + } + + List _getEffectiveFonts() { + final List fallbackFonts = renderer.fontCollection.fontFallbackManager!.globalFontFallbacks; + final List? currentFonts = + textStyleStack.isEmpty ? null : textStyleStack.last.style.fontFamilies; + if (currentFonts != null) { + return [ + ...currentFonts, + ...fallbackFonts, + ]; + } else if (style.defaultFontFamily != null) { + return [ + style.defaultFontFamily!, + ...fallbackFonts, + ]; + } else { + return fallbackFonts; + } } @override void addText(String text) { + renderer.fontCollection.fontFallbackManager!.ensureFontsSupportText( + text, _getEffectiveFonts() + ); + final SkString16Handle stringHandle = skString16FromDartString(text); + paragraphBuilderAddText(handle, stringHandle); + skString16Free(stringHandle); } @override ui.Paragraph build() { - return SkwasmParagraph(); + return SkwasmParagraph(paragraphBuilderBuild(handle)); } @override - int get placeholderCount => 0; - - @override - List get placeholderScales => []; + int get placeholderCount => placeholderScales.length; @override void pop() { + final TextStyleHandle textStyleHandle = textStyleStack.removeLast().handle; + textStyleDispose(textStyleHandle); + paragraphBuilderPop(handle); } @override - void pushStyle(ui.TextStyle style) { + void pushStyle(ui.TextStyle textStyle) { + textStyle as SkwasmTextStyle; + TextStyleHandle sourceStyleHandle = nullptr; + if (textStyleStack.isNotEmpty) { + sourceStyleHandle = textStyleStack.last.handle; + } + if (sourceStyleHandle == nullptr) { + sourceStyleHandle = style.textStyleHandle; + } + if (sourceStyleHandle == nullptr) { + sourceStyleHandle = + (renderer.fontCollection as SkwasmFontCollection).defaultTextStyle; + } + final TextStyleHandle styleHandle = textStyleCopy(sourceStyleHandle); + textStyle.applyToHandle(styleHandle); + textStyleStack.add(_TextStyleStackEntry(textStyle, styleHandle)); + paragraphBuilderPushStyle(handle, styleHandle); } } diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_canvas.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_canvas.dart index 276fc0dd02461..8571c000f7338 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_canvas.dart +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_canvas.dart @@ -137,6 +137,19 @@ external void canvasDrawShadow( bool transparentOccluder, ); +@Native(symbol: 'canvas_drawParagraph', isLeaf: true) +external void canvasDrawParagraph( + CanvasHandle handle, + ParagraphHandle paragraphHandle, + double x, + double y, +); + @Native( symbol: 'canvas_getTransform', isLeaf: true) external void canvasGetTransform(CanvasHandle canvas, RawMatrix44 outMatrix); diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_fonts.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_fonts.dart index 5bbd928d1ce2e..83ce54e125456 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_fonts.dart +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_fonts.dart @@ -12,19 +12,41 @@ import 'package:ui/src/engine/skwasm/skwasm_impl.dart'; final class RawFontCollection extends Opaque {} typedef FontCollectionHandle = Pointer; +final class RawTypeface extends Opaque {} +typedef TypefaceHandle = Pointer; + @Native(symbol: 'fontCollection_create', isLeaf: true) external FontCollectionHandle fontCollectionCreate(); @Native(symbol: 'fontCollection_dispose', isLeaf: true) external void fontCollectionDispose(FontCollectionHandle handle); -@Native(symbol: 'typeface_create', isLeaf: true) +external TypefaceHandle typefaceCreate(SkDataHandle fontData); + +@Native(symbol: 'typeface_dispose', isLeaf: true) +external void typefaceDispose(TypefaceHandle handle); + +@Native, + Int, + Pointer, + Int, +)>(symbol: 'typefaces_filterCoveredCodePoints', isLeaf: true) +external int typefacesFilterCoveredCodePoints( + Pointer typefaces, + int typefaceCount, + Pointer codepoints, + int codePointCount, +); + +@Native(symbol: 'fontCollection_registerFont', isLeaf: true) -external bool fontCollectionRegisterFont( +)>(symbol: 'fontCollection_registerTypeface', isLeaf: true) +external void fontCollectionRegisterTypeface( FontCollectionHandle handle, - SkDataHandle fontData, + TypefaceHandle typeface, SkStringHandle fontName, ); diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_skstring.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_skstring.dart index 1b5af811542ad..84a500ebc5950 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_skstring.dart +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_skstring.dart @@ -5,11 +5,15 @@ @DefaultAsset('skwasm') library skwasm_impl; +import 'dart:convert'; import 'dart:ffi'; final class RawSkString extends Opaque {} typedef SkStringHandle = Pointer; +final class RawSkString16 extends Opaque {} +typedef SkString16Handle = Pointer; + @Native(symbol: 'skString_allocate', isLeaf: true) external SkStringHandle skStringAllocate(int size); @@ -18,3 +22,31 @@ external Pointer skStringGetData(SkStringHandle handle); @Native(symbol: 'skString_free', isLeaf: true) external void skStringFree(SkStringHandle handle); + +@Native(symbol: 'skString16_allocate', isLeaf: true) +external SkString16Handle skString16Allocate(int size); + +@Native Function(SkString16Handle)>(symbol: 'skString16_getData', isLeaf: true) +external Pointer skString16GetData(SkString16Handle handle); + +@Native(symbol: 'skString16_free', isLeaf: true) +external void skString16Free(SkString16Handle handle); + +SkStringHandle skStringFromDartString(String string) { + final List rawUtf8Bytes = utf8.encode(string); + final SkStringHandle stringHandle = skStringAllocate(rawUtf8Bytes.length); + final Pointer stringDataPointer = skStringGetData(stringHandle); + for (int i = 0; i < rawUtf8Bytes.length; i++) { + stringDataPointer[i] = rawUtf8Bytes[i]; + } + return stringHandle; +} + +SkString16Handle skString16FromDartString(String string) { + final SkString16Handle stringHandle = skString16Allocate(string.length); + final Pointer stringDataPointer = skString16GetData(stringHandle); + for (int i = 0; i < string.length; i++) { + stringDataPointer[i] = string.codeUnitAt(i); + } + return stringHandle; +} diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/text/raw_line_metrics.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/text/raw_line_metrics.dart new file mode 100644 index 0000000000000..c00d4acd1641e --- /dev/null +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/text/raw_line_metrics.dart @@ -0,0 +1,70 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@DefaultAsset('skwasm') +library skwasm_impl; + +import 'dart:ffi'; + +final class RawLineMetrics extends Opaque {} +typedef LineMetricsHandle = Pointer; + +@Native(symbol: 'lineMetrics_create', isLeaf: true) +external LineMetricsHandle lineMetricsCreate( + bool hardBreak, + double ascent, + double descent, + double unscaledAscent, + double height, + double width, + double left, + double baseline, + int lineNumber +); + +@Native(symbol: 'lineMetrics_dispose', isLeaf: true) +external void lineMetricsDispose(LineMetricsHandle handle); + +@Native(symbol: 'lineMetrics_getHardBreak', isLeaf: true) +external bool lineMetricsGetHardBreak(LineMetricsHandle handle); + +@Native(symbol: 'lineMetrics_getAscent', isLeaf: true) +external double lineMetricsGetAscent(LineMetricsHandle handle); + +@Native(symbol: 'lineMetrics_getDescent', isLeaf: true) +external double lineMetricsGetDescent(LineMetricsHandle handle); + +@Native(symbol: 'lineMetrics_getUnscaledAscent', isLeaf: true) +external double lineMetricsGetUnscaledAscent(LineMetricsHandle handle); + +@Native(symbol: 'lineMetrics_getHeight', isLeaf: true) +external double lineMetricsGetHeight(LineMetricsHandle handle); + +@Native(symbol: 'lineMetrics_getWidth', isLeaf: true) +external double lineMetricsGetWidth(LineMetricsHandle handle); + +@Native(symbol: 'lineMetrics_getLeft', isLeaf: true) +external double lineMetricsGetLeft(LineMetricsHandle handle); + +@Native(symbol: 'lineMetrics_getBaseline', isLeaf: true) +external double lineMetricsGetBaseline(LineMetricsHandle handle); + +@Native(symbol: 'lineMetrics_getLineNumber', isLeaf: true) +external int lineMetricsGetLineNumber(LineMetricsHandle handle); + +@Native(symbol: 'lineMetrics_getStartIndex', isLeaf: true) +external int lineMetricsGetStartIndex(LineMetricsHandle handle); + +@Native(symbol: 'lineMetrics_getEndIndex', isLeaf: true) +external int lineMetricsGetEndIndex(LineMetricsHandle handle); diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/text/raw_paragraph.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/text/raw_paragraph.dart new file mode 100644 index 0000000000000..c255dd27ebdd0 --- /dev/null +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/text/raw_paragraph.dart @@ -0,0 +1,121 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@DefaultAsset('skwasm') +library skwasm_impl; + +import 'dart:ffi'; + +import 'package:ui/src/engine/skwasm/skwasm_impl.dart'; + +final class RawParagraph extends Opaque {} +typedef ParagraphHandle = Pointer; + +final class RawTextBoxList extends Opaque {} +typedef TextBoxListHandle = Pointer; + +@Native(symbol: 'paragraph_dispose', isLeaf: true) +external void paragraphDispose(ParagraphHandle handle); + +@Native(symbol: 'paragraph_getWidth', isLeaf: true) +external double paragraphGetWidth(ParagraphHandle handle); + +@Native(symbol: 'paragraph_getHeight', isLeaf: true) +external double paragraphGetHeight(ParagraphHandle handle); + +@Native(symbol: 'paragraph_getLongestLine', isLeaf: true) +external double paragraphGetLongestLine(ParagraphHandle handle); + +@Native(symbol: 'paragraph_getMinIntrinsicWidth', isLeaf: true) +external double paragraphGetMinIntrinsicWidth(ParagraphHandle handle); + +@Native(symbol: 'paragraph_getMaxIntrinsicWidth', isLeaf: true) +external double paragraphGetMaxIntrinsicWidth(ParagraphHandle handle); + +@Native(symbol: 'paragraph_getAlphabeticBaseline', isLeaf: true) +external double paragraphGetAlphabeticBaseline(ParagraphHandle handle); + +@Native(symbol: 'paragraph_getIdeographicBaseline', isLeaf: true) +external double paragraphGetIdeographicBaseline(ParagraphHandle handle); + +@Native(symbol: 'paragraph_getDidExceedMaxLines', isLeaf: true) +external bool paragraphGetDidExceedMaxLines(ParagraphHandle handle); + +@Native(symbol: 'paragraph_layout', isLeaf: true) +external void paragraphLayout(ParagraphHandle handle, double width); + +@Native +)>(symbol: 'paragraph_getPositionForOffset', isLeaf: true) +external int paragraphGetPositionForOffset( + ParagraphHandle handle, + double offsetX, + double offsetY, + Pointer outAffinity, +); + +@Native, +)>(symbol: 'paragraph_getWordBoundary', isLeaf: true) +external void paragraphGetWordBoundary( + ParagraphHandle handle, + int position, + Pointer outRange, // Two `size_t`s, start and end +); + +@Native(symbol: 'paragraph_getLineCount', isLeaf: true) +external int paragraphGetLineCount(ParagraphHandle handle); + +@Native(symbol: 'paragraph_getLineNumberAt', isLeaf: true) +external int paragraphGetLineNumberAt(ParagraphHandle handle, int characterIndex); + +@Native(symbol: 'paragraph_getLineMetricsAtIndex', isLeaf: true) +external LineMetricsHandle paragraphGetLineMetricsAtIndex( + ParagraphHandle handle, + int index, +); + +@Native(symbol: 'textBoxList_dispose', isLeaf: true) +external void textBoxListDispose(TextBoxListHandle handle); + +@Native(symbol: 'textBoxList_getLength', isLeaf: true) +external int textBoxListGetLength(TextBoxListHandle handle); + +@Native(symbol: 'textBoxList_getBoxAtIndex', isLeaf: true) +external int textBoxListGetBoxAtIndex( + TextBoxListHandle handle, + int index, + RawRect outRect, +); + +@Native(symbol: 'paragraph_getBoxesForRange', isLeaf: true) +external TextBoxListHandle paragraphGetBoxesForRange( + ParagraphHandle handle, + int start, + int end, + int heightStyle, + int widthStyle, +); + +@Native( + symbol: 'paragraph_getBoxesForPlaceholders', isLeaf: true) +external TextBoxListHandle paragraphGetBoxesForPlaceholders(ParagraphHandle handle); diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/text/raw_paragraph_builder.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/text/raw_paragraph_builder.dart new file mode 100644 index 0000000000000..eaca68847bcf2 --- /dev/null +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/text/raw_paragraph_builder.dart @@ -0,0 +1,66 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@DefaultAsset('skwasm') +library skwasm_impl; + +import 'dart:ffi'; + +import 'package:ui/src/engine/skwasm/skwasm_impl.dart'; + +final class RawParagraphBuilder extends Opaque {} +typedef ParagraphBuilderHandle = Pointer; + +@Native(symbol: 'paragraphBuilder_create', isLeaf: true) +external ParagraphBuilderHandle paragraphBuilderCreate( + ParagraphStyleHandle styleHandle, + FontCollectionHandle fontCollectionHandle, +); + +@Native(symbol: 'paragraphBuilder_dispose', isLeaf: true) +external void paragraphBuilderDispose(ParagraphBuilderHandle handle); + +@Native(symbol: 'paragraphBuilder_addPlaceholder', isLeaf: true) +external void paragraphBuilderAddPlaceholder( + ParagraphBuilderHandle handle, + double width, + double height, + int alignment, + double baslineOffset, + int baseline, +); + +@Native(symbol: 'paragraphBuilder_addText', isLeaf: true) +external void paragraphBuilderAddText( + ParagraphBuilderHandle handle, + SkString16Handle text, +); + +@Native(symbol: 'paragraphBuilder_pushStyle', isLeaf: true) +external void paragraphBuilderPushStyle( + ParagraphBuilderHandle handle, + TextStyleHandle styleHandle, +); + +@Native(symbol: 'paragraphBuilder_pop', isLeaf: true) +external void paragraphBuilderPop(ParagraphBuilderHandle handle); + +@Native(symbol: 'paragraphBuilder_build', isLeaf: true) +external ParagraphHandle paragraphBuilderBuild(ParagraphBuilderHandle handle); diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/text/raw_paragraph_style.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/text/raw_paragraph_style.dart new file mode 100644 index 0000000000000..e65a5f7690066 --- /dev/null +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/text/raw_paragraph_style.dart @@ -0,0 +1,51 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@DefaultAsset('skwasm') +library skwasm_impl; + +import 'dart:ffi'; + +import 'package:ui/src/engine/skwasm/skwasm_impl.dart'; + +final class RawParagraphStyle extends Opaque {} +typedef ParagraphStyleHandle = Pointer; + +@Native(symbol: 'paragraphStyle_create', isLeaf: true) +external ParagraphStyleHandle paragraphStyleCreate(); + +@Native(symbol: 'paragraphStyle_dispose', isLeaf: true) +external void paragraphStyleDispose(ParagraphStyleHandle handle); + +@Native(symbol: 'paragraphStyle_setTextAlign', isLeaf: true) +external void paragraphStyleSetTextAlign(ParagraphStyleHandle handle, int textAlign); + +@Native(symbol: 'paragraphStyle_setTextDirection', isLeaf: true) +external void paragraphStyleSetTextDirection(ParagraphStyleHandle handle, int textDirection); + +@Native(symbol: 'paragraphStyle_setMaxLines', isLeaf: true) +external void paragraphStyleSetMaxLines(ParagraphStyleHandle handle, int maxLines); + +@Native(symbol: 'paragraphStyle_setHeight', isLeaf: true) +external void paragraphStyleSetHeight(ParagraphStyleHandle handle, double height); + +@Native(symbol: 'paragraphStyle_setTextHeightBehavior', isLeaf: true) +external void paragraphStyleSetTextHeightBehavior( + ParagraphStyleHandle handle, + bool applyHeightToFirstAscent, + bool applyHeightToLastDescent, +); + +@Native(symbol: 'paragraphStyle_setEllipsis', isLeaf: true) +external void paragraphStyleSetEllipsis(ParagraphStyleHandle handle, SkStringHandle ellipsis); + +@Native(symbol: 'paragraphStyle_setStrutStyle', isLeaf: true) +external void paragraphStyleSetStrutStyle(ParagraphStyleHandle handle, StrutStyleHandle strutStyle); + +@Native(symbol: 'paragraphStyle_setTextStyle', isLeaf: true) +external void paragraphStyleSetTextStyle(ParagraphStyleHandle handle, TextStyleHandle textStyle); diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/text/raw_strut_style.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/text/raw_strut_style.dart new file mode 100644 index 0000000000000..d9f925b125444 --- /dev/null +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/text/raw_strut_style.dart @@ -0,0 +1,57 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@DefaultAsset('skwasm') +library skwasm_impl; + +import 'dart:ffi'; + +import 'package:ui/src/engine/skwasm/skwasm_impl.dart'; + +final class RawStrutStyle extends Opaque {} + +typedef StrutStyleHandle = Pointer; + +@Native(symbol: 'strutStyle_create', isLeaf: true) +external StrutStyleHandle strutStyleCreate(); + +@Native(symbol: 'strutStyle_dispose', isLeaf: true) +external void strutStyleDispose(StrutStyleHandle handle); + +@Native families, + Int count +)>(symbol: 'strutStyle_setFontFamilies', isLeaf: true) +external void strutStyleSetFontFamilies( + StrutStyleHandle handle, + Pointer families, + int count +); + +@Native(symbol: 'strutStyle_setFontSize', isLeaf: true) +external void strutStyleSetFontSize(StrutStyleHandle handle, double fontSize); + +@Native(symbol: 'strutStyle_setHeight', isLeaf: true) +external void strutStyleSetHeight(StrutStyleHandle handle, double height); + +@Native(symbol: 'strutStyle_setHalfLeading', isLeaf: true) +external void strutStyleSetHalfLeading(StrutStyleHandle handle, bool height); + +@Native(symbol: 'strutStyle_setLeading', isLeaf: true) +external void strutStyleSetLeading(StrutStyleHandle handle, double leading); + +@Native(symbol: 'strutStyle_setFontStyle', isLeaf: true) +external void strutStyleSetFontStyle( + StrutStyleHandle handle, + int weight, + int slant, +); + +@Native(symbol: 'strutStyle_setForceStrutHeight', isLeaf: true) +external void strutStyleSetForceStrutHeight(StrutStyleHandle handle, bool forceStrutHeight); diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/text/raw_text_style.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/text/raw_text_style.dart new file mode 100644 index 0000000000000..3a8aca91674ae --- /dev/null +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/text/raw_text_style.dart @@ -0,0 +1,129 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@DefaultAsset('skwasm') +library skwasm_impl; + +import 'dart:ffi'; + +import 'package:ui/src/engine/skwasm/skwasm_impl.dart'; + +final class RawTextStyle extends Opaque {} + +typedef TextStyleHandle = Pointer; + +@Native(symbol: 'textStyle_create', isLeaf: true) +external TextStyleHandle textStyleCreate(); + +@Native(symbol: 'textStyle_copy', isLeaf: true) +external TextStyleHandle textStyleCopy(TextStyleHandle handle); + +@Native(symbol: 'textStyle_dispose', isLeaf: true) +external void textStyleDispose(TextStyleHandle handle); + +@Native(symbol: 'textStyle_setColor', isLeaf: true) +external void textStyleSetColor(TextStyleHandle handle, int color); + +@Native(symbol: 'textStyle_setDecoration', isLeaf: true) +external void textStyleSetDecoration(TextStyleHandle handle, int decoration); + +@Native(symbol: 'textStyle_setDecorationColor', isLeaf: true) +external void textStyleSetDecorationColor(TextStyleHandle handle, int decorationColor); + +@Native(symbol: 'textStyle_setDecorationStyle', isLeaf: true) +external void textStyleSetDecorationStyle(TextStyleHandle handle, int decorationStyle); + +@Native(symbol: 'textStyle_setDecorationThickness', isLeaf: true) +external void textStyleSetDecorationThickness(TextStyleHandle handle, double decorationThickness); + +@Native(symbol: 'textStyle_setFontStyle', isLeaf: true) +external void textStyleSetFontStyle( + TextStyleHandle handle, + int weight, + int slant +); + +@Native(symbol: 'textStyle_setTextBaseline', isLeaf: true) +external void textStyleSetTextBaseline(TextStyleHandle handle, int baseline); + +@Native(symbol: 'textStyle_clearFontFamilies', isLeaf: true) +external void textStyleClearFontFamilies(TextStyleHandle handle); + +@Native, + Int count +)>(symbol: 'textStyle_addFontFamilies', isLeaf: true) +external void textStyleAddFontFamilies( + TextStyleHandle handle, + Pointer families, + int count +); + +@Native(symbol: 'textStyle_setFontSize', isLeaf: true) +external void textStyleSetFontSize(TextStyleHandle handle, double size); + +@Native(symbol: 'textStyle_setLetterSpacing', isLeaf: true) +external void textStyleSetLetterSpacing(TextStyleHandle handle, double spacing); + +@Native(symbol: 'textStyle_setWordSpacing', isLeaf: true) +external void textStyleSetWordSpacing(TextStyleHandle handle, double spacing); + +@Native(symbol: 'textStyle_setHeight', isLeaf: true) +external void textStyleSetHeight(TextStyleHandle handle, double height); + +@Native(symbol: 'textStyle_setHalfLeading', isLeaf: true) +external void textStyleSetHalfLeading(TextStyleHandle handle, bool halfLeading); + +@Native(symbol: 'textStyle_setLocale', isLeaf: true) +external void textStyleSetLocale(TextStyleHandle handle, SkStringHandle locale); + +@Native(symbol: 'textStyle_setBackground', isLeaf: true) +external void textStyleSetBackground(TextStyleHandle handle, PaintHandle paint); + +@Native(symbol: 'textStyle_setForeground', isLeaf: true) +external void textStyleSetForeground(TextStyleHandle handle, PaintHandle paint); + +@Native(symbol: 'textStyle_addShadow', isLeaf: true) +external void textStyleAddShadow( + TextStyleHandle handle, + int color, + double offsetX, + double offsetY, + double blurSigma, +); + +@Native(symbol: 'textStyle_addFontFeature', isLeaf: true) +external void textStyleAddFontFeature( + TextStyleHandle handle, + SkStringHandle featureName, + int value, +); + +@Native, + Pointer, + Int +)>(symbol: 'textStyle_setFontVariations', isLeaf: true) +external void textStyleSetFontVariations( + TextStyleHandle handle, + Pointer axes, + Pointer values, + int count, +); diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart index 73b8e4e21fc83..59594feaf68ca 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart @@ -105,7 +105,8 @@ class SkwasmRenderer implements Renderer { ui.Paint createPaint() => SkwasmPaint(); @override - ui.ParagraphBuilder createParagraphBuilder(ui.ParagraphStyle style) => SkwasmParagraphBuilder(); + ui.ParagraphBuilder createParagraphBuilder(ui.ParagraphStyle style) => + SkwasmParagraphBuilder(style as SkwasmParagraphStyle, fontCollection); @override ui.ParagraphStyle createParagraphStyle({ @@ -120,7 +121,20 @@ class SkwasmRenderer implements Renderer { ui.StrutStyle? strutStyle, String? ellipsis, ui.Locale? locale - }) => SkwasmParagraphStyle(); + }) => SkwasmParagraphStyle( + textAlign: textAlign, + textDirection: textDirection, + maxLines: maxLines, + fontFamily: fontFamily, + fontSize: fontSize, + height: height, + textHeightBehavior: textHeightBehavior, + fontWeight: fontWeight, + fontStyle: fontStyle, + strutStyle: strutStyle, + ellipsis: ellipsis, + locale: locale, + ); @override ui.Path createPath() => SkwasmPath(); @@ -159,9 +173,17 @@ class SkwasmRenderer implements Renderer { ui.FontWeight? fontWeight, ui.FontStyle? fontStyle, bool? forceStrutHeight - }) { - throw UnimplementedError('createStrutStyle not yet implemented'); - } + }) => SkwasmStrutStyle( + fontFamily: fontFamily, + fontFamilyFallback: fontFamilyFallback, + fontSize: fontSize, + height: height, + leadingDistribution: leadingDistribution, + leading: leading, + fontWeight: fontWeight, + fontStyle: fontStyle, + forceStrutHeight: forceStrutHeight, + ); @override ui.Gradient createSweepGradient( @@ -205,7 +227,29 @@ class SkwasmRenderer implements Renderer { List? shadows, List? fontFeatures, List? fontVariations - }) => SkwasmTextStyle(); + }) => SkwasmTextStyle( + color: color, + decoration: decoration, + decorationColor: decorationColor, + decorationStyle: decorationStyle, + decorationThickness: decorationThickness, + fontWeight: fontWeight, + fontStyle: fontStyle, + textBaseline: textBaseline, + fontFamily: fontFamily, + fontFamilyFallback: fontFamilyFallback, + fontSize: fontSize, + letterSpacing: letterSpacing, + wordSpacing: wordSpacing, + height: height, + leadingDistribution: leadingDistribution, + locale: locale, + background: background, + foreground: foreground, + shadows: shadows, + fontFeatures: fontFeatures, + fontVariations: fontVariations, + ); @override ui.Vertices createVertices( @@ -309,4 +353,27 @@ class SkwasmRenderer implements Renderer { return SkwasmFragmentProgram.fromBytes(assetKey, data.buffer.asUint8List()); }); } + + @override + ui.LineMetrics createLineMetrics({ + required bool hardBreak, + required double ascent, + required double descent, + required double unscaledAscent, + required double height, + required double width, + required double left, + required double baseline, + required int lineNumber + }) => SkwasmLineMetrics( + hardBreak: hardBreak, + ascent: ascent, + descent: descent, + unscaledAscent: unscaledAscent, + height: height, + width: width, + left: left, + baseline: baseline, + lineNumber: lineNumber + ); } diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_stub/renderer.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_stub/renderer.dart index 40082de28d793..524de360fb16f 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_stub/renderer.dart +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_stub/renderer.dart @@ -175,4 +175,17 @@ class SkwasmRenderer implements Renderer { return CkFragmentProgram.fromBytes(assetKey, data.buffer.asUint8List()); }); } + + @override + ui.LineMetrics createLineMetrics({ + required bool hardBreak, + required double ascent, + required double descent, + required double unscaledAscent, + required double height, + required double width, + required double left, + required double baseline, + required int lineNumber + }) => throw UnimplementedError('Skwasm not implemented on this platform.'); } diff --git a/lib/web_ui/lib/src/engine/text/font_collection.dart b/lib/web_ui/lib/src/engine/text/font_collection.dart index c6666183a3fbf..95d0fea5eb80f 100644 --- a/lib/web_ui/lib/src/engine/text/font_collection.dart +++ b/lib/web_ui/lib/src/engine/text/font_collection.dart @@ -51,6 +51,9 @@ class HtmlFontCollection implements FlutterFontCollection { return _loadFontFaceBytes(fontFamily, list); } + @override + Null get fontFallbackManager => null; + /// Unregister all fonts that have been registered. @override void clear() { @@ -171,4 +174,8 @@ class HtmlFontCollection implements FlutterFontCollection { } return true; } + + @override + void debugResetFallbackFonts() { + } } diff --git a/lib/web_ui/lib/text.dart b/lib/web_ui/lib/text.dart index 3ad62f0fafc90..491966a0f88c2 100644 --- a/lib/web_ui/lib/text.dart +++ b/lib/web_ui/lib/text.dart @@ -229,6 +229,9 @@ class TextDecoration { } final int _mask; + + int get maskValue => _mask; + bool contains(TextDecoration other) { return (_mask | other._mask) == _mask; } @@ -634,7 +637,18 @@ abstract class LineMetrics { required double left, required double baseline, required int lineNumber, - }) = engine.EngineLineMetrics; + }) => engine.renderer.createLineMetrics( + hardBreak: hardBreak, + ascent: ascent, + descent: descent, + unscaledAscent: unscaledAscent, + height: height, + width: width, + left: left, + baseline: baseline, + lineNumber: lineNumber, + ); + bool get hardBreak; double get ascent; double get descent; diff --git a/lib/web_ui/skwasm/BUILD.gn b/lib/web_ui/skwasm/BUILD.gn index 7fb55ceab5bc9..362a75a1a7b84 100644 --- a/lib/web_ui/skwasm/BUILD.gn +++ b/lib/web_ui/skwasm/BUILD.gn @@ -18,6 +18,12 @@ wasm_lib("skwasm") { "shaders.cpp", "string.cpp", "surface.cpp", + "text/line_metrics.cpp", + "text/paragraph.cpp", + "text/paragraph_builder.cpp", + "text/paragraph_style.cpp", + "text/strut_style.cpp", + "text/text_style.cpp", "wrappers.h", ] @@ -34,6 +40,7 @@ wasm_lib("skwasm") { "-lexports.js", "-sEXPORTED_FUNCTIONS=[stackAlloc]", "-sEXPORTED_RUNTIME_METHODS=[addFunction,removeFunction]", + "-Wno-pthreads-mem-growth", ] if (is_debug) { diff --git a/lib/web_ui/skwasm/canvas.cpp b/lib/web_ui/skwasm/canvas.cpp index 9a07aef09bba1..bad7efee94ec9 100644 --- a/lib/web_ui/skwasm/canvas.cpp +++ b/lib/web_ui/skwasm/canvas.cpp @@ -8,6 +8,9 @@ #include "third_party/skia/include/core/SkPoint3.h" #include "third_party/skia/include/utils/SkShadowUtils.h" +#include "third_party/skia/modules/skparagraph/include/Paragraph.h" + +using namespace skia::textlayout; using namespace Skwasm; @@ -205,6 +208,13 @@ SKWASM_EXPORT void canvas_drawShadow(CanvasWrapper* wrapper, devicePixelRatio * kShadowLightRadius, outAmbient, outSpot, flags); } +SKWASM_EXPORT void canvas_drawParagraph(CanvasWrapper* wrapper, + Paragraph* paragraph, + SkScalar x, + SkScalar y) { + paragraph->paint(wrapper->canvas, x, y); +} + SKWASM_EXPORT void canvas_drawPicture(CanvasWrapper* wrapper, SkPicture* picture) { makeCurrent(wrapper->context); diff --git a/lib/web_ui/skwasm/fonts.cpp b/lib/web_ui/skwasm/fonts.cpp index 22b66419cf8e9..313e24dd63eb6 100644 --- a/lib/web_ui/skwasm/fonts.cpp +++ b/lib/web_ui/skwasm/fonts.cpp @@ -6,19 +6,18 @@ #include "third_party/skia/include/core/SkFontMgr.h" #include "third_party/skia/modules/skparagraph/include/FontCollection.h" #include "third_party/skia/modules/skparagraph/include/TypefaceFontProvider.h" +#include "wrappers.h" -using namespace skia::textlayout; +#include -struct FlutterFontCollection { - sk_sp collection; - sk_sp provider; -}; +using namespace skia::textlayout; +using namespace Skwasm; SKWASM_EXPORT FlutterFontCollection* fontCollection_create() { auto collection = sk_make_sp(); auto provider = sk_make_sp(); collection->enableFontFallback(); - collection->setDefaultFontManager(provider); + collection->setDefaultFontManager(provider, "Roboto"); return new FlutterFontCollection{ std::move(collection), std::move(provider), @@ -29,21 +28,59 @@ SKWASM_EXPORT void fontCollection_dispose(FlutterFontCollection* collection) { delete collection; } -SKWASM_EXPORT bool fontCollection_registerFont( - FlutterFontCollection* collection, - SkData* fontData, - SkString* fontName) { +SKWASM_EXPORT SkTypeface* typeface_create(SkData* fontData) { fontData->ref(); - auto typeFace = + auto typeface = SkFontMgr::RefDefault()->makeFromData(sk_sp(fontData)); - if (!typeFace) { - return false; + return typeface.release(); +} + +SKWASM_EXPORT void typeface_dispose(SkTypeface* typeface) { + typeface->unref(); +} + +// Calculates the code points that are not covered by the specified typefaces. +// This function mutates the `codePoints` buffer in place and returns the count +// of code points that are not covered by the fonts. +SKWASM_EXPORT int typefaces_filterCoveredCodePoints(SkTypeface** typefaces, + int typefaceCount, + SkUnichar* codePoints, + int codePointCount) { + std::unique_ptr glyphBuffer = + std::make_unique(codePointCount); + SkGlyphID* glyphPointer = glyphBuffer.get(); + int remainingCodePointCount = codePointCount; + for (int typefaceIndex = 0; typefaceIndex < typefaceCount; typefaceIndex++) { + typefaces[typefaceIndex]->unicharsToGlyphs( + codePoints, remainingCodePointCount, glyphPointer); + int outputIndex = 0; + for (int inputIndex = 0; inputIndex < remainingCodePointCount; + inputIndex++) { + if (glyphPointer[inputIndex] == 0) { + if (outputIndex != inputIndex) { + codePoints[outputIndex] = codePoints[inputIndex]; + } + outputIndex++; + } + } + if (outputIndex == 0) { + return 0; + } else { + remainingCodePointCount = outputIndex; + } } + return remainingCodePointCount; +} + +SKWASM_EXPORT void fontCollection_registerTypeface( + FlutterFontCollection* collection, + SkTypeface* typeface, + SkString* fontName) { + typeface->ref(); if (fontName) { SkString alias = *fontName; - collection->provider->registerTypeface(std::move(typeFace), alias); + collection->provider->registerTypeface(sk_sp(typeface), alias); } else { - collection->provider->registerTypeface(std::move(typeFace)); + collection->provider->registerTypeface(sk_sp(typeface)); } - return true; } diff --git a/lib/web_ui/skwasm/string.cpp b/lib/web_ui/skwasm/string.cpp index 77e66d705dcfc..d7591a546c462 100644 --- a/lib/web_ui/skwasm/string.cpp +++ b/lib/web_ui/skwasm/string.cpp @@ -17,3 +17,17 @@ SKWASM_EXPORT char* skString_getData(SkString* string) { SKWASM_EXPORT void skString_free(SkString* string) { return delete string; } + +SKWASM_EXPORT std::u16string* skString16_allocate(size_t length) { + std::u16string* string = new std::u16string(); + string->resize(length); + return string; +} + +SKWASM_EXPORT char16_t* skString16_getData(std::u16string* string) { + return string->data(); +} + +SKWASM_EXPORT void skString16_free(std::u16string* string) { + delete string; +} diff --git a/lib/web_ui/skwasm/text/line_metrics.cpp b/lib/web_ui/skwasm/text/line_metrics.cpp new file mode 100644 index 0000000000000..c163dc322dee2 --- /dev/null +++ b/lib/web_ui/skwasm/text/line_metrics.cpp @@ -0,0 +1,78 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "../export.h" +#include "third_party/skia/modules/skparagraph/include/Paragraph.h" + +using namespace skia::textlayout; + +SKWASM_EXPORT LineMetrics* lineMetrics_create(bool hardBreak, + double ascent, + double descent, + double unscaledAscent, + double height, + double width, + double left, + double baseline, + size_t lineNumber) { + auto metrics = new LineMetrics(); + metrics->fHardBreak = hardBreak; + metrics->fAscent = ascent; + metrics->fDescent = descent; + metrics->fUnscaledAscent = unscaledAscent; + metrics->fHeight = height; + metrics->fWidth = width; + metrics->fLeft = left; + metrics->fBaseline = baseline; + metrics->fLineNumber = lineNumber; + return metrics; +} + +SKWASM_EXPORT void lineMetrics_dispose(LineMetrics* metrics) { + delete metrics; +} + +SKWASM_EXPORT bool lineMetrics_getHardBreak(LineMetrics* metrics) { + return metrics->fHardBreak; +} + +SKWASM_EXPORT SkScalar lineMetrics_getAscent(LineMetrics* metrics) { + return metrics->fAscent; +} + +SKWASM_EXPORT SkScalar lineMetrics_getDescent(LineMetrics* metrics) { + return metrics->fDescent; +} + +SKWASM_EXPORT SkScalar lineMetrics_getUnscaledAscent(LineMetrics* metrics) { + return metrics->fUnscaledAscent; +} + +SKWASM_EXPORT SkScalar lineMetrics_getHeight(LineMetrics* metrics) { + return metrics->fHeight; +} + +SKWASM_EXPORT SkScalar lineMetrics_getWidth(LineMetrics* metrics) { + return metrics->fWidth; +} + +SKWASM_EXPORT SkScalar lineMetrics_getLeft(LineMetrics* metrics) { + return metrics->fLeft; +} + +SKWASM_EXPORT SkScalar lineMetrics_getBaseline(LineMetrics* metrics) { + return metrics->fBaseline; +} + +SKWASM_EXPORT int lineMetrics_getLineNumber(LineMetrics* metrics) { + return metrics->fLineNumber; +} + +SKWASM_EXPORT size_t lineMetrics_getStartIndex(LineMetrics* metrics) { + return metrics->fStartIndex; +} + +SKWASM_EXPORT size_t lineMetrics_getEndIndex(LineMetrics* metrics) { + return metrics->fEndIndex; +} diff --git a/lib/web_ui/skwasm/text/paragraph.cpp b/lib/web_ui/skwasm/text/paragraph.cpp new file mode 100644 index 0000000000000..34fe9af8d93e5 --- /dev/null +++ b/lib/web_ui/skwasm/text/paragraph.cpp @@ -0,0 +1,120 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "third_party/skia/modules/skparagraph/include/Paragraph.h" +#include "../export.h" + +using namespace skia::textlayout; + +SKWASM_EXPORT void paragraph_dispose(Paragraph* paragraph) { + delete paragraph; +} + +SKWASM_EXPORT SkScalar paragraph_getWidth(Paragraph* paragraph) { + return paragraph->getMaxWidth(); +} + +SKWASM_EXPORT SkScalar paragraph_getHeight(Paragraph* paragraph) { + return paragraph->getHeight(); +} + +SKWASM_EXPORT SkScalar paragraph_getLongestLine(Paragraph* paragraph) { + return paragraph->getLongestLine(); +} + +SKWASM_EXPORT SkScalar paragraph_getMinIntrinsicWidth(Paragraph* paragraph) { + return paragraph->getMinIntrinsicWidth(); +} + +SKWASM_EXPORT SkScalar paragraph_getMaxIntrinsicWidth(Paragraph* paragraph) { + return paragraph->getMaxIntrinsicWidth(); +} + +SKWASM_EXPORT SkScalar paragraph_getAlphabeticBaseline(Paragraph* paragraph) { + return paragraph->getAlphabeticBaseline(); +} + +SKWASM_EXPORT SkScalar paragraph_getIdeographicBaseline(Paragraph* paragraph) { + return paragraph->getIdeographicBaseline(); +} + +SKWASM_EXPORT bool paragraph_getDidExceedMaxLines(Paragraph* paragraph) { + return paragraph->didExceedMaxLines(); +} + +SKWASM_EXPORT void paragraph_layout(Paragraph* paragraph, SkScalar width) { + paragraph->layout(width); +} + +SKWASM_EXPORT int32_t paragraph_getPositionForOffset(Paragraph* paragraph, + SkScalar offsetX, + SkScalar offsetY, + Affinity* outAffinity) { + auto position = paragraph->getGlyphPositionAtCoordinate(offsetX, offsetY); + if (outAffinity) { + *outAffinity = position.affinity; + } + return position.position; +} + +SKWASM_EXPORT void paragraph_getWordBoundary( + Paragraph* paragraph, + unsigned int position, + int32_t* outRange // Two `int32_t`s, start and end +) { + auto range = paragraph->getWordBoundary(position); + outRange[0] = range.start; + outRange[1] = range.end; +} + +SKWASM_EXPORT size_t paragraph_getLineCount(Paragraph* paragraph) { + return paragraph->lineNumber(); +} + +SKWASM_EXPORT int paragraph_getLineNumberAt(Paragraph* paragraph, + size_t characterIndex) { + return paragraph->getLineNumberAt(characterIndex); +} + +SKWASM_EXPORT LineMetrics* paragraph_getLineMetricsAtIndex(Paragraph* paragraph, + size_t index) { + auto metrics = new LineMetrics(); + paragraph->getLineMetricsAt(index, metrics); + return metrics; +} + +struct TextBoxList { + std::vector boxes; +}; + +SKWASM_EXPORT void textBoxList_dispose(TextBoxList* list) { + delete list; +} + +SKWASM_EXPORT size_t textBoxList_getLength(TextBoxList* list) { + return list->boxes.size(); +} + +SKWASM_EXPORT TextDirection textBoxList_getBoxAtIndex(TextBoxList* list, + size_t index, + SkRect* outRect) { + const auto& box = list->boxes[index]; + *outRect = box.rect; + return box.direction; +} + +SKWASM_EXPORT TextBoxList* paragraph_getBoxesForRange( + Paragraph* paragraph, + int start, + int end, + RectHeightStyle heightStyle, + RectWidthStyle widthStyle) { + return new TextBoxList{ + paragraph->getRectsForRange(start, end, heightStyle, widthStyle)}; +} + +SKWASM_EXPORT TextBoxList* paragraph_getBoxesForPlaceholders( + Paragraph* paragraph) { + return new TextBoxList{paragraph->getRectsForPlaceholders()}; +} diff --git a/lib/web_ui/skwasm/text/paragraph_builder.cpp b/lib/web_ui/skwasm/text/paragraph_builder.cpp new file mode 100644 index 0000000000000..74b25fa5293de --- /dev/null +++ b/lib/web_ui/skwasm/text/paragraph_builder.cpp @@ -0,0 +1,49 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "../export.h" +#include "../wrappers.h" +#include "third_party/skia/modules/skparagraph/include/ParagraphBuilder.h" + +using namespace skia::textlayout; +using namespace Skwasm; + +SKWASM_EXPORT ParagraphBuilder* paragraphBuilder_create( + ParagraphStyle* style, + FlutterFontCollection* collection) { + return ParagraphBuilder::make(*style, collection->collection).release(); +} + +SKWASM_EXPORT void paragraphBuilder_dispose(ParagraphBuilder* builder) { + delete builder; +} + +SKWASM_EXPORT void paragraphBuilder_addPlaceholder( + ParagraphBuilder* builder, + SkScalar width, + SkScalar height, + PlaceholderAlignment alignment, + SkScalar baselineOffset, + TextBaseline baseline) { + builder->addPlaceholder( + PlaceholderStyle(width, height, alignment, baseline, baselineOffset)); +} + +SKWASM_EXPORT void paragraphBuilder_addText(ParagraphBuilder* builder, + std::u16string* text) { + builder->addText(*text); +} + +SKWASM_EXPORT void paragraphBuilder_pushStyle(ParagraphBuilder* builder, + TextStyle* style) { + builder->pushStyle(*style); +} + +SKWASM_EXPORT void paragraphBuilder_pop(ParagraphBuilder* builder) { + builder->pop(); +} + +SKWASM_EXPORT Paragraph* paragraphBuilder_build(ParagraphBuilder* builder) { + return builder->Build().release(); +} diff --git a/lib/web_ui/skwasm/text/paragraph_style.cpp b/lib/web_ui/skwasm/text/paragraph_style.cpp new file mode 100644 index 0000000000000..9a9dc82fac616 --- /dev/null +++ b/lib/web_ui/skwasm/text/paragraph_style.cpp @@ -0,0 +1,80 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "../export.h" +#include "../wrappers.h" +#include "third_party/skia/modules/skparagraph/include/Paragraph.h" + +using namespace skia::textlayout; +using namespace Skwasm; + +SKWASM_EXPORT ParagraphStyle* paragraphStyle_create() { + auto style = new ParagraphStyle(); + + // This is the default behavior in Flutter + style->setReplaceTabCharacters(true); + + // Default text style has a black color + TextStyle textStyle; + textStyle.setColor(SK_ColorBLACK); + style->setTextStyle(textStyle); + + return style; +} + +SKWASM_EXPORT void paragraphStyle_dispose(ParagraphStyle* style) { + delete style; +} + +SKWASM_EXPORT void paragraphStyle_setTextAlign(ParagraphStyle* style, + TextAlign align) { + style->setTextAlign(align); +} + +SKWASM_EXPORT void paragraphStyle_setTextDirection(ParagraphStyle* style, + TextDirection direction) { + style->setTextDirection(direction); +} + +SKWASM_EXPORT void paragraphStyle_setMaxLines(ParagraphStyle* style, + size_t maxLines) { + style->setMaxLines(maxLines); +} + +SKWASM_EXPORT void paragraphStyle_setHeight(ParagraphStyle* style, + SkScalar height) { + style->setHeight(height); +} + +SKWASM_EXPORT void paragraphStyle_setTextHeightBehavior( + ParagraphStyle* style, + bool applyHeightToFirstAscent, + bool applyHeightToLastDescent) { + TextHeightBehavior behavior; + if (!applyHeightToFirstAscent && !applyHeightToLastDescent) { + behavior = kDisableAll; + } else if (!applyHeightToLastDescent) { + behavior = kDisableLastDescent; + } else if (!applyHeightToFirstAscent) { + behavior = kDisableFirstAscent; + } else { + behavior = kAll; + } + style->setTextHeightBehavior(behavior); +} + +SKWASM_EXPORT void paragraphStyle_setEllipsis(ParagraphStyle* style, + SkString* ellipsis) { + style->setEllipsis(*ellipsis); +} + +SKWASM_EXPORT void paragraphStyle_setStrutStyle(ParagraphStyle* style, + StrutStyle* strutStyle) { + style->setStrutStyle(*strutStyle); +} + +SKWASM_EXPORT void paragraphStyle_setTextStyle(ParagraphStyle* style, + TextStyle* textStyle) { + style->setTextStyle(*textStyle); +} diff --git a/lib/web_ui/skwasm/text/strut_style.cpp b/lib/web_ui/skwasm/text/strut_style.cpp new file mode 100644 index 0000000000000..41f9edf52e7fd --- /dev/null +++ b/lib/web_ui/skwasm/text/strut_style.cpp @@ -0,0 +1,56 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "../export.h" +#include "third_party/skia/modules/skparagraph/include/Paragraph.h" + +using namespace skia::textlayout; + +SKWASM_EXPORT StrutStyle* strutStyle_create() { + return new StrutStyle(); +} + +SKWASM_EXPORT void strutStyle_dispose(StrutStyle* style) { + delete style; +} + +SKWASM_EXPORT void strutStyle_setFontFamilies(StrutStyle* style, + SkString** fontFamilies, + int count) { + std::vector families; + families.reserve(count); + for (int i = 0; i < count; i++) { + families.push_back(*fontFamilies[i]); + } + style->setFontFamilies(std::move(families)); +} + +SKWASM_EXPORT void strutStyle_setFontSize(StrutStyle* style, + SkScalar fontSize) { + style->setFontSize(fontSize); +} + +SKWASM_EXPORT void strutStyle_setHeight(StrutStyle* style, SkScalar height) { + style->setHeight(height); +} + +SKWASM_EXPORT void strutStyle_setHalfLeading(StrutStyle* style, + bool halfLeading) { + style->setHalfLeading(halfLeading); +} + +SKWASM_EXPORT void strutStyle_setLeading(StrutStyle* style, SkScalar leading) { + style->setLeading(leading); +} + +SKWASM_EXPORT void strutStyle_setFontStyle(StrutStyle* style, + int weight, + SkFontStyle::Slant slant) { + style->setFontStyle(SkFontStyle(weight, SkFontStyle::kNormal_Width, slant)); +} + +SKWASM_EXPORT void strutStyle_setForceStrutHeight(StrutStyle* style, + bool forceStrutHeight) { + style->setForceStrutHeight(forceStrutHeight); +} diff --git a/lib/web_ui/skwasm/text/text_style.cpp b/lib/web_ui/skwasm/text/text_style.cpp new file mode 100644 index 0000000000000..b520f52a5424d --- /dev/null +++ b/lib/web_ui/skwasm/text/text_style.cpp @@ -0,0 +1,144 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "../export.h" +#include "../wrappers.h" +#include "third_party/skia/modules/skparagraph/include/Paragraph.h" + +using namespace skia::textlayout; +using namespace Skwasm; + +SKWASM_EXPORT TextStyle* textStyle_create() { + auto style = new TextStyle(); + + // Default color in flutter is black. + style->setColor(SK_ColorBLACK); + return style; +} + +SKWASM_EXPORT TextStyle* textStyle_copy(TextStyle* style) { + return new TextStyle(*style); +} + +SKWASM_EXPORT void textStyle_dispose(TextStyle* style) { + delete style; +} + +SKWASM_EXPORT void textStyle_setColor(TextStyle* style, SkColor color) { + style->setColor(color); +} + +SKWASM_EXPORT void textStyle_setDecoration(TextStyle* style, + TextDecoration decoration) { + style->setDecoration(decoration); +} + +SKWASM_EXPORT void textStyle_setDecorationColor(TextStyle* style, + SkColor color) { + style->setDecorationColor(color); +} + +SKWASM_EXPORT void textStyle_setDecorationStyle( + TextStyle* style, + TextDecorationStyle decorationStyle) { + style->setDecorationStyle(decorationStyle); +} + +SKWASM_EXPORT void textStyle_setDecorationThickness(TextStyle* style, + SkScalar thickness) { + style->setDecorationThicknessMultiplier(thickness); +} + +SKWASM_EXPORT void textStyle_setFontStyle(TextStyle* style, + int weight, + SkFontStyle::Slant slant) { + style->setFontStyle(SkFontStyle(weight, SkFontStyle::kNormal_Width, slant)); +} + +SKWASM_EXPORT void textStyle_setTextBaseline(TextStyle* style, + TextBaseline baseline) { + style->setTextBaseline(baseline); +} + +SKWASM_EXPORT void textStyle_clearFontFamilies(TextStyle* style) { + style->setFontFamilies({}); +} + +SKWASM_EXPORT void textStyle_addFontFamilies(TextStyle* style, + SkString** fontFamilies, + int count) { + const std::vector& currentFamilies = style->getFontFamilies(); + std::vector newFamilies; + newFamilies.reserve(currentFamilies.size() + count); + for (int i = 0; i < count; i++) { + newFamilies.push_back(*fontFamilies[i]); + } + for (const auto& family : currentFamilies) { + newFamilies.push_back(family); + } + style->setFontFamilies(std::move(newFamilies)); +} + +SKWASM_EXPORT void textStyle_setFontSize(TextStyle* style, SkScalar size) { + style->setFontSize(size); +} + +SKWASM_EXPORT void textStyle_setLetterSpacing(TextStyle* style, + SkScalar letterSpacing) { + style->setLetterSpacing(letterSpacing); +} + +SKWASM_EXPORT void textStyle_setWordSpacing(TextStyle* style, + SkScalar wordSpacing) { + style->setWordSpacing(wordSpacing); +} + +SKWASM_EXPORT void textStyle_setHeight(TextStyle* style, SkScalar height) { + style->setHeight(height); +} + +SKWASM_EXPORT void textStyle_setHalfLeading(TextStyle* style, + bool halfLeading) { + style->setHalfLeading(halfLeading); +} + +SKWASM_EXPORT void textStyle_setLocale(TextStyle* style, SkString* locale) { + style->setLocale(*locale); +} + +SKWASM_EXPORT void textStyle_setBackground(TextStyle* style, SkPaint* paint) { + style->setBackgroundColor(*paint); +} + +SKWASM_EXPORT void textStyle_setForeground(TextStyle* style, SkPaint* paint) { + style->setForegroundColor(*paint); +} + +SKWASM_EXPORT void textStyle_addShadow(TextStyle* style, + SkColor color, + SkScalar offsetX, + SkScalar offsetY, + SkScalar blurSigma) { + style->addShadow(TextShadow(color, {offsetX, offsetY}, blurSigma)); +} + +SKWASM_EXPORT void textStyle_addFontFeature(TextStyle* style, + SkString* featureName, + int value) { + style->addFontFeature(*featureName, value); +} + +SKWASM_EXPORT void textStyle_setFontVariations(TextStyle* style, + SkFourByteTag* axes, + float* values, + int count) { + std::vector coordinates; + for (int i = 0; i < count; i++) { + coordinates.push_back({axes[i], values[i]}); + } + SkFontArguments::VariationPosition position = { + coordinates.data(), static_cast(coordinates.size())}; + style->setFontArguments( + SkFontArguments().setVariationDesignPosition(position)); +} diff --git a/lib/web_ui/skwasm/wrappers.h b/lib/web_ui/skwasm/wrappers.h index 2acc209015d36..ffb356bba9c67 100644 --- a/lib/web_ui/skwasm/wrappers.h +++ b/lib/web_ui/skwasm/wrappers.h @@ -7,6 +7,8 @@ #include #include "third_party/skia/include/core/SkCanvas.h" #include "third_party/skia/include/core/SkSurface.h" +#include "third_party/skia/modules/skparagraph/include/FontCollection.h" +#include "third_party/skia/modules/skparagraph/include/TypefaceFontProvider.h" namespace Skwasm { @@ -31,4 +33,9 @@ inline void makeCurrent(EMSCRIPTEN_WEBGL_CONTEXT_HANDLE handle) { } } +struct FlutterFontCollection { + sk_sp collection; + sk_sp provider; +}; + } // namespace Skwasm diff --git a/lib/web_ui/test/canvaskit/canvas_golden_test.dart b/lib/web_ui/test/canvaskit/canvas_golden_test.dart index f58485f1f0124..234f4bf27594c 100644 --- a/lib/web_ui/test/canvaskit/canvas_golden_test.dart +++ b/lib/web_ui/test/canvaskit/canvas_golden_test.dart @@ -25,16 +25,8 @@ void testMain() { setUpCanvasKitTest(); setUp(() { - expect(notoDownloadQueue.downloader.debugActiveDownloadCount, 0); - expect(notoDownloadQueue.isPending, isFalse); - - FontFallbackData.debugReset(); - notoDownloadQueue.downloader.fallbackFontUrlPrefixOverride = 'assets/fallback_fonts/'; - }); - - tearDown(() { - expect(notoDownloadQueue.downloader.debugActiveDownloadCount, 0); - expect(notoDownloadQueue.isPending, isFalse); + renderer.fontCollection.debugResetFallbackFonts(); + renderer.fontCollection.fontFallbackManager!.downloadQueue.fallbackFontUrlPrefixOverride = 'assets/fallback_fonts/'; }); test('renders using non-recording canvas if weak refs are supported', @@ -50,369 +42,6 @@ void testMain() { ); }); - test('text styles - default', () async { - await testTextStyle('default'); - }); - - test('text styles - center aligned', () async { - await testTextStyle('center aligned', - paragraphTextAlign: ui.TextAlign.center); - }); - - test('text styles - right aligned', () async { - await testTextStyle('right aligned', - paragraphTextAlign: ui.TextAlign.right); - }); - - test('text styles - rtl', () async { - await testTextStyle('rtl', paragraphTextDirection: ui.TextDirection.rtl); - }); - - test('text styles - multiline', () async { - await testTextStyle('multiline', layoutWidth: 50); - }); - - test('text styles - max lines', () async { - await testTextStyle('max lines', paragraphMaxLines: 1, layoutWidth: 50); - }); - - test('text styles - ellipsis', () async { - await testTextStyle('ellipsis', - paragraphMaxLines: 1, paragraphEllipsis: '...', layoutWidth: 60); - }); - - test('text styles - paragraph font family', () async { - await testTextStyle('paragraph font family', paragraphFontFamily: 'Ahem'); - }); - - test('text styles - paragraph font size', () async { - await testTextStyle('paragraph font size', paragraphFontSize: 22); - }); - - test('text styles - paragraph height', () async { - await testTextStyle('paragraph height', - layoutWidth: 50, paragraphHeight: 1.5); - }); - - test('text styles - paragraph text height behavior', () async { - await testTextStyle('paragraph text height behavior', - layoutWidth: 50, - paragraphHeight: 1.5, - paragraphTextHeightBehavior: const ui.TextHeightBehavior( - applyHeightToFirstAscent: false, - applyHeightToLastDescent: false, - )); - }); - - test('text styles - paragraph weight', () async { - await testTextStyle('paragraph weight', - paragraphFontWeight: ui.FontWeight.w900); - }); - - test('text style - paragraph font style', () async { - await testTextStyle( - 'paragraph font style', - paragraphFontStyle: ui.FontStyle.italic, - ); - }); - - // TODO(yjbanov): locales specified in paragraph styles don't work: - // https://github.com/flutter/flutter/issues/74687 - // TODO(yjbanov): spaces are not rendered correctly: - // https://github.com/flutter/flutter/issues/74742 - test('text styles - paragraph locale zh_CN', () async { - await testTextStyle('paragraph locale zh_CN', - outerText: '次 化 刃 直 入 令', - innerText: '', - paragraphLocale: const ui.Locale('zh', 'CN')); - }); - - test('text styles - paragraph locale zh_TW', () async { - await testTextStyle('paragraph locale zh_TW', - outerText: '次 化 刃 直 入 令', - innerText: '', - paragraphLocale: const ui.Locale('zh', 'TW')); - }); - - test('text styles - paragraph locale ja', () async { - await testTextStyle('paragraph locale ja', - outerText: '次 化 刃 直 入 令', - innerText: '', - paragraphLocale: const ui.Locale('ja')); - }); - - test('text styles - paragraph locale ko', () async { - await testTextStyle('paragraph locale ko', - outerText: '次 化 刃 直 入 令', - innerText: '', - paragraphLocale: const ui.Locale('ko')); - }); - - test('text styles - color', () async { - await testTextStyle('color', color: const ui.Color(0xFF009900)); - }); - - test('text styles - decoration', () async { - await testTextStyle('decoration', - decoration: ui.TextDecoration.underline); - }); - - test('text styles - decoration style', () async { - await testTextStyle('decoration style', - decoration: ui.TextDecoration.underline, - decorationStyle: ui.TextDecorationStyle.dashed); - }); - - test('text styles - decoration thickness', () async { - await testTextStyle('decoration thickness', - decoration: ui.TextDecoration.underline, decorationThickness: 5.0); - }); - - test('text styles - font weight', () async { - await testTextStyle('font weight', fontWeight: ui.FontWeight.w900); - }); - - test('text styles - font style', () async { - await testTextStyle('font style', fontStyle: ui.FontStyle.italic); - }); - - // TODO(yjbanov): not sure how to test this. - test('text styles - baseline', () async { - await testTextStyle('baseline', - textBaseline: ui.TextBaseline.ideographic); - }); - - test('text styles - font family', () async { - await testTextStyle('font family', fontFamily: 'Ahem'); - }); - - test('text styles - non-existent font family', () async { - await testTextStyle('non-existent font family', - fontFamily: 'DoesNotExist'); - }); - - test('text styles - family fallback', () async { - await testTextStyle('family fallback', - fontFamily: 'DoesNotExist', fontFamilyFallback: ['Ahem']); - }); - - test('text styles - font size', () async { - await testTextStyle('font size', fontSize: 24); - }); - - // A regression test for the special case when CanvasKit would default to - // a positive font size when Flutter specifies zero. - // - // See: https://github.com/flutter/flutter/issues/98248 - test('text styles - zero font size', () async { - // This only sets the inner text style, but not the paragraph style, so - // "Hello" should be visible, but "World!" should disappear. - await testTextStyle('zero font size', fontSize: 0); - - // This sets the paragraph font size to zero, but the inner text gets - // an explicit non-zero size that should override paragraph properties, - // so this time "Hello" should disappear, but "World!" should still be - // visible. - await testTextStyle('zero paragraph font size', paragraphFontSize: 0, fontSize: 14); - }); - - test('text styles - letter spacing', () async { - await testTextStyle('letter spacing', letterSpacing: 5); - }); - - test('text styles - word spacing', () async { - await testTextStyle('word spacing', - innerText: 'Beautiful World!', wordSpacing: 25); - }); - - test('text styles - height', () async { - await testTextStyle('height', height: 2); - }); - - test('text styles - leading distribution', () async { - await testTextStyle('half leading', - height: 20, - fontSize: 10, - leadingDistribution: ui.TextLeadingDistribution.even); - await testTextStyle( - 'half leading inherited from paragraph', - height: 20, - fontSize: 10, - paragraphTextHeightBehavior: const ui.TextHeightBehavior( - leadingDistribution: ui.TextLeadingDistribution.even, - ), - ); - await testTextStyle( - 'text style half leading overrides paragraph style half leading', - height: 20, - fontSize: 10, - leadingDistribution: ui.TextLeadingDistribution.proportional, - paragraphTextHeightBehavior: const ui.TextHeightBehavior( - leadingDistribution: ui.TextLeadingDistribution.even, - ), - ); - }); - - // TODO(yjbanov): locales specified in text styles don't work: - // https://github.com/flutter/flutter/issues/74687 - // TODO(yjbanov): spaces are not rendered correctly: - // https://github.com/flutter/flutter/issues/74742 - test('text styles - locale zh_CN', () async { - await testTextStyle('locale zh_CN', - innerText: '次 化 刃 直 入 令', - outerText: '', - locale: const ui.Locale('zh', 'CN')); - }); - - test('text styles - locale zh_TW', () async { - await testTextStyle('locale zh_TW', - innerText: '次 化 刃 直 入 令', - outerText: '', - locale: const ui.Locale('zh', 'TW')); - }); - - test('text styles - locale ja', () async { - await testTextStyle('locale ja', - innerText: '次 化 刃 直 入 令', - outerText: '', - locale: const ui.Locale('ja')); - }); - - test('text styles - locale ko', () async { - await testTextStyle('locale ko', - innerText: '次 化 刃 直 入 令', - outerText: '', - locale: const ui.Locale('ko')); - }); - - test('text styles - background', () async { - await testTextStyle('background', - background: CkPaint()..color = const ui.Color(0xFF00FF00)); - }); - - test('text styles - foreground', () async { - await testTextStyle('foreground', - foreground: CkPaint()..color = const ui.Color(0xFF0000FF)); - }); - - test('text styles - foreground and background', () async { - await testTextStyle( - 'foreground and background', - foreground: CkPaint()..color = const ui.Color(0xFFFF5555), - background: CkPaint()..color = const ui.Color(0xFF007700), - ); - }); - - test('text styles - background and color', () async { - await testTextStyle( - 'background and color', - color: const ui.Color(0xFFFFFF00), - background: CkPaint()..color = const ui.Color(0xFF007700), - ); - }); - - test('text styles - shadows', () async { - await testTextStyle('shadows', shadows: [ - const ui.Shadow( - color: ui.Color(0xFF999900), - offset: ui.Offset(10, 10), - blurRadius: 5, - ), - const ui.Shadow( - color: ui.Color(0xFF009999), - offset: ui.Offset(-10, -10), - blurRadius: 10, - ), - ]); - }); - - test('text styles - old style figures', () async { - await testTextStyle( - 'old style figures', - paragraphFontFamily: 'Roboto', - paragraphFontSize: 24, - outerText: '0 1 2 3 4 5 ', - innerText: '0 1 2 3 4 5', - fontFeatures: [const ui.FontFeature.oldstyleFigures()], - ); - }); - - test('text styles - stylistic set 1', () async { - await testTextStyle( - 'stylistic set 1', - paragraphFontFamily: 'Roboto', - paragraphFontSize: 24, - outerText: 'g', - innerText: 'g', - fontFeatures: [ui.FontFeature.stylisticSet(1)], - ); - }); - - test('text styles - stylistic set 2', () async { - await testTextStyle( - 'stylistic set 2', - paragraphFontFamily: 'Roboto', - paragraphFontSize: 24, - outerText: 'α', - innerText: 'α', - fontFeatures: [ui.FontFeature.stylisticSet(2)], - ); - }); - - test('text styles - override font family', () async { - await testTextStyle( - 'override font family', - paragraphFontFamily: 'Ahem', - fontFamily: 'Roboto', - ); - }); - - test('text styles - override font size', () async { - await testTextStyle( - 'override font size', - paragraphFontSize: 36, - fontSize: 18, - ); - }); - - test('text style - override font weight', () async { - await testTextStyle( - 'override font weight', - paragraphFontWeight: ui.FontWeight.w900, - fontWeight: ui.FontWeight.normal, - ); - }); - - test('text style - override font style', () async { - await testTextStyle( - 'override font style', - paragraphFontStyle: ui.FontStyle.italic, - fontStyle: ui.FontStyle.normal, - ); - }); - - test('text style - characters from multiple fallback fonts', () async { - await testTextStyle( - 'multi-font characters', - // This character is claimed by multiple fonts. This test makes sure - // we can find a font supporting it. - outerText: '欢', - innerText: '', - ); - }); - - test('text style - symbols', () async { - // One of the CJK fonts loaded in one of the tests above also contains - // some of these symbols. To make sure the test produces predictable - // results we reset the fallback data forcing the engine to reload - // fallbacks, which for this test will only load Noto Symbols. - await testTextStyle( - 'symbols', - outerText: '← ↑ → ↓ ', - innerText: '', - ); - }); - test( 'text style - foreground/background/color do not leak across paragraphs', () async { @@ -505,131 +134,6 @@ void testMain() { ); }); - test('sample Chinese text', () async { - await testSampleText( - 'chinese', - '也称乱数假文或者哑元文本, ' - '是印刷及排版领域所常用的虚拟文字。' - '由于曾经一台匿名的打印机刻意打乱了' - '一盒印刷字体从而造出一本字体样品书', - ); - }); - - test('sample Armenian text', () async { - await testSampleText( - 'armenian', - 'տպագրության և տպագրական արդյունաբերության համար նախատեսված մոդելային տեքստ է', - ); - }); - - test('sample Albanian text', () async { - await testSampleText( - 'albanian', - 'është një tekst shabllon i industrisë së printimit dhe shtypshkronjave Lorem Ipsum ka qenë teksti shabllon', - ); - }); - - test('sample Arabic text', () async { - await testSampleText( - 'arabic', - 'هناك حقيقة مثبتة منذ زمن طويل وهي أن المحتوى المقروء لصفحة ما سيلهي', - textDirection: ui.TextDirection.rtl, - ); - }); - - test('sample Bulgarian text', () async { - await testSampleText( - 'bulgarian', - 'е елементарен примерен текст използван в печатарската и типографската индустрия', - ); - }); - - test('sample Catalan text', () async { - await testSampleText( - 'catalan', - 'és un text de farciment usat per la indústria de la tipografia i la impremta', - ); - }); - - test('sample English text', () async { - await testSampleText( - 'english', - 'Lorem Ipsum is simply dummy text of the printing and typesetting industry', - ); - }); - - test('sample Greek text', () async { - await testSampleText( - 'greek', - 'είναι απλά ένα κείμενο χωρίς νόημα για τους επαγγελματίες της τυπογραφίας και στοιχειοθεσίας', - ); - }); - - test('sample Hebrew text', () async { - await testSampleText( - 'hebrew', - 'זוהי עובדה מבוססת שדעתו של הקורא תהיה מוסחת על ידי טקטס קריא כאשר הוא יביט בפריסתו', - textDirection: ui.TextDirection.rtl, - ); - }); - - test('sample Hindi text', () async { - await testSampleText( - 'hindi', - 'छपाई और अक्षर योजन उद्योग का एक साधारण डमी पाठ है सन १५०० के बाद से अभी तक इस उद्योग का मानक डमी पाठ मन गया जब एक अज्ञात मुद्रक ने नमूना लेकर एक नमूना किताब बनाई', - ); - }); - - test('sample Thai text', () async { - await testSampleText( - 'thai', - 'คือ เนื้อหาจำลองแบบเรียบๆ ที่ใช้กันในธุรกิจงานพิมพ์หรืองานเรียงพิมพ์ มันได้กลายมาเป็นเนื้อหาจำลองมาตรฐานของธุรกิจดังกล่าวมาตั้งแต่ศตวรรษที่', - ); - }); - - test('sample Georgian text', () async { - await testSampleText( - 'georgian', - 'საბეჭდი და ტიპოგრაფიული ინდუსტრიის უშინაარსო ტექსტია. იგი სტანდარტად', - ); - }); - - test('sample Bengali text', () async { - await testSampleText( - 'bengali', - 'ঈদের জামাত মসজিদে, মানতে হবে স্বাস্থ্যবিধি: ধর্ম মন্ত্রণালয়', - ); - }); - - test('hindi svayan test', () async { - await testSampleText('hindi_svayan', 'स्वयं'); - }); - - // We've seen text break when we load many fonts simultaneously. This test - // combines text in multiple languages into one long paragraph to make sure - // we can handle it. - test('sample multilingual text', () async { - await testSampleText( - 'multilingual', - '也称乱数假文或者哑元文本, 是印刷及排版领域所常用的虚拟文字。 ' - 'տպագրության և տպագրական արդյունաբերության համար ' - 'është një tekst shabllon i industrisë së printimit ' - ' زمن طويل وهي أن المحتوى المقروء لصفحة ما سيلهي ' - 'е елементарен примерен текст използван в печатарската ' - 'és un text de farciment usat per la indústria de la ' - 'Lorem Ipsum is simply dummy text of the printing ' - 'είναι απλά ένα κείμενο χωρίς νόημα για τους επαγγελματίες ' - ' זוהי עובדה מבוססת שדעתו של הקורא תהיה מוסחת על ידי טקטס קריא ' - 'छपाई और अक्षर योजन उद्योग का एक साधारण डमी पाठ है सन ' - 'คือ เนื้อหาจำลองแบบเรียบๆ ที่ใช้กันในธุรกิจงานพิมพ์หรืองานเรียงพิมพ์ ' - 'საბეჭდი და ტიპოგრაფიული ინდუსტრიის უშინაარსო ტექსტია ', - ); - }); - - test('emoji text with skin tone', () async { - await testSampleText('emoji_with_skin_tone', '👋🏿 👋🏾 👋🏽 👋🏼 👋🏻'); - }, timeout: const Timeout.factor(2)); - // Make sure we clear the canvas in between frames. test('empty frame after contentful frame', () async { // First draw a frame with a red rectangle @@ -714,31 +218,6 @@ void testMain() { }); } -Future testSampleText(String language, String text, - {ui.TextDirection textDirection = ui.TextDirection.ltr}) async { - const double testWidth = 300; - double paragraphHeight = 0; - final CkPicture picture = await generatePictureWhenFontsStable(() { - final CkPictureRecorder recorder = CkPictureRecorder(); - final CkCanvas canvas = recorder.beginRecording(ui.Rect.largest); - final CkParagraphBuilder paragraphBuilder = - CkParagraphBuilder(CkParagraphStyle( - textDirection: textDirection, - )); - paragraphBuilder.addText(text); - final CkParagraph paragraph = paragraphBuilder.build(); - paragraph.layout(const ui.ParagraphConstraints(width: testWidth - 20)); - canvas.drawParagraph(paragraph, const ui.Offset(10, 10)); - paragraphHeight = paragraph.height; - return recorder.endRecording(); - }); - await matchPictureGolden( - 'canvaskit_sample_text_$language.png', - picture, - region: ui.Rect.fromLTRB(0, 0, testWidth, paragraphHeight + 20), - ); -} - typedef ParagraphFactory = CkParagraph Function(); void drawTestPicture(CkCanvas canvas) { @@ -1057,187 +536,3 @@ CkImage generateTestImage() { 4 * 20)!; return CkImage(skImage); } - -/// A convenience function for testing paragraph and text styles. -/// -/// Renders a paragraph with two pieces of text, [outerText] and [innerText]. -/// [outerText] is added to the root of the paragraph where only paragraph -/// style applies. [innerText] is added under a text style with properties -/// set from the arguments to this method. Parameters with prefix "paragraph" -/// are applied to the paragraph style. Others are applied to the text style. -/// -/// [name] is the name of the test used as the description on the golden as -/// well as in the golden file name. Avoid special characters. Spaces are OK; -/// they are replaced by "_" in the file name. -/// -/// Use [layoutWidth] to customize the width of the paragraph constraints. -Future testTextStyle( - // Test properties - String name, { - double? layoutWidth, - // Top-level text where only paragraph style applies - String outerText = 'Hello ', - // Second-level text where paragraph and text styles both apply. - String innerText = 'World!', - - // ParagraphStyle properties - ui.TextAlign? paragraphTextAlign, - ui.TextDirection? paragraphTextDirection, - int? paragraphMaxLines, - String? paragraphFontFamily, - double? paragraphFontSize, - double? paragraphHeight, - ui.TextHeightBehavior? paragraphTextHeightBehavior, - ui.FontWeight? paragraphFontWeight, - ui.FontStyle? paragraphFontStyle, - ui.StrutStyle? paragraphStrutStyle, - String? paragraphEllipsis, - ui.Locale? paragraphLocale, - - // TextStyle properties - ui.Color? color, - ui.TextDecoration? decoration, - ui.Color? decorationColor, - ui.TextDecorationStyle? decorationStyle, - double? decorationThickness, - ui.FontWeight? fontWeight, - ui.FontStyle? fontStyle, - ui.TextBaseline? textBaseline, - String? fontFamily, - List? fontFamilyFallback, - double? fontSize, - double? letterSpacing, - double? wordSpacing, - double? height, - ui.TextLeadingDistribution? leadingDistribution, - ui.Locale? locale, - CkPaint? background, - CkPaint? foreground, - List? shadows, - List? fontFeatures, -}) async { - late ui.Rect region; - CkPicture renderPicture() { - const double testWidth = 512; - final CkPictureRecorder recorder = CkPictureRecorder(); - final CkCanvas canvas = recorder.beginRecording(ui.Rect.largest); - canvas.translate(30, 10); - final CkParagraphBuilder descriptionBuilder = - CkParagraphBuilder(CkParagraphStyle()); - descriptionBuilder.addText(name); - final CkParagraph descriptionParagraph = descriptionBuilder.build(); - descriptionParagraph - .layout(const ui.ParagraphConstraints(width: testWidth / 2 - 70)); - const ui.Offset descriptionOffset = ui.Offset(testWidth / 2 + 30, 0); - canvas.drawParagraph(descriptionParagraph, descriptionOffset); - - final CkParagraphBuilder pb = CkParagraphBuilder(CkParagraphStyle( - textAlign: paragraphTextAlign, - textDirection: paragraphTextDirection, - maxLines: paragraphMaxLines, - fontFamily: paragraphFontFamily, - fontSize: paragraphFontSize, - height: paragraphHeight, - textHeightBehavior: paragraphTextHeightBehavior, - fontWeight: paragraphFontWeight, - fontStyle: paragraphFontStyle, - strutStyle: paragraphStrutStyle, - ellipsis: paragraphEllipsis, - locale: paragraphLocale, - )); - - pb.addText(outerText); - - pb.pushStyle(CkTextStyle( - color: color, - decoration: decoration, - decorationColor: decorationColor, - decorationStyle: decorationStyle, - decorationThickness: decorationThickness, - fontWeight: fontWeight, - fontStyle: fontStyle, - textBaseline: textBaseline, - fontFamily: fontFamily, - fontFamilyFallback: fontFamilyFallback, - fontSize: fontSize, - letterSpacing: letterSpacing, - wordSpacing: wordSpacing, - height: height, - leadingDistribution: leadingDistribution, - locale: locale, - background: background, - foreground: foreground, - shadows: shadows, - fontFeatures: fontFeatures, - )); - pb.addText(innerText); - pb.pop(); - final CkParagraph p = pb.build(); - p.layout(ui.ParagraphConstraints(width: layoutWidth ?? testWidth / 2)); - canvas.drawParagraph(p, ui.Offset.zero); - - canvas.drawPath( - CkPath() - ..moveTo(-10, 0) - ..lineTo(-20, 0) - ..lineTo(-20, p.height) - ..lineTo(-10, p.height), - CkPaint() - ..style = ui.PaintingStyle.stroke - ..strokeWidth = 1.0, - ); - canvas.drawPath( - CkPath() - ..moveTo(testWidth / 2 + 10, 0) - ..lineTo(testWidth / 2 + 20, 0) - ..lineTo(testWidth / 2 + 20, p.height) - ..lineTo(testWidth / 2 + 10, p.height), - CkPaint() - ..style = ui.PaintingStyle.stroke - ..strokeWidth = 1.0, - ); - const double padding = 20; - region = ui.Rect.fromLTRB( - 0, - 0, - testWidth, - math.max( - descriptionOffset.dy + descriptionParagraph.height + padding, - p.height + padding, - ), - ); - return recorder.endRecording(); - } - - // Render once to trigger font downloads. - final CkPicture picture = await generatePictureWhenFontsStable(renderPicture); - await matchPictureGolden( - 'canvaskit_text_styles_${name.replaceAll(' ', '_')}.png', - picture, - region: region, - ); - expect(notoDownloadQueue.debugIsLoadingFonts, isFalse); - expect(notoDownloadQueue.pendingFonts, isEmpty); - expect(notoDownloadQueue.downloader.debugActiveDownloadCount, 0); -} - -typedef PictureGenerator = CkPicture Function(); - -Future generatePictureWhenFontsStable( - PictureGenerator generator) async { - CkPicture picture = generator(); - // Fallback fonts start downloading as a post-frame callback. - CanvasKitRenderer.instance.rasterizer.debugRunPostFrameCallbacks(); - // Font downloading begins asynchronously so we inject a timer before checking the download queue. - await Future.delayed(Duration.zero); - while (notoDownloadQueue.isPending || - notoDownloadQueue.downloader.debugActiveDownloadCount > 0) { - await notoDownloadQueue.debugWhenIdle(); - await notoDownloadQueue.downloader.debugWhenIdle(); - picture = generator(); - CanvasKitRenderer.instance.rasterizer.debugRunPostFrameCallbacks(); - // Dummy timer for the same reason as above. - await Future.delayed(Duration.zero); - } - return picture; -} diff --git a/lib/web_ui/test/canvaskit/common.dart b/lib/web_ui/test/canvaskit/common.dart index 842d3a4a70a92..a8a2b04ce9469 100644 --- a/lib/web_ui/test/canvaskit/common.dart +++ b/lib/web_ui/test/canvaskit/common.dart @@ -22,19 +22,17 @@ void setUpCanvasKitTest() { setUpTestViewDimensions: false, ); - setUpAll(() { - // Ahem must be added to font fallbacks list regardless of where it was - // downloaded from. - FontFallbackData.instance.globalFontFallbacks.add('Ahem'); - }); - tearDown(() { HtmlViewEmbedder.instance.debugClear(); SurfaceFactory.instance.debugClear(); }); - setUp(() => notoDownloadQueue.downloader.fallbackFontUrlPrefixOverride = 'assets/fallback_fonts/'); - tearDown(() => notoDownloadQueue.downloader.fallbackFontUrlPrefixOverride = null); + setUp(() => + renderer.fontCollection.fontFallbackManager!.downloadQueue.fallbackFontUrlPrefixOverride + = 'assets/fallback_fonts/'); + tearDown(() => + renderer.fontCollection.fontFallbackManager!.downloadQueue.fallbackFontUrlPrefixOverride + = null); } /// Utility function for CanvasKit tests to draw pictures without diff --git a/lib/web_ui/test/canvaskit/font_variation_golden_test.dart b/lib/web_ui/test/canvaskit/font_variation_golden_test.dart deleted file mode 100644 index 3b856829dc0b0..0000000000000 --- a/lib/web_ui/test/canvaskit/font_variation_golden_test.dart +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:test/bootstrap/browser.dart'; -import 'package:test/test.dart'; - -import 'package:ui/src/engine.dart'; -import 'package:ui/ui.dart' as ui; - -import 'common.dart'; - -void main() { - internalBootstrapBrowserTest(() => testMain); -} - -void testMain() { - setUpAll(() async { - await ui.webOnlyInitializePlatform(); - }); - - group('font variation', () { - test('is correctly rendered', () async { - const double testWidth = 300; - final CkPictureRecorder recorder = CkPictureRecorder(); - final CkCanvas canvas = recorder.beginRecording(ui.Rect.largest); - final CkParagraphBuilder builder = - CkParagraphBuilder(CkParagraphStyle( - fontSize: 40.0, - textDirection: ui.TextDirection.ltr, - )); - - builder.pushStyle(CkTextStyle( - fontFamily: 'RobotoVariable', - )); - builder.addText('Normal\n'); - builder.pop(); - - ui.FontVariation weight(double w) => ui.FontVariation('wght', w); - builder.pushStyle(CkTextStyle( - fontFamily: 'RobotoVariable', - fontVariations: [weight(900)], - )); - builder.addText('Heavy\n'); - builder.pop(); - - builder.pushStyle(CkTextStyle( - fontFamily: 'RobotoVariable', - fontVariations: [weight(100)], - )); - builder.addText('Light\n'); - builder.pop(); - - final CkParagraph paragraph = builder.build(); - paragraph.layout(const ui.ParagraphConstraints(width: testWidth - 20)); - canvas.drawParagraph(paragraph, const ui.Offset(10, 10)); - final CkPicture picture = recorder.endRecording(); - await matchPictureGolden( - 'font_variation.png', - picture, - region: ui.Rect.fromLTRB(0, 0, testWidth, paragraph.height + 20), - ); - }); - }); -} diff --git a/lib/web_ui/test/canvaskit/interval_tree_test.dart b/lib/web_ui/test/canvaskit/interval_tree_test.dart index 39eb3ff917fd4..a106faa893e9e 100644 --- a/lib/web_ui/test/canvaskit/interval_tree_test.dart +++ b/lib/web_ui/test/canvaskit/interval_tree_test.dart @@ -13,9 +13,9 @@ void main() { void testMain() { group('$IntervalTree', () { test('is balanced', () { - final Map> ranges = >{ - 'A': const [CodeunitRange(0, 5), CodeunitRange(6, 10)], - 'B': const [CodeunitRange(4, 6)], + final Map> ranges = >{ + 'A': const [CodePointRange(0, 5), CodePointRange(6, 10)], + 'B': const [CodePointRange(4, 6)], }; // Should create a balanced 3-node tree with a root with a left and right @@ -30,23 +30,23 @@ void testMain() { expect(root.right!.right, isNull); // Should create a balanced 15-node tree (4 layers deep). - final Map> ranges2 = >{ - 'A': const [ - CodeunitRange(1, 1), - CodeunitRange(2, 2), - CodeunitRange(3, 3), - CodeunitRange(4, 4), - CodeunitRange(5, 5), - CodeunitRange(6, 6), - CodeunitRange(7, 7), - CodeunitRange(8, 8), - CodeunitRange(9, 9), - CodeunitRange(10, 10), - CodeunitRange(11, 11), - CodeunitRange(12, 12), - CodeunitRange(13, 13), - CodeunitRange(14, 14), - CodeunitRange(15, 15), + final Map> ranges2 = >{ + 'A': const [ + CodePointRange(1, 1), + CodePointRange(2, 2), + CodePointRange(3, 3), + CodePointRange(4, 4), + CodePointRange(5, 5), + CodePointRange(6, 6), + CodePointRange(7, 7), + CodePointRange(8, 8), + CodePointRange(9, 9), + CodePointRange(10, 10), + CodePointRange(11, 11), + CodePointRange(12, 12), + CodePointRange(13, 13), + CodePointRange(14, 14), + CodePointRange(15, 15), ], }; @@ -66,9 +66,9 @@ void testMain() { }); test('finds values whose intervals overlap with a given point', () { - final Map> ranges = >{ - 'A': const [CodeunitRange(0, 5), CodeunitRange(7, 10)], - 'B': const [CodeunitRange(4, 6)], + final Map> ranges = >{ + 'A': const [CodePointRange(0, 5), CodePointRange(7, 10)], + 'B': const [CodePointRange(4, 6)], }; final IntervalTree tree = IntervalTree.createFromRanges(ranges); diff --git a/lib/web_ui/test/common/test_initialization.dart b/lib/web_ui/test/common/test_initialization.dart index 812210624111e..427d47dcd3b02 100644 --- a/lib/web_ui/test/common/test_initialization.dart +++ b/lib/web_ui/test/common/test_initialization.dart @@ -27,10 +27,10 @@ void setUpUnitTests({ 'useColorEmoji': true, }) as engine.JsFlutterConfiguration); engine.debugSetConfiguration(config); - engine.notoDownloadQueue.downloader.fallbackFontUrlPrefixOverride = 'assets/fallback_fonts/'; debugFontsScope = configureDebugFontsAssetScope(fakeAssetManager); await engine.initializeEngine(assetManager: fakeAssetManager); + engine.renderer.fontCollection.fontFallbackManager?.downloadQueue.fallbackFontUrlPrefixOverride = 'assets/fallback_fonts/'; if (setUpTestViewDimensions) { // Force-initialize FlutterViewEmbedder so it doesn't overwrite test pixel ratio. diff --git a/lib/web_ui/test/canvaskit/fallback_fonts_golden_test.dart b/lib/web_ui/test/ui/fallback_fonts_golden_test.dart similarity index 55% rename from lib/web_ui/test/canvaskit/fallback_fonts_golden_test.dart rename to lib/web_ui/test/ui/fallback_fonts_golden_test.dart index f8bff49380d7f..8d84501cb1926 100644 --- a/lib/web_ui/test/canvaskit/fallback_fonts_golden_test.dart +++ b/lib/web_ui/test/ui/fallback_fonts_golden_test.dart @@ -2,17 +2,17 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; import 'dart:math' as math; -import 'dart:typed_data'; import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; +import 'package:web_engine_tester/golden_tester.dart'; -import 'common.dart'; +import '../common/test_initialization.dart'; +import 'utils.dart'; void main() { internalBootstrapBrowserTest(() => testMain); @@ -22,99 +22,102 @@ const ui.Rect kDefaultRegion = ui.Rect.fromLTRB(0, 0, 100, 100); void testMain() { group('Font fallbacks', () { - setUpCanvasKitTest(); + setUpUnitTests( + emulateTesterEnvironment: false, + setUpTestViewDimensions: false, + ); - setUpAll(() { + setUp(() { debugDisableFontFallbacks = false; }); /// Used to save and restore [ui.window.onPlatformMessage] after each test. ui.PlatformMessageCallback? savedCallback; + final List downloadedFontFamilies = []; + setUp(() { - FontFallbackData.debugReset(); - notoDownloadQueue.downloader.fallbackFontUrlPrefixOverride = 'assets/fallback_fonts/'; + renderer.fontCollection.debugResetFallbackFonts(); + renderer.fontCollection.fontFallbackManager!.downloadQueue.fallbackFontUrlPrefixOverride = 'assets/fallback_fonts/'; + renderer.fontCollection.fontFallbackManager!.downloadQueue.debugOnLoadFontFamily + = (String family) => downloadedFontFamilies.add(family); savedCallback = ui.window.onPlatformMessage; }); tearDown(() { + downloadedFontFamilies.clear(); ui.window.onPlatformMessage = savedCallback; }); test('Roboto is always a fallback font', () { - expect(FontFallbackData.instance.globalFontFallbacks, contains('Roboto')); + expect(renderer.fontCollection.fontFallbackManager!.globalFontFallbacks, contains('Roboto')); }); test('will download Noto Sans Arabic if Arabic text is added', () async { - final Rasterizer rasterizer = CanvasKitRenderer.instance.rasterizer; - expect(FontFallbackData.instance.globalFontFallbacks, ['Roboto']); + expect(renderer.fontCollection.fontFallbackManager!.globalFontFallbacks, ['Roboto']); // Creating this paragraph should cause us to start to download the // fallback font. - CkParagraphBuilder pb = CkParagraphBuilder( - CkParagraphStyle(), + ui.ParagraphBuilder pb = ui.ParagraphBuilder( + ui.ParagraphStyle(), ); pb.addText('مرحبا'); - rasterizer.debugRunPostFrameCallbacks(); - await notoDownloadQueue.debugWhenIdle(); + await renderer.fontCollection.fontFallbackManager!.debugWhenIdle(); - expect(FontFallbackData.instance.globalFontFallbacks, + expect(renderer.fontCollection.fontFallbackManager!.globalFontFallbacks, contains('Noto Sans Arabic')); - final CkPictureRecorder recorder = CkPictureRecorder(); - final CkCanvas canvas = recorder.beginRecording(kDefaultRegion); + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final ui.Canvas canvas = ui.Canvas(recorder); - pb = CkParagraphBuilder( - CkParagraphStyle(), + pb = ui.ParagraphBuilder( + ui.ParagraphStyle(), ); pb.pushStyle(ui.TextStyle(fontSize: 32)); pb.addText('مرحبا'); pb.pop(); - final CkParagraph paragraph = pb.build(); + final ui.Paragraph paragraph = pb.build(); paragraph.layout(const ui.ParagraphConstraints(width: 1000)); canvas.drawParagraph(paragraph, ui.Offset.zero); + await drawPictureUsingCurrentRenderer(recorder.endRecording()); - await matchPictureGolden( - 'canvaskit_font_fallback_arabic.png', - recorder.endRecording(), + await matchGoldenFile( + 'ui_font_fallback_arabic.png', region: kDefaultRegion, ); // TODO(hterkelsen): https://github.com/flutter/flutter/issues/71520 - }, skip: isSafari || isFirefox); + }); test('will put the Noto Color Emoji font before other fallback fonts in the list', () async { - final Rasterizer rasterizer = CanvasKitRenderer.instance.rasterizer; - expect(FontFallbackData.instance.globalFontFallbacks, ['Roboto']); + expect(renderer.fontCollection.fontFallbackManager!.globalFontFallbacks, ['Roboto']); // Creating this paragraph should cause us to start to download the // Arabic fallback font. - CkParagraphBuilder pb = CkParagraphBuilder( - CkParagraphStyle(), + ui.ParagraphBuilder pb = ui.ParagraphBuilder( + ui.ParagraphStyle(), ); pb.addText('مرحبا'); - rasterizer.debugRunPostFrameCallbacks(); - await notoDownloadQueue.debugWhenIdle(); + await renderer.fontCollection.fontFallbackManager!.debugWhenIdle(); - expect(FontFallbackData.instance.globalFontFallbacks, + expect(renderer.fontCollection.fontFallbackManager!.globalFontFallbacks, ['Roboto', 'Noto Sans Arabic']); - pb = CkParagraphBuilder( - CkParagraphStyle(), + pb = ui.ParagraphBuilder( + ui.ParagraphStyle(), ); pb.pushStyle(ui.TextStyle(fontSize: 26)); pb.addText('Hello 😊 مرحبا'); pb.pop(); - final CkParagraph paragraph = pb.build(); + final ui.Paragraph paragraph = pb.build(); paragraph.layout(const ui.ParagraphConstraints(width: 1000)); - rasterizer.debugRunPostFrameCallbacks(); - await notoDownloadQueue.debugWhenIdle(); + await renderer.fontCollection.fontFallbackManager!.debugWhenIdle(); - expect(FontFallbackData.instance.globalFontFallbacks, [ + expect(renderer.fontCollection.fontFallbackManager!.globalFontFallbacks, [ 'Roboto', 'Noto Color Emoji', 'Noto Sans Arabic', @@ -123,61 +126,54 @@ void testMain() { test('will download Noto Color Emojis and Noto Symbols if no matching Noto Font', () async { - final Rasterizer rasterizer = CanvasKitRenderer.instance.rasterizer; - expect(FontFallbackData.instance.globalFontFallbacks, ['Roboto']); + expect(renderer.fontCollection.fontFallbackManager!.globalFontFallbacks, ['Roboto']); // Creating this paragraph should cause us to start to download the // fallback font. - CkParagraphBuilder pb = CkParagraphBuilder( - CkParagraphStyle(), + ui.ParagraphBuilder pb = ui.ParagraphBuilder( + ui.ParagraphStyle(), ); pb.addText('Hello 😊'); - rasterizer.debugRunPostFrameCallbacks(); - await notoDownloadQueue.debugWhenIdle(); + await renderer.fontCollection.fontFallbackManager!.debugWhenIdle(); - expect(FontFallbackData.instance.globalFontFallbacks, + expect(renderer.fontCollection.fontFallbackManager!.globalFontFallbacks, contains('Noto Color Emoji')); - final CkPictureRecorder recorder = CkPictureRecorder(); - final CkCanvas canvas = recorder.beginRecording(kDefaultRegion); + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final ui.Canvas canvas = ui.Canvas(recorder); - pb = CkParagraphBuilder( - CkParagraphStyle(), + pb = ui.ParagraphBuilder( + ui.ParagraphStyle(), ); pb.pushStyle(ui.TextStyle(fontSize: 26)); pb.addText('Hello 😊'); pb.pop(); - final CkParagraph paragraph = pb.build(); + final ui.Paragraph paragraph = pb.build(); paragraph.layout(const ui.ParagraphConstraints(width: 1000)); canvas.drawParagraph(paragraph, ui.Offset.zero); + await drawPictureUsingCurrentRenderer(recorder.endRecording()); - await matchPictureGolden( - 'canvaskit_font_fallback_emoji.png', - recorder.endRecording(), + await matchGoldenFile( + 'ui_font_fallback_emoji.png', region: kDefaultRegion, ); // TODO(hterkelsen): https://github.com/flutter/flutter/issues/71520 - }, skip: isSafari || isFirefox); + }); // Regression test for https://github.com/flutter/flutter/issues/75836 // When we had this bug our font fallback resolution logic would end up in an // infinite loop and this test would freeze and time out. test( - 'Can find fonts for two adjacent unmatched code units from different fonts', + 'Can find fonts for two adjacent unmatched code points from different fonts', () async { - final Rasterizer rasterizer = CanvasKitRenderer.instance.rasterizer; - final LoggingDownloader loggingDownloader = - LoggingDownloader(NotoDownloader()); - notoDownloadQueue.downloader = loggingDownloader; // Try rendering text that requires fallback fonts, initially before the fonts are loaded. - CkParagraphBuilder(CkParagraphStyle()).addText('ヽಠ'); - rasterizer.debugRunPostFrameCallbacks(); - await notoDownloadQueue.debugWhenIdle(); + ui.ParagraphBuilder(ui.ParagraphStyle()).addText('ヽಠ'); + await renderer.fontCollection.fontFallbackManager!.debugWhenIdle(); expect( - loggingDownloader.log, + downloadedFontFamilies, [ 'Noto Sans SC', 'Noto Sans Kannada', @@ -185,60 +181,49 @@ void testMain() { ); // Do the same thing but this time with loaded fonts. - loggingDownloader.log.clear(); - CkParagraphBuilder(CkParagraphStyle()).addText('ヽಠ'); - rasterizer.debugRunPostFrameCallbacks(); - await notoDownloadQueue.debugWhenIdle(); - expect(loggingDownloader.log, isEmpty); + downloadedFontFamilies.clear(); + ui.ParagraphBuilder(ui.ParagraphStyle()).addText('ヽಠ'); + await renderer.fontCollection.fontFallbackManager!.debugWhenIdle(); + expect(downloadedFontFamilies, isEmpty); }); test('can find glyph for 2/3 symbol', () async { - final Rasterizer rasterizer = CanvasKitRenderer.instance.rasterizer; - final LoggingDownloader loggingDownloader = - LoggingDownloader(NotoDownloader()); - notoDownloadQueue.downloader = loggingDownloader; // Try rendering text that requires fallback fonts, initially before the fonts are loaded. - CkParagraphBuilder(CkParagraphStyle()).addText('⅔'); - rasterizer.debugRunPostFrameCallbacks(); - await notoDownloadQueue.debugWhenIdle(); + ui.ParagraphBuilder(ui.ParagraphStyle()).addText('⅔'); + await renderer.fontCollection.fontFallbackManager!.debugWhenIdle(); expect( - loggingDownloader.log, + downloadedFontFamilies, [ 'Noto Sans', ], ); // Do the same thing but this time with loaded fonts. - loggingDownloader.log.clear(); - CkParagraphBuilder(CkParagraphStyle()).addText('⅔'); - rasterizer.debugRunPostFrameCallbacks(); - await notoDownloadQueue.debugWhenIdle(); - expect(loggingDownloader.log, isEmpty); + downloadedFontFamilies.clear(); + ui.ParagraphBuilder(ui.ParagraphStyle()).addText('⅔'); + await renderer.fontCollection.fontFallbackManager!.debugWhenIdle(); + expect(downloadedFontFamilies, isEmpty); }); - test('findMinimumFontsForCodeunits for all supported code units', () async { - final LoggingDownloader loggingDownloader = - LoggingDownloader(NotoDownloader()); - notoDownloadQueue.downloader = loggingDownloader; - - // Collect all supported code units from all fallback fonts in the Noto + test('findMinimumFontsForCodePoints for all supported code points', () async { + // Collect all supported code points from all fallback fonts in the Noto // font tree. final Set testedFonts = {}; - final Set supportedUniqueCodeUnits = {}; + final Set supportedUniqueCodePoints = {}; final IntervalTree notoTree = - FontFallbackData.instance.notoTree; - for (final NotoFont font in FontFallbackData.instance.fallbackFonts) { + renderer.fontCollection.fontFallbackManager!.notoTree; + for (final NotoFont font in renderer.fontCollection.fontFallbackManager!.fallbackFonts) { testedFonts.add(font.name); - for (final CodeunitRange range in font.computeUnicodeRanges()) { - for (int codeUnit = range.start; codeUnit < range.end; codeUnit++) { - supportedUniqueCodeUnits.add(codeUnit); + for (final CodePointRange range in font.computeUnicodeRanges()) { + for (int codePoint = range.start; codePoint < range.end; codePoint++) { + supportedUniqueCodePoints.add(codePoint); } } } expect( - supportedUniqueCodeUnits.length, greaterThan(10000)); // sanity check + supportedUniqueCodePoints.length, greaterThan(10000)); // sanity check expect( testedFonts, unorderedEquals({ @@ -385,9 +370,9 @@ void testMain() { 'Noto Sans Zanabazar Square', })); - // Construct random paragraphs out of supported code units. + // Construct random paragraphs out of supported code points. final math.Random random = math.Random(0); - final List supportedCodeUnits = supportedUniqueCodeUnits.toList() + final List supportedCodePoints = supportedUniqueCodePoints.toList() ..shuffle(random); const int paragraphLength = 3; const int totalTestSize = 1000; @@ -396,27 +381,27 @@ void testMain() { batchStart < totalTestSize; batchStart += paragraphLength) { final int batchEnd = - math.min(batchStart + paragraphLength, supportedCodeUnits.length); - final Set codeUnits = {}; + math.min(batchStart + paragraphLength, supportedCodePoints.length); + final Set codePoints = {}; for (int i = batchStart; i < batchEnd; i += 1) { - codeUnits.add(supportedCodeUnits[i]); + codePoints.add(supportedCodePoints[i]); } final Set fonts = {}; - for (final int codeUnit in codeUnits) { - final List fontsForUnit = notoTree.intersections(codeUnit); + for (final int codePoint in codePoints) { + final List fontsForPoint = notoTree.intersections(codePoint); - // All code units are extracted from the same tree, so there must - // be at least one font supporting each code unit - expect(fontsForUnit, isNotEmpty); - fonts.addAll(fontsForUnit); + // All code points are extracted from the same tree, so there must + // be at least one font supporting each code point + expect(fontsForPoint, isNotEmpty); + fonts.addAll(fontsForPoint); } try { - FontFallbackData.instance.findMinimumFontsForCodeUnits(codeUnits, fonts); + renderer.fontCollection.fontFallbackManager!.findMinimumFontsForCodePoints(codePoints, fonts); } catch (e) { print( - 'findMinimumFontsForCodeunits failed:\n' - ' Code units: ${codeUnits.join(', ')}\n' + 'findMinimumFontsForCodePoints failed:\n' + ' Code points: ${codePoints.join(', ')}\n' ' Fonts: ${fonts.map((NotoFont f) => f.name).join(', ')}', ); rethrow; @@ -424,75 +409,7 @@ void testMain() { } }); }, - skip: isSafari, + // HTML renderer doesn't use the fallback font manager. + skip: isHtml, timeout: const Timeout.factor(4)); } - -class TestDownloader extends NotoDownloader { - // Where to redirect downloads to. - static final Map mockDownloads = {}; - @override - Future downloadAsString(String url, - {String? debugDescription}) async { - if (mockDownloads.containsKey(url)) { - url = mockDownloads[url]!; - final Uri uri = Uri.parse(url); - expect(uri.isScheme('http'), isFalse); - expect(uri.isScheme('https'), isFalse); - return super.downloadAsString(url); - } else { - return ''; - } - } - - @override - Future downloadAsBytes(String url, {String? debugDescription}) { - if (mockDownloads.containsKey(url)) { - url = mockDownloads[url]!; - final Uri uri = Uri.parse(url); - expect(uri.isScheme('http'), isFalse); - expect(uri.isScheme('https'), isFalse); - return super.downloadAsBytes(url); - } else { - return Future.value(Uint8List(0).buffer); - } - } -} - -class LoggingDownloader implements NotoDownloader { - LoggingDownloader(this.delegate); - - final List log = []; - - final NotoDownloader delegate; - - @override - Future debugWhenIdle() { - return delegate.debugWhenIdle(); - } - - @override - Future downloadAsBytes(String url, {String? debugDescription}) { - log.add(debugDescription ?? url); - return delegate.downloadAsBytes(url); - } - - @override - Future downloadAsString(String url, {String? debugDescription}) { - log.add(debugDescription ?? url); - return delegate.downloadAsString(url); - } - - @override - int get debugActiveDownloadCount => delegate.debugActiveDownloadCount; - - @override - String? get fallbackFontUrlPrefixOverride => - delegate.fallbackFontUrlPrefixOverride; - - @override set fallbackFontUrlPrefixOverride(String? override) => - delegate.fallbackFontUrlPrefixOverride; - - @override - String get fallbackFontUrlPrefix => delegate.fallbackFontUrlPrefix; -} diff --git a/lib/web_ui/test/ui/paragraph_builder_test.dart b/lib/web_ui/test/ui/paragraph_builder_test.dart index fe31eed072861..4139ebe593fa2 100644 --- a/lib/web_ui/test/ui/paragraph_builder_test.dart +++ b/lib/web_ui/test/ui/paragraph_builder_test.dart @@ -7,7 +7,6 @@ import 'package:test/test.dart'; import 'package:ui/ui.dart'; import '../common/test_initialization.dart'; -import 'utils.dart'; void main() { internalBootstrapBrowserTest(() => testMain); @@ -28,7 +27,7 @@ Future testMain() async { paragraph.layout(const ParagraphConstraints(width: 800.0)); expect(paragraph.width, isNonZero); expect(paragraph.height, isNonZero); - }, skip: isSkwasm); + }); test('the presence of foreground style should not throw', () { final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle()); @@ -38,5 +37,5 @@ Future testMain() async { builder.addText('hi'); expect(() => builder.build(), returnsNormally); - }, skip: isSkwasm); + }); } diff --git a/lib/web_ui/test/ui/text_golden_test.dart b/lib/web_ui/test/ui/text_golden_test.dart new file mode 100644 index 0000000000000..f77a583df5d79 --- /dev/null +++ b/lib/web_ui/test/ui/text_golden_test.dart @@ -0,0 +1,747 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math' as math; + +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; +import 'package:ui/src/engine.dart'; +import 'package:ui/ui.dart' as ui; +import 'package:web_engine_tester/golden_tester.dart'; + +import '../common/test_initialization.dart'; +import 'utils.dart'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +Future testMain() async { + setUpUnitTests( + emulateTesterEnvironment: false, + setUpTestViewDimensions: false, + ); + + test('text styles - default', () async { + await testTextStyle('default'); + }); + + test('text styles - center aligned', () async { + await testTextStyle('center aligned', + paragraphTextAlign: ui.TextAlign.center); + }); + + test('text styles - right aligned', () async { + await testTextStyle('right aligned', + paragraphTextAlign: ui.TextAlign.right); + }); + + test('text styles - rtl', () async { + await testTextStyle('rtl', paragraphTextDirection: ui.TextDirection.rtl); + }); + + test('text styles - multiline', () async { + await testTextStyle('multiline', layoutWidth: 50); + }); + + test('text styles - max lines', () async { + await testTextStyle('max lines', paragraphMaxLines: 1, layoutWidth: 50); + }); + + test('text styles - ellipsis', () async { + await testTextStyle('ellipsis', + paragraphMaxLines: 1, paragraphEllipsis: '...', layoutWidth: 60); + }); + + test('text styles - paragraph font family', () async { + await testTextStyle('paragraph font family', paragraphFontFamily: 'Ahem'); + }); + + test('text styles - paragraph font size', () async { + await testTextStyle('paragraph font size', paragraphFontSize: 22); + }); + + test('text styles - paragraph height', () async { + await testTextStyle('paragraph height', + layoutWidth: 50, paragraphHeight: 1.5); + }); + + test('text styles - paragraph text height behavior', () async { + await testTextStyle('paragraph text height behavior', + layoutWidth: 50, + paragraphHeight: 1.5, + paragraphTextHeightBehavior: const ui.TextHeightBehavior( + applyHeightToFirstAscent: false, + applyHeightToLastDescent: false, + )); + }); + + test('text styles - paragraph weight', () async { + await testTextStyle('paragraph weight', + paragraphFontWeight: ui.FontWeight.w900); + }); + + test('text style - paragraph font style', () async { + await testTextStyle( + 'paragraph font style', + paragraphFontStyle: ui.FontStyle.italic, + ); + }); + + // TODO(yjbanov): locales specified in paragraph styles don't work: + // https://github.com/flutter/flutter/issues/74687 + // TODO(yjbanov): spaces are not rendered correctly: + // https://github.com/flutter/flutter/issues/74742 + test('text styles - paragraph locale zh_CN', () async { + await testTextStyle('paragraph locale zh_CN', + outerText: '次 化 刃 直 入 令', + innerText: '', + paragraphLocale: const ui.Locale('zh', 'CN')); + }); + + test('text styles - paragraph locale zh_TW', () async { + await testTextStyle('paragraph locale zh_TW', + outerText: '次 化 刃 直 入 令', + innerText: '', + paragraphLocale: const ui.Locale('zh', 'TW')); + }); + + test('text styles - paragraph locale ja', () async { + await testTextStyle('paragraph locale ja', + outerText: '次 化 刃 直 入 令', + innerText: '', + paragraphLocale: const ui.Locale('ja')); + }); + + test('text styles - paragraph locale ko', () async { + await testTextStyle('paragraph locale ko', + outerText: '次 化 刃 直 入 令', + innerText: '', + paragraphLocale: const ui.Locale('ko')); + }); + + test('text styles - color', () async { + await testTextStyle('color', color: const ui.Color(0xFF009900)); + }); + + test('text styles - decoration', () async { + await testTextStyle('decoration', + decoration: ui.TextDecoration.underline); + }); + + test('text styles - decoration style', () async { + await testTextStyle('decoration style', + decoration: ui.TextDecoration.underline, + decorationStyle: ui.TextDecorationStyle.dashed); + }); + + test('text styles - decoration thickness', () async { + await testTextStyle('decoration thickness', + decoration: ui.TextDecoration.underline, decorationThickness: 5.0); + }); + + test('text styles - font weight', () async { + await testTextStyle('font weight', fontWeight: ui.FontWeight.w900); + }); + + test('text styles - font style', () async { + await testTextStyle('font style', fontStyle: ui.FontStyle.italic); + }); + + // TODO(yjbanov): not sure how to test this. + test('text styles - baseline', () async { + await testTextStyle('baseline', + textBaseline: ui.TextBaseline.ideographic); + }); + + test('text styles - font family', () async { + await testTextStyle('font family', fontFamily: 'Ahem'); + }); + + test('text styles - non-existent font family', () async { + await testTextStyle('non-existent font family', + fontFamily: 'DoesNotExist'); + }); + + test('text styles - family fallback', () async { + await testTextStyle('family fallback', + fontFamily: 'DoesNotExist', fontFamilyFallback: ['Ahem']); + }); + + test('text styles - font size', () async { + await testTextStyle('font size', fontSize: 24); + }); + + // A regression test for the special case when CanvasKit would default to + // a positive font size when Flutter specifies zero. + // + // See: https://github.com/flutter/flutter/issues/98248 + test('text styles - zero font size', () async { + // This only sets the inner text style, but not the paragraph style, so + // "Hello" should be visible, but "World!" should disappear. + await testTextStyle('zero font size', fontSize: 0); + + // This sets the paragraph font size to zero, but the inner text gets + // an explicit non-zero size that should override paragraph properties, + // so this time "Hello" should disappear, but "World!" should still be + // visible. + await testTextStyle('zero paragraph font size', paragraphFontSize: 0, fontSize: 14); + }); + + test('text styles - letter spacing', () async { + await testTextStyle('letter spacing', letterSpacing: 5); + }); + + test('text styles - word spacing', () async { + await testTextStyle('word spacing', + innerText: 'Beautiful World!', wordSpacing: 25); + }); + + test('text styles - height', () async { + await testTextStyle('height', height: 2); + }); + + test('text styles - leading distribution', () async { + await testTextStyle('half leading', + height: 20, + fontSize: 10, + leadingDistribution: ui.TextLeadingDistribution.even); + await testTextStyle( + 'half leading inherited from paragraph', + height: 20, + fontSize: 10, + paragraphTextHeightBehavior: const ui.TextHeightBehavior( + leadingDistribution: ui.TextLeadingDistribution.even, + ), + ); + await testTextStyle( + 'text style half leading overrides paragraph style half leading', + height: 20, + fontSize: 10, + leadingDistribution: ui.TextLeadingDistribution.proportional, + paragraphTextHeightBehavior: const ui.TextHeightBehavior( + leadingDistribution: ui.TextLeadingDistribution.even, + ), + ); + }); + + // TODO(yjbanov): locales specified in text styles don't work: + // https://github.com/flutter/flutter/issues/74687 + // TODO(yjbanov): spaces are not rendered correctly: + // https://github.com/flutter/flutter/issues/74742 + test('text styles - locale zh_CN', () async { + await testTextStyle('locale zh_CN', + innerText: '次 化 刃 直 入 令', + outerText: '', + locale: const ui.Locale('zh', 'CN')); + }); + + test('text styles - locale zh_TW', () async { + await testTextStyle('locale zh_TW', + innerText: '次 化 刃 直 入 令', + outerText: '', + locale: const ui.Locale('zh', 'TW')); + }); + + test('text styles - locale ja', () async { + await testTextStyle('locale ja', + innerText: '次 化 刃 直 入 令', + outerText: '', + locale: const ui.Locale('ja')); + }); + + test('text styles - locale ko', () async { + await testTextStyle('locale ko', + innerText: '次 化 刃 直 入 令', + outerText: '', + locale: const ui.Locale('ko')); + }); + + test('text styles - background', () async { + await testTextStyle('background', + background: ui.Paint()..color = const ui.Color(0xFF00FF00)); + }); + + test('text styles - foreground', () async { + await testTextStyle('foreground', + foreground: ui.Paint()..color = const ui.Color(0xFF0000FF)); + }); + + test('text styles - foreground and background', () async { + await testTextStyle( + 'foreground and background', + foreground: ui.Paint()..color = const ui.Color(0xFFFF5555), + background: ui.Paint()..color = const ui.Color(0xFF007700), + ); + }); + + test('text styles - background and color', () async { + await testTextStyle( + 'background and color', + color: const ui.Color(0xFFFFFF00), + background: ui.Paint()..color = const ui.Color(0xFF007700), + ); + }); + + test('text styles - shadows', () async { + await testTextStyle('shadows', shadows: [ + const ui.Shadow( + color: ui.Color(0xFF999900), + offset: ui.Offset(10, 10), + blurRadius: 5, + ), + const ui.Shadow( + color: ui.Color(0xFF009999), + offset: ui.Offset(-10, -10), + blurRadius: 10, + ), + ]); + }); + + test('text styles - old style figures', () async { + await testTextStyle( + 'old style figures', + paragraphFontFamily: 'Roboto', + paragraphFontSize: 24, + outerText: '0 1 2 3 4 5 ', + innerText: '0 1 2 3 4 5', + fontFeatures: [const ui.FontFeature.oldstyleFigures()], + ); + }); + + test('text styles - stylistic set 1', () async { + await testTextStyle( + 'stylistic set 1', + paragraphFontFamily: 'Roboto', + paragraphFontSize: 24, + outerText: 'g', + innerText: 'g', + fontFeatures: [ui.FontFeature.stylisticSet(1)], + ); + }); + + test('text styles - stylistic set 2', () async { + await testTextStyle( + 'stylistic set 2', + paragraphFontFamily: 'Roboto', + paragraphFontSize: 24, + outerText: 'α', + innerText: 'α', + fontFeatures: [ui.FontFeature.stylisticSet(2)], + ); + }); + + test('text styles - override font family', () async { + await testTextStyle( + 'override font family', + paragraphFontFamily: 'Ahem', + fontFamily: 'Roboto', + ); + }); + + test('text styles - override font size', () async { + await testTextStyle( + 'override font size', + paragraphFontSize: 36, + fontSize: 18, + ); + }); + + test('text style - override font weight', () async { + await testTextStyle( + 'override font weight', + paragraphFontWeight: ui.FontWeight.w900, + fontWeight: ui.FontWeight.normal, + ); + }); + + test('text style - override font style', () async { + await testTextStyle( + 'override font style', + paragraphFontStyle: ui.FontStyle.italic, + fontStyle: ui.FontStyle.normal, + ); + }); + + test('text style - characters from multiple fallback fonts', () async { + await testTextStyle( + 'multi-font characters', + // This character is claimed by multiple fonts. This test makes sure + // we can find a font supporting it. + outerText: '欢', + innerText: '', + ); + }); + + test('text style - symbols', () async { + // One of the CJK fonts loaded in one of the tests above also contains + // some of these symbols. To make sure the test produces predictable + // results we reset the fallback data forcing the engine to reload + // fallbacks, which for this test will only load Noto Symbols. + await testTextStyle( + 'symbols', + outerText: '← ↑ → ↓ ', + innerText: '', + ); + }); + + test('sample Chinese text', () async { + await testSampleText( + 'chinese', + '也称乱数假文或者哑元文本, ' + '是印刷及排版领域所常用的虚拟文字。' + '由于曾经一台匿名的打印机刻意打乱了' + '一盒印刷字体从而造出一本字体样品书', + ); + }); + + test('sample Armenian text', () async { + await testSampleText( + 'armenian', + 'տպագրության և տպագրական արդյունաբերության համար նախատեսված մոդելային տեքստ է', + ); + }); + + test('sample Albanian text', () async { + await testSampleText( + 'albanian', + 'është një tekst shabllon i industrisë së printimit dhe shtypshkronjave Lorem Ipsum ka qenë teksti shabllon', + ); + }); + + test('sample Arabic text', () async { + await testSampleText( + 'arabic', + 'هناك حقيقة مثبتة منذ زمن طويل وهي أن المحتوى المقروء لصفحة ما سيلهي', + textDirection: ui.TextDirection.rtl, + ); + }); + + test('sample Bulgarian text', () async { + await testSampleText( + 'bulgarian', + 'е елементарен примерен текст използван в печатарската и типографската индустрия', + ); + }); + + test('sample Catalan text', () async { + await testSampleText( + 'catalan', + 'és un text de farciment usat per la indústria de la tipografia i la impremta', + ); + }); + + test('sample English text', () async { + await testSampleText( + 'english', + 'Lorem Ipsum is simply dummy text of the printing and typesetting industry', + ); + }); + + test('sample Greek text', () async { + await testSampleText( + 'greek', + 'είναι απλά ένα κείμενο χωρίς νόημα για τους επαγγελματίες της τυπογραφίας και στοιχειοθεσίας', + ); + }); + + test('sample Hebrew text', () async { + await testSampleText( + 'hebrew', + 'זוהי עובדה מבוססת שדעתו של הקורא תהיה מוסחת על ידי טקטס קריא כאשר הוא יביט בפריסתו', + textDirection: ui.TextDirection.rtl, + ); + }); + + test('sample Hindi text', () async { + await testSampleText( + 'hindi', + 'छपाई और अक्षर योजन उद्योग का एक साधारण डमी पाठ है सन १५०० के बाद से अभी तक इस उद्योग का मानक डमी पाठ मन गया जब एक अज्ञात मुद्रक ने नमूना लेकर एक नमूना किताब बनाई', + ); + }); + + test('sample Thai text', () async { + await testSampleText( + 'thai', + 'คือ เนื้อหาจำลองแบบเรียบๆ ที่ใช้กันในธุรกิจงานพิมพ์หรืองานเรียงพิมพ์ มันได้กลายมาเป็นเนื้อหาจำลองมาตรฐานของธุรกิจดังกล่าวมาตั้งแต่ศตวรรษที่', + ); + }); + + test('sample Georgian text', () async { + await testSampleText( + 'georgian', + 'საბეჭდი და ტიპოგრაფიული ინდუსტრიის უშინაარსო ტექსტია. იგი სტანდარტად', + ); + }); + + test('sample Bengali text', () async { + await testSampleText( + 'bengali', + 'ঈদের জামাত মসজিদে, মানতে হবে স্বাস্থ্যবিধি: ধর্ম মন্ত্রণালয়', + ); + }); + + test('hindi svayan test', () async { + await testSampleText('hindi_svayan', 'स्वयं'); + }); + + // We've seen text break when we load many fonts simultaneously. This test + // combines text in multiple languages into one long paragraph to make sure + // we can handle it. + test('sample multilingual text', () async { + await testSampleText( + 'multilingual', + '也称乱数假文或者哑元文本, 是印刷及排版领域所常用的虚拟文字。 ' + 'տպագրության և տպագրական արդյունաբերության համար ' + 'është një tekst shabllon i industrisë së printimit ' + ' زمن طويل وهي أن المحتوى المقروء لصفحة ما سيلهي ' + 'е елементарен примерен текст използван в печатарската ' + 'és un text de farciment usat per la indústria de la ' + 'Lorem Ipsum is simply dummy text of the printing ' + 'είναι απλά ένα κείμενο χωρίς νόημα για τους επαγγελματίες ' + ' זוהי עובדה מבוססת שדעתו של הקורא תהיה מוסחת על ידי טקטס קריא ' + 'छपाई और अक्षर योजन उद्योग का एक साधारण डमी पाठ है सन ' + 'คือ เนื้อหาจำลองแบบเรียบๆ ที่ใช้กันในธุรกิจงานพิมพ์หรืองานเรียงพิมพ์ ' + 'საბეჭდი და ტიპოგრაფიული ინდუსტრიის უშინაარსო ტექსტია ', + ); + }); + + test('emoji text with skin tone', () async { + await testSampleText('emoji_with_skin_tone', '👋🏿 👋🏾 👋🏽 👋🏼 👋🏻'); + }, timeout: const Timeout.factor(2)); + + test('font variations are correctly rendered', () async { + const double testWidth = 300; + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final ui.Canvas canvas = ui.Canvas(recorder); + final ui.ParagraphBuilder builder = + ui.ParagraphBuilder(ui.ParagraphStyle( + fontSize: 40.0, + textDirection: ui.TextDirection.ltr, + )); + + builder.pushStyle(ui.TextStyle( + fontFamily: 'RobotoVariable', + )); + builder.addText('Normal\n'); + builder.pop(); + + ui.FontVariation weight(double w) => ui.FontVariation('wght', w); + builder.pushStyle(ui.TextStyle( + fontFamily: 'RobotoVariable', + fontVariations: [weight(900)], + )); + builder.addText('Heavy\n'); + builder.pop(); + + builder.pushStyle(ui.TextStyle( + fontFamily: 'RobotoVariable', + fontVariations: [weight(100)], + )); + builder.addText('Light\n'); + builder.pop(); + + final ui.Paragraph paragraph = builder.build(); + paragraph.layout(const ui.ParagraphConstraints(width: testWidth - 20)); + canvas.drawParagraph(paragraph, const ui.Offset(10, 10)); + final ui.Picture picture = recorder.endRecording(); + await drawPictureUsingCurrentRenderer(picture); + await matchGoldenFile( + 'ui_text_font_variation.png', + region: ui.Rect.fromLTRB(0, 0, testWidth, paragraph.height + 20), + ); + }); +} + +/// A convenience function for testing paragraph and text styles. +/// +/// Renders a paragraph with two pieces of text, [outerText] and [innerText]. +/// [outerText] is added to the root of the paragraph where only paragraph +/// style applies. [innerText] is added under a text style with properties +/// set from the arguments to this method. Parameters with prefix "paragraph" +/// are applied to the paragraph style. Others are applied to the text style. +/// +/// [name] is the name of the test used as the description on the golden as +/// well as in the golden file name. Avoid special characters. Spaces are OK; +/// they are replaced by "_" in the file name. +/// +/// Use [layoutWidth] to customize the width of the paragraph constraints. +Future testTextStyle( + // Test properties + String name, { + double? layoutWidth, + // Top-level text where only paragraph style applies + String outerText = 'Hello ', + // Second-level text where paragraph and text styles both apply. + String innerText = 'World!', + + // ParagraphStyle properties + ui.TextAlign? paragraphTextAlign, + ui.TextDirection? paragraphTextDirection, + int? paragraphMaxLines, + String? paragraphFontFamily, + double? paragraphFontSize, + double? paragraphHeight, + ui.TextHeightBehavior? paragraphTextHeightBehavior, + ui.FontWeight? paragraphFontWeight, + ui.FontStyle? paragraphFontStyle, + ui.StrutStyle? paragraphStrutStyle, + String? paragraphEllipsis, + ui.Locale? paragraphLocale, + + // TextStyle properties + ui.Color? color, + ui.TextDecoration? decoration, + ui.Color? decorationColor, + ui.TextDecorationStyle? decorationStyle, + double? decorationThickness, + ui.FontWeight? fontWeight, + ui.FontStyle? fontStyle, + ui.TextBaseline? textBaseline, + String? fontFamily, + List? fontFamilyFallback, + double? fontSize, + double? letterSpacing, + double? wordSpacing, + double? height, + ui.TextLeadingDistribution? leadingDistribution, + ui.Locale? locale, + ui.Paint? background, + ui.Paint? foreground, + List? shadows, + List? fontFeatures, +}) async { + late ui.Rect region; + ui.Picture renderPicture() { + const double testWidth = 512; + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final ui.Canvas canvas = ui.Canvas(recorder); + canvas.translate(30, 10); + final ui.ParagraphBuilder descriptionBuilder = + ui.ParagraphBuilder(ui.ParagraphStyle()); + descriptionBuilder.addText(name); + final ui.Paragraph descriptionParagraph = descriptionBuilder.build(); + descriptionParagraph + .layout(const ui.ParagraphConstraints(width: testWidth / 2 - 70)); + const ui.Offset descriptionOffset = ui.Offset(testWidth / 2 + 30, 0); + canvas.drawParagraph(descriptionParagraph, descriptionOffset); + + final ui.ParagraphBuilder pb = ui.ParagraphBuilder(ui.ParagraphStyle( + textAlign: paragraphTextAlign, + textDirection: paragraphTextDirection, + maxLines: paragraphMaxLines, + fontFamily: paragraphFontFamily, + fontSize: paragraphFontSize, + height: paragraphHeight, + textHeightBehavior: paragraphTextHeightBehavior, + fontWeight: paragraphFontWeight, + fontStyle: paragraphFontStyle, + strutStyle: paragraphStrutStyle, + ellipsis: paragraphEllipsis, + locale: paragraphLocale, + )); + + pb.addText(outerText); + + pb.pushStyle(ui.TextStyle( + color: color, + decoration: decoration, + decorationColor: decorationColor, + decorationStyle: decorationStyle, + decorationThickness: decorationThickness, + fontWeight: fontWeight, + fontStyle: fontStyle, + textBaseline: textBaseline, + fontFamily: fontFamily, + fontFamilyFallback: fontFamilyFallback, + fontSize: fontSize, + letterSpacing: letterSpacing, + wordSpacing: wordSpacing, + height: height, + leadingDistribution: leadingDistribution, + locale: locale, + background: background, + foreground: foreground, + shadows: shadows, + fontFeatures: fontFeatures, + )); + pb.addText(innerText); + pb.pop(); + final ui.Paragraph p = pb.build(); + p.layout(ui.ParagraphConstraints(width: layoutWidth ?? testWidth / 2)); + canvas.drawParagraph(p, ui.Offset.zero); + + canvas.drawPath( + ui.Path() + ..moveTo(-10, 0) + ..lineTo(-20, 0) + ..lineTo(-20, p.height) + ..lineTo(-10, p.height), + ui.Paint() + ..style = ui.PaintingStyle.stroke + ..strokeWidth = 1.0, + ); + canvas.drawPath( + ui.Path() + ..moveTo(testWidth / 2 + 10, 0) + ..lineTo(testWidth / 2 + 20, 0) + ..lineTo(testWidth / 2 + 20, p.height) + ..lineTo(testWidth / 2 + 10, p.height), + ui.Paint() + ..style = ui.PaintingStyle.stroke + ..strokeWidth = 1.0, + ); + const double padding = 20; + region = ui.Rect.fromLTRB( + 0, + 0, + testWidth, + math.max( + descriptionOffset.dy + descriptionParagraph.height + padding, + p.height + padding, + ), + ); + return recorder.endRecording(); + } + + // Render once to trigger font downloads. + renderPicture(); + await renderer.fontCollection.fontFallbackManager?.debugWhenIdle(); + final ui.Picture picture = renderPicture(); + await drawPictureUsingCurrentRenderer(picture); + + await matchGoldenFile( + 'ui_text_styles_${name.replaceAll(' ', '_')}.png', + region: region, + ); +} + +Future testSampleText(String language, String text, + {ui.TextDirection textDirection = ui.TextDirection.ltr}) async { + const double testWidth = 300; + double paragraphHeight = 0; + ui.Picture renderPicture() { + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final ui.Canvas canvas = ui.Canvas(recorder); + final ui.ParagraphBuilder paragraphBuilder = + ui.ParagraphBuilder(ui.ParagraphStyle( + textDirection: textDirection, + )); + paragraphBuilder.addText(text); + final ui.Paragraph paragraph = paragraphBuilder.build(); + paragraph.layout(const ui.ParagraphConstraints(width: testWidth - 20)); + canvas.drawParagraph(paragraph, const ui.Offset(10, 10)); + paragraphHeight = paragraph.height; + return recorder.endRecording(); + } + // Render once to trigger font downloads. + renderPicture(); + await renderer.fontCollection.fontFallbackManager?.debugWhenIdle(); + final ui.Picture picture = renderPicture(); + await drawPictureUsingCurrentRenderer(picture); + await matchGoldenFile( + 'ui_sample_text_$language.png', + region: ui.Rect.fromLTRB(0, 0, testWidth, paragraphHeight + 20), + ); +} diff --git a/third_party/canvaskit/BUILD.gn b/third_party/canvaskit/BUILD.gn index af6b617bd8624..d4d0801220f82 100644 --- a/third_party/canvaskit/BUILD.gn +++ b/third_party/canvaskit/BUILD.gn @@ -71,11 +71,13 @@ copy("canvaskit_chromium_group") { # This toolchain is only to be used by skwasm_group below. wasm_toolchain("skwasm") { extra_toolchain_args = { - # In Chromium browsers, we can use the browser's APIs to get the necessary - # ICU data. - skia_use_icu = false - skia_use_client_icu = true - skia_icu_bidi_third_party_dir = "//flutter/third_party/canvaskit/icu_bidi" + # Include ICU data. Eventually we'd like to omit this, like we do in the + # canvaskit_chromium build, but as of right now the client ICU APIs are + # only available in private skia headers, which we can't include in our own + # sources. Once skia provides this capability in their public headers, we + # can use client ICU instead. + skia_use_icu = true + skia_use_client_icu = false skia_use_libjpeg_turbo_decode = false skia_use_libpng_decode = false