mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Automatically download Noto fonts as backup fonts in CanvasKit mode (flutter/engine#23728)
* Revert "Revert "[CanvasKit] Automatically fall back to Noto fonts (#23096)" (#23357)" This reverts commit b00fa0f20a358c2e78d54a0c7d2e5c93c4234880. * WIP * Use an Interval Tree to store the unicode ranges for the Noto Fonts * Update licenses * Remove debug print statements * Respond to comments * Fix analysis error * Add tests * Respond to comments * Fix test * Update goldens lock * Skip screenshot test on Safari * Skip CanvasKit tests on iOS Safari * Move CanvasKit initialization so it doesn't run on iOS Safari
This commit is contained in:
parent
44a8785d64
commit
51757a7d4b
@ -434,10 +434,12 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart
|
||||
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/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/initialization.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
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
repository: https://github.com/flutter/goldens.git
|
||||
revision: 999507db8c924635a605325252702bad661e2ad2
|
||||
revision: bdb442c42588b25c657779c78523822e349742d5
|
||||
|
||||
@ -234,7 +234,7 @@ class TestCommand extends Command<bool> with ArgUtils {
|
||||
Future<bool> runIntegrationTests() async {
|
||||
// Parse additional arguments specific for integration testing.
|
||||
IntegrationTestsArgumentParser.instance.parseOptions(argResults);
|
||||
if(!_testPreparationReady) {
|
||||
if (!_testPreparationReady) {
|
||||
await _prepare();
|
||||
}
|
||||
return IntegrationTestsManager(
|
||||
@ -522,9 +522,12 @@ class TestCommand extends Command<bool> with ArgUtils {
|
||||
}
|
||||
|
||||
// All files under test/golden_tests are considered golden tests.
|
||||
final bool isUnderGoldenTestsDirectory = path.split(testFilePath.relativeToWebUi).contains('golden_tests');
|
||||
final bool isUnderGoldenTestsDirectory =
|
||||
path.split(testFilePath.relativeToWebUi).contains('golden_tests');
|
||||
// Any file whose name ends with "_golden_test.dart" is run as a golden test.
|
||||
final bool isGoldenTestFile = path.basename(testFilePath.relativeToWebUi).endsWith('_golden_test.dart');
|
||||
final bool isGoldenTestFile = path
|
||||
.basename(testFilePath.relativeToWebUi)
|
||||
.endsWith('_golden_test.dart');
|
||||
if (isUnderGoldenTestsDirectory || isGoldenTestFile) {
|
||||
screenshotTestFiles.add(testFilePath);
|
||||
} else {
|
||||
@ -767,7 +770,11 @@ class TestCommand extends Command<bool> with ArgUtils {
|
||||
}
|
||||
}
|
||||
|
||||
const List<String> _kTestFonts = <String>['ahem.ttf', 'Roboto-Regular.ttf'];
|
||||
const List<String> _kTestFonts = <String>[
|
||||
'ahem.ttf',
|
||||
'Roboto-Regular.ttf',
|
||||
'NotoNaskhArabic-Regular.ttf',
|
||||
];
|
||||
|
||||
void _copyTestFontsIntoWebUi() {
|
||||
final String fontsPath = path.join(
|
||||
|
||||
@ -32,9 +32,11 @@ part 'engine/canvaskit/canvaskit_api.dart';
|
||||
part 'engine/canvaskit/color_filter.dart';
|
||||
part 'engine/canvaskit/embedded_views.dart';
|
||||
part 'engine/canvaskit/fonts.dart';
|
||||
part 'engine/canvaskit/font_fallbacks.dart';
|
||||
part 'engine/canvaskit/image.dart';
|
||||
part 'engine/canvaskit/image_filter.dart';
|
||||
part 'engine/canvaskit/initialization.dart';
|
||||
part 'engine/canvaskit/interval_tree.dart';
|
||||
part 'engine/canvaskit/layer.dart';
|
||||
part 'engine/canvaskit/layer_scene_builder.dart';
|
||||
part 'engine/canvaskit/layer_tree.dart';
|
||||
|
||||
@ -1177,7 +1177,8 @@ class SkPath {
|
||||
|
||||
/// Serializes the path into a list of commands.
|
||||
///
|
||||
/// The list can be used to create a new [SkPath] using [CanvasKit.Path.MakeFromCmds].
|
||||
/// The list can be used to create a new [SkPath] using
|
||||
/// [CanvasKit.Path.MakeFromCmds].
|
||||
external List<dynamic> toCmds();
|
||||
|
||||
external void delete();
|
||||
|
||||
@ -0,0 +1,684 @@
|
||||
// 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.
|
||||
|
||||
// @dart = 2.12
|
||||
part of engine;
|
||||
|
||||
/// Whether or not "Noto Sans Symbols" and "Noto Color Emoji" fonts have been
|
||||
/// downloaded. We download these as fallbacks when no other font covers the
|
||||
/// given code units.
|
||||
bool _registeredSymbolsAndEmoji = false;
|
||||
|
||||
final Set<int> codeUnitsWithNoKnownFont = <int>{};
|
||||
|
||||
Future<void> _findFontsForMissingCodeunits(List<int> codeunits) async {
|
||||
_ensureNotoFontTreeCreated();
|
||||
|
||||
// If all of the code units are known to have no Noto Font which covers them,
|
||||
// then just give up. We have already logged a warning.
|
||||
if (codeunits.every((u) => codeUnitsWithNoKnownFont.contains(u))) {
|
||||
return;
|
||||
}
|
||||
Set<_NotoFont> fonts = <_NotoFont>{};
|
||||
Set<int> coveredCodeUnits = <int>{};
|
||||
Set<int> missingCodeUnits = <int>{};
|
||||
for (int codeunit in codeunits) {
|
||||
List<_NotoFont> fontsForUnit = _notoTree!.intersections(codeunit);
|
||||
fonts.addAll(fontsForUnit);
|
||||
if (fontsForUnit.isNotEmpty) {
|
||||
coveredCodeUnits.add(codeunit);
|
||||
} else {
|
||||
missingCodeUnits.add(codeunit);
|
||||
}
|
||||
}
|
||||
|
||||
fonts = _findMinimumFontsForCodeunits(coveredCodeUnits, fonts);
|
||||
|
||||
for (_NotoFont font in fonts) {
|
||||
await font.ensureResolved();
|
||||
}
|
||||
|
||||
Set<_ResolvedNotoSubset> resolvedFonts = <_ResolvedNotoSubset>{};
|
||||
for (int codeunit in coveredCodeUnits) {
|
||||
for (_NotoFont font in fonts) {
|
||||
if (font.resolvedFont == null) {
|
||||
// We failed to resolve the font earlier.
|
||||
continue;
|
||||
}
|
||||
resolvedFonts.addAll(font.resolvedFont!.tree.intersections(codeunit));
|
||||
}
|
||||
}
|
||||
|
||||
for (_ResolvedNotoSubset resolvedFont in resolvedFonts) {
|
||||
notoDownloadQueue.add(resolvedFont);
|
||||
}
|
||||
|
||||
if (missingCodeUnits.isNotEmpty && !notoDownloadQueue.isPending) {
|
||||
if (!_registeredSymbolsAndEmoji) {
|
||||
_registerSymbolsAndEmoji();
|
||||
} else {
|
||||
if (!notoDownloadQueue.isPending) {
|
||||
html.window.console.log(
|
||||
'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');
|
||||
codeUnitsWithNoKnownFont.addAll(missingCodeUnits);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the CSS file for a font and make a list of resolved subsets.
|
||||
///
|
||||
/// A CSS file from Google Fonts looks like this:
|
||||
///
|
||||
/// /* [0] */
|
||||
/// @font-face {
|
||||
/// font-family: 'Noto Sans KR';
|
||||
/// font-style: normal;
|
||||
/// font-weight: 400;
|
||||
/// src: url(https://fonts.gstatic.com/s/notosanskr/v13/PbykFmXiEBPT4ITbgNA5Cgm20xz64px_1hVWr0wuPNGmlQNMEfD4.0.woff2) format('woff2');
|
||||
/// unicode-range: U+f9ca-fa0b, U+ff03-ff05, U+ff07, U+ff0a-ff0b, U+ff0d-ff19, U+ff1b, U+ff1d, U+ff20-ff5b, U+ff5d, U+ffe0-ffe3, U+ffe5-ffe6;
|
||||
/// }
|
||||
/// /* [1] */
|
||||
/// @font-face {
|
||||
/// font-family: 'Noto Sans KR';
|
||||
/// font-style: normal;
|
||||
/// font-weight: 400;
|
||||
/// src: url(https://fonts.gstatic.com/s/notosanskr/v13/PbykFmXiEBPT4ITbgNA5Cgm20xz64px_1hVWr0wuPNGmlQNMEfD4.1.woff2) format('woff2');
|
||||
/// unicode-range: U+f92f-f980, U+f982-f9c9;
|
||||
/// }
|
||||
/// /* [2] */
|
||||
/// @font-face {
|
||||
/// font-family: 'Noto Sans KR';
|
||||
/// font-style: normal;
|
||||
/// font-weight: 400;
|
||||
/// src: url(https://fonts.gstatic.com/s/notosanskr/v13/PbykFmXiEBPT4ITbgNA5Cgm20xz64px_1hVWr0wuPNGmlQNMEfD4.2.woff2) format('woff2');
|
||||
/// unicode-range: U+d723-d728, U+d72a-d733, U+d735-d748, U+d74a-d74f, U+d752-d753, U+d755-d757, U+d75a-d75f, U+d762-d764, U+d766-d768, U+d76a-d76b, U+d76d-d76f, U+d771-d787, U+d789-d78b, U+d78d-d78f, U+d791-d797, U+d79a, U+d79c, U+d79e-d7a3, U+f900-f909, U+f90b-f92e;
|
||||
/// }
|
||||
_ResolvedNotoFont? _makeResolvedNotoFontFromCss(String css, String name) {
|
||||
List<_ResolvedNotoSubset> subsets = <_ResolvedNotoSubset>[];
|
||||
bool resolvingFontFace = false;
|
||||
String? fontFaceUrl;
|
||||
List<CodeunitRange>? fontFaceUnicodeRanges;
|
||||
for (final String line in LineSplitter.split(css)) {
|
||||
// Search for the beginning of a @font-face.
|
||||
if (!resolvingFontFace) {
|
||||
if (line == '@font-face {') {
|
||||
resolvingFontFace = true;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// We are resolving a @font-face, read out the url and ranges.
|
||||
if (line.startsWith(' src:')) {
|
||||
int urlStart = line.indexOf('url(');
|
||||
if (urlStart == -1) {
|
||||
html.window.console.warn('Unable to resolve Noto font URL: $line');
|
||||
return null;
|
||||
}
|
||||
int urlEnd = line.indexOf(')');
|
||||
fontFaceUrl = line.substring(urlStart + 4, urlEnd);
|
||||
} else if (line.startsWith(' unicode-range:')) {
|
||||
fontFaceUnicodeRanges = <CodeunitRange>[];
|
||||
String rangeString = line.substring(17, line.length - 1);
|
||||
List<String> rawRanges = rangeString.split(', ');
|
||||
for (final String rawRange in rawRanges) {
|
||||
List<String> startEnd = rawRange.split('-');
|
||||
if (startEnd.length == 1) {
|
||||
String singleRange = startEnd.single;
|
||||
assert(singleRange.startsWith('U+'));
|
||||
int rangeValue = int.parse(singleRange.substring(2), radix: 16);
|
||||
fontFaceUnicodeRanges.add(CodeunitRange(rangeValue, rangeValue));
|
||||
} else {
|
||||
assert(startEnd.length == 2);
|
||||
String startRange = startEnd[0];
|
||||
String endRange = startEnd[1];
|
||||
assert(startRange.startsWith('U+'));
|
||||
int startValue = int.parse(startRange.substring(2), radix: 16);
|
||||
int endValue = int.parse(endRange, radix: 16);
|
||||
fontFaceUnicodeRanges.add(CodeunitRange(startValue, endValue));
|
||||
}
|
||||
}
|
||||
} else if (line == '}') {
|
||||
if (fontFaceUrl == null || fontFaceUnicodeRanges == null) {
|
||||
html.window.console.warn('Unable to parse Google Fonts CSS: $css');
|
||||
return null;
|
||||
}
|
||||
subsets
|
||||
.add(_ResolvedNotoSubset(fontFaceUrl, name, fontFaceUnicodeRanges));
|
||||
resolvingFontFace = false;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (resolvingFontFace) {
|
||||
html.window.console.warn('Unable to parse Google Fonts CSS: $css');
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<_ResolvedNotoSubset, List<CodeunitRange>> rangesMap =
|
||||
<_ResolvedNotoSubset, List<CodeunitRange>>{};
|
||||
for (_ResolvedNotoSubset subset in subsets) {
|
||||
for (CodeunitRange range in subset.ranges) {
|
||||
rangesMap.putIfAbsent(subset, () => <CodeunitRange>[]).add(range);
|
||||
}
|
||||
}
|
||||
|
||||
IntervalTree<_ResolvedNotoSubset> tree =
|
||||
IntervalTree<_ResolvedNotoSubset>.createFromRanges(rangesMap);
|
||||
|
||||
return _ResolvedNotoFont(name, subsets, tree);
|
||||
}
|
||||
|
||||
/// In the case where none of the known Noto Fonts cover a set of code units,
|
||||
/// try the Symbols and Emoji fonts. We don't know the exact range of code units
|
||||
/// that are covered by these fonts, so we download them and hope for the best.
|
||||
Future<void> _registerSymbolsAndEmoji() async {
|
||||
if (_registeredSymbolsAndEmoji) {
|
||||
return;
|
||||
}
|
||||
_registeredSymbolsAndEmoji = true;
|
||||
const String symbolsUrl =
|
||||
'https://fonts.googleapis.com/css2?family=Noto+Sans+Symbols';
|
||||
const String emojiUrl =
|
||||
'https://fonts.googleapis.com/css2?family=Noto+Color+Emoji+Compat';
|
||||
|
||||
String symbolsCss =
|
||||
await notoDownloadQueue.downloader.downloadAsString(symbolsUrl);
|
||||
String emojiCss =
|
||||
await notoDownloadQueue.downloader.downloadAsString(emojiUrl);
|
||||
|
||||
String? extractUrlFromCss(String css) {
|
||||
for (final String line in LineSplitter.split(css)) {
|
||||
if (line.startsWith(' src:')) {
|
||||
int urlStart = line.indexOf('url(');
|
||||
if (urlStart == -1) {
|
||||
html.window.console.warn('Unable to resolve Noto font URL: $line');
|
||||
return null;
|
||||
}
|
||||
int urlEnd = line.indexOf(')');
|
||||
return line.substring(urlStart + 4, urlEnd);
|
||||
}
|
||||
}
|
||||
html.window.console.warn('Unable to determine URL for Noto font');
|
||||
return null;
|
||||
}
|
||||
|
||||
String? symbolsFontUrl = extractUrlFromCss(symbolsCss);
|
||||
String? emojiFontUrl = extractUrlFromCss(emojiCss);
|
||||
|
||||
if (symbolsFontUrl == null || emojiFontUrl == null) {
|
||||
html.window.console
|
||||
.warn('Error parsing CSS for Noto Emoji and Symbols font.');
|
||||
}
|
||||
|
||||
notoDownloadQueue.add(_ResolvedNotoSubset(
|
||||
symbolsFontUrl!, 'Noto Sans Symbols', const <CodeunitRange>[]));
|
||||
notoDownloadQueue.add(_ResolvedNotoSubset(
|
||||
emojiFontUrl!, 'Noto Color Emoji Compat', const <CodeunitRange>[]));
|
||||
}
|
||||
|
||||
/// Finds the minimum set of fonts which covers all of the [codeunits].
|
||||
///
|
||||
/// Since set cover is NP-complete, we approximate using a greedy algorithm
|
||||
/// which finds the font which covers the most codeunits. If multiple CJK
|
||||
/// fonts match the same number of codeunits, we choose one based on the user's
|
||||
/// locale.
|
||||
Set<_NotoFont> _findMinimumFontsForCodeunits(
|
||||
Iterable<int> codeunits, Set<_NotoFont> fonts) {
|
||||
List<int> unmatchedCodeunits = List<int>.from(codeunits);
|
||||
Set<_NotoFont> minimumFonts = <_NotoFont>{};
|
||||
List<_NotoFont> bestFonts = <_NotoFont>[];
|
||||
int maxCodeunitsCovered = 0;
|
||||
|
||||
String language = html.window.navigator.language;
|
||||
|
||||
// This is guaranteed to terminate because [codeunits] is a list of fonts
|
||||
// which we've already determined are covered by [fonts].
|
||||
while (unmatchedCodeunits.isNotEmpty) {
|
||||
for (var font in fonts) {
|
||||
int codeunitsCovered = 0;
|
||||
for (int codeunit in unmatchedCodeunits) {
|
||||
if (font.matchesCodeunit(codeunit)) {
|
||||
codeunitsCovered++;
|
||||
}
|
||||
}
|
||||
if (codeunitsCovered > maxCodeunitsCovered) {
|
||||
bestFonts.clear();
|
||||
bestFonts.add(font);
|
||||
maxCodeunitsCovered = codeunitsCovered;
|
||||
} else if (codeunitsCovered == maxCodeunitsCovered) {
|
||||
bestFonts.add(font);
|
||||
}
|
||||
}
|
||||
assert(bestFonts.isNotEmpty);
|
||||
// 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((font) => _cjkFonts.contains(font))) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
unmatchedCodeunits
|
||||
.removeWhere((codeunit) => bestFont.matchesCodeunit(codeunit));
|
||||
minimumFonts.add(bestFont);
|
||||
}
|
||||
return minimumFonts;
|
||||
}
|
||||
|
||||
void _ensureNotoFontTreeCreated() {
|
||||
if (_notoTree != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<_NotoFont, List<CodeunitRange>> ranges =
|
||||
<_NotoFont, List<CodeunitRange>>{};
|
||||
|
||||
for (_NotoFont font in _notoFonts) {
|
||||
for (CodeunitRange range in font.unicodeRanges) {
|
||||
ranges.putIfAbsent(font, () => <CodeunitRange>[]).add(range);
|
||||
}
|
||||
}
|
||||
|
||||
_notoTree = IntervalTree<_NotoFont>.createFromRanges(ranges);
|
||||
}
|
||||
|
||||
class _NotoFont {
|
||||
final String name;
|
||||
final List<CodeunitRange> unicodeRanges;
|
||||
|
||||
Completer<void>? _decodingCompleter;
|
||||
|
||||
_ResolvedNotoFont? resolvedFont;
|
||||
|
||||
_NotoFont(this.name, this.unicodeRanges);
|
||||
|
||||
bool matchesCodeunit(int codeunit) {
|
||||
for (CodeunitRange range in unicodeRanges) {
|
||||
if (range.contains(codeunit)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
String get googleFontsCssUrl =>
|
||||
'https://fonts.googleapis.com/css2?family=${name.replaceAll(' ', '+')}';
|
||||
|
||||
Future<void> ensureResolved() async {
|
||||
if (resolvedFont == null) {
|
||||
if (_decodingCompleter == null) {
|
||||
_decodingCompleter = Completer<void>();
|
||||
String googleFontCss = await notoDownloadQueue.downloader
|
||||
.downloadAsString(googleFontsCssUrl);
|
||||
final _ResolvedNotoFont? googleFont =
|
||||
_makeResolvedNotoFontFromCss(googleFontCss, name);
|
||||
resolvedFont = googleFont;
|
||||
_decodingCompleter!.complete();
|
||||
} else {
|
||||
await _decodingCompleter!.future;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CodeunitRange {
|
||||
final int start;
|
||||
final int end;
|
||||
|
||||
const CodeunitRange(this.start, this.end);
|
||||
|
||||
bool contains(int codeUnit) {
|
||||
return start <= codeUnit && codeUnit <= end;
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
if (other is! CodeunitRange) {
|
||||
return false;
|
||||
}
|
||||
CodeunitRange range = other;
|
||||
return range.start == start && range.end == end;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => ui.hashValues(start, end);
|
||||
|
||||
@override
|
||||
String toString() => '[$start, $end]';
|
||||
}
|
||||
|
||||
class _ResolvedNotoFont {
|
||||
final String name;
|
||||
final List<_ResolvedNotoSubset> subsets;
|
||||
final IntervalTree<_ResolvedNotoSubset> tree;
|
||||
|
||||
const _ResolvedNotoFont(this.name, this.subsets, this.tree);
|
||||
}
|
||||
|
||||
class _ResolvedNotoSubset {
|
||||
final String url;
|
||||
final String family;
|
||||
final List<CodeunitRange> ranges;
|
||||
|
||||
_ResolvedNotoSubset(this.url, this.family, this.ranges);
|
||||
}
|
||||
|
||||
_NotoFont _notoSansSC = _NotoFont('Noto Sans SC', <CodeunitRange>[
|
||||
CodeunitRange(12288, 12591),
|
||||
CodeunitRange(12800, 13311),
|
||||
CodeunitRange(19968, 40959),
|
||||
CodeunitRange(65072, 65135),
|
||||
CodeunitRange(65280, 65519),
|
||||
]);
|
||||
|
||||
_NotoFont _notoSansTC = _NotoFont('Noto Sans TC', <CodeunitRange>[
|
||||
CodeunitRange(12288, 12351),
|
||||
CodeunitRange(12549, 12585),
|
||||
CodeunitRange(19968, 40959),
|
||||
]);
|
||||
|
||||
_NotoFont _notoSansHK = _NotoFont('Noto Sans HK', <CodeunitRange>[
|
||||
CodeunitRange(12288, 12351),
|
||||
CodeunitRange(12549, 12585),
|
||||
CodeunitRange(19968, 40959),
|
||||
]);
|
||||
|
||||
_NotoFont _notoSansJP = _NotoFont('Noto Sans JP', <CodeunitRange>[
|
||||
CodeunitRange(12288, 12543),
|
||||
CodeunitRange(19968, 40959),
|
||||
CodeunitRange(65280, 65519),
|
||||
]);
|
||||
|
||||
List<_NotoFont> _cjkFonts = <_NotoFont>[
|
||||
_notoSansSC,
|
||||
_notoSansTC,
|
||||
_notoSansHK,
|
||||
_notoSansJP,
|
||||
];
|
||||
|
||||
List<_NotoFont> _notoFonts = <_NotoFont>[
|
||||
_notoSansSC,
|
||||
_notoSansTC,
|
||||
_notoSansHK,
|
||||
_notoSansJP,
|
||||
_NotoFont('Noto Naskh Arabic UI', <CodeunitRange>[
|
||||
CodeunitRange(1536, 1791),
|
||||
CodeunitRange(8204, 8206),
|
||||
CodeunitRange(8208, 8209),
|
||||
CodeunitRange(8271, 8271),
|
||||
CodeunitRange(11841, 11841),
|
||||
CodeunitRange(64336, 65023),
|
||||
CodeunitRange(65132, 65276),
|
||||
]),
|
||||
_NotoFont('Noto Sans Armenian', <CodeunitRange>[
|
||||
CodeunitRange(1328, 1424),
|
||||
CodeunitRange(64275, 64279),
|
||||
]),
|
||||
_NotoFont('Noto Sans Bengali UI', <CodeunitRange>[
|
||||
CodeunitRange(2404, 2405),
|
||||
CodeunitRange(2433, 2555),
|
||||
CodeunitRange(8204, 8205),
|
||||
CodeunitRange(8377, 8377),
|
||||
CodeunitRange(9676, 9676),
|
||||
]),
|
||||
_NotoFont('Noto Sans Myanmar UI', <CodeunitRange>[
|
||||
CodeunitRange(4096, 4255),
|
||||
CodeunitRange(8204, 8205),
|
||||
CodeunitRange(9676, 9676),
|
||||
]),
|
||||
_NotoFont('Noto Sans Egyptian Hieroglyphs', <CodeunitRange>[
|
||||
CodeunitRange(77824, 78894),
|
||||
]),
|
||||
_NotoFont('Noto Sans Ethiopic', <CodeunitRange>[
|
||||
CodeunitRange(4608, 5017),
|
||||
CodeunitRange(11648, 11742),
|
||||
CodeunitRange(43777, 43822),
|
||||
]),
|
||||
_NotoFont('Noto Sans Georgian', <CodeunitRange>[
|
||||
CodeunitRange(1417, 1417),
|
||||
CodeunitRange(4256, 4351),
|
||||
CodeunitRange(11520, 11567),
|
||||
]),
|
||||
_NotoFont('Noto Sans Gujarati UI', <CodeunitRange>[
|
||||
CodeunitRange(2404, 2405),
|
||||
CodeunitRange(2688, 2815),
|
||||
CodeunitRange(8204, 8205),
|
||||
CodeunitRange(8377, 8377),
|
||||
CodeunitRange(9676, 9676),
|
||||
CodeunitRange(43056, 43065),
|
||||
]),
|
||||
_NotoFont('Noto Sans Gurmukhi UI', <CodeunitRange>[
|
||||
CodeunitRange(2404, 2405),
|
||||
CodeunitRange(2561, 2677),
|
||||
CodeunitRange(8204, 8205),
|
||||
CodeunitRange(8377, 8377),
|
||||
CodeunitRange(9676, 9676),
|
||||
CodeunitRange(9772, 9772),
|
||||
CodeunitRange(43056, 43065),
|
||||
]),
|
||||
_NotoFont('Noto Sans Hebrew', <CodeunitRange>[
|
||||
CodeunitRange(1424, 1535),
|
||||
CodeunitRange(8362, 8362),
|
||||
CodeunitRange(9676, 9676),
|
||||
CodeunitRange(64285, 64335),
|
||||
]),
|
||||
_NotoFont('Noto Sans Devanagari UI', <CodeunitRange>[
|
||||
CodeunitRange(2304, 2431),
|
||||
CodeunitRange(7376, 7414),
|
||||
CodeunitRange(7416, 7417),
|
||||
CodeunitRange(8204, 9205),
|
||||
CodeunitRange(8360, 8360),
|
||||
CodeunitRange(8377, 8377),
|
||||
CodeunitRange(9676, 9676),
|
||||
CodeunitRange(43056, 43065),
|
||||
CodeunitRange(43232, 43259),
|
||||
]),
|
||||
_NotoFont('Noto Sans Kannada UI', <CodeunitRange>[
|
||||
CodeunitRange(2404, 2405),
|
||||
CodeunitRange(3202, 3314),
|
||||
CodeunitRange(8204, 8205),
|
||||
CodeunitRange(8377, 8377),
|
||||
CodeunitRange(9676, 9676),
|
||||
]),
|
||||
_NotoFont('Noto Sans Khmer UI', <CodeunitRange>[
|
||||
CodeunitRange(6016, 6143),
|
||||
CodeunitRange(8204, 8204),
|
||||
CodeunitRange(9676, 9676),
|
||||
]),
|
||||
_NotoFont('Noto Sans KR', <CodeunitRange>[
|
||||
CodeunitRange(12593, 12686),
|
||||
CodeunitRange(12800, 12828),
|
||||
CodeunitRange(12896, 12923),
|
||||
CodeunitRange(44032, 55215),
|
||||
]),
|
||||
_NotoFont('Noto Sans Lao UI', <CodeunitRange>[
|
||||
CodeunitRange(3713, 3807),
|
||||
CodeunitRange(9676, 9676),
|
||||
]),
|
||||
_NotoFont('Noto Sans Malayalam UI', <CodeunitRange>[
|
||||
CodeunitRange(775, 775),
|
||||
CodeunitRange(803, 803),
|
||||
CodeunitRange(2404, 2405),
|
||||
CodeunitRange(3330, 3455),
|
||||
CodeunitRange(8204, 8205),
|
||||
CodeunitRange(8377, 8377),
|
||||
CodeunitRange(9676, 9676),
|
||||
]),
|
||||
_NotoFont('Noto Sans Sinhala', <CodeunitRange>[
|
||||
CodeunitRange(2404, 2405),
|
||||
CodeunitRange(3458, 3572),
|
||||
CodeunitRange(8204, 8205),
|
||||
CodeunitRange(9676, 9676),
|
||||
]),
|
||||
_NotoFont('Noto Sans Tamil UI', <CodeunitRange>[
|
||||
CodeunitRange(2404, 2405),
|
||||
CodeunitRange(2946, 3066),
|
||||
CodeunitRange(8204, 8205),
|
||||
CodeunitRange(8377, 8377),
|
||||
CodeunitRange(9676, 9676),
|
||||
]),
|
||||
_NotoFont('Noto Sans Telugu UI', <CodeunitRange>[
|
||||
CodeunitRange(2385, 2386),
|
||||
CodeunitRange(2404, 2405),
|
||||
CodeunitRange(3072, 3199),
|
||||
CodeunitRange(7386, 7386),
|
||||
CodeunitRange(8204, 8205),
|
||||
CodeunitRange(9676, 9676),
|
||||
]),
|
||||
_NotoFont('Noto Sans Thai UI', <CodeunitRange>[
|
||||
CodeunitRange(3585, 3675),
|
||||
CodeunitRange(8204, 8205),
|
||||
CodeunitRange(9676, 9676),
|
||||
]),
|
||||
_NotoFont('Noto Sans', <CodeunitRange>[
|
||||
CodeunitRange(0, 255),
|
||||
CodeunitRange(305, 305),
|
||||
CodeunitRange(338, 339),
|
||||
CodeunitRange(699, 700),
|
||||
CodeunitRange(710, 710),
|
||||
CodeunitRange(730, 730),
|
||||
CodeunitRange(732, 732),
|
||||
CodeunitRange(8192, 8303),
|
||||
CodeunitRange(8308, 8308),
|
||||
CodeunitRange(8364, 8364),
|
||||
CodeunitRange(8482, 8482),
|
||||
CodeunitRange(8593, 8593),
|
||||
CodeunitRange(8595, 8595),
|
||||
CodeunitRange(8722, 8722),
|
||||
CodeunitRange(8725, 8725),
|
||||
CodeunitRange(65279, 65279),
|
||||
CodeunitRange(65533, 65533),
|
||||
CodeunitRange(1024, 1119),
|
||||
CodeunitRange(1168, 1169),
|
||||
CodeunitRange(1200, 1201),
|
||||
CodeunitRange(8470, 8470),
|
||||
CodeunitRange(1120, 1327),
|
||||
CodeunitRange(7296, 7304),
|
||||
CodeunitRange(8372, 8372),
|
||||
CodeunitRange(11744, 11775),
|
||||
CodeunitRange(42560, 42655),
|
||||
CodeunitRange(65070, 65071),
|
||||
CodeunitRange(880, 1023),
|
||||
CodeunitRange(7936, 8191),
|
||||
CodeunitRange(256, 591),
|
||||
CodeunitRange(601, 601),
|
||||
CodeunitRange(7680, 7935),
|
||||
CodeunitRange(8224, 8224),
|
||||
CodeunitRange(8352, 8363),
|
||||
CodeunitRange(8365, 8399),
|
||||
CodeunitRange(8467, 8467),
|
||||
CodeunitRange(11360, 11391),
|
||||
CodeunitRange(42784, 43007),
|
||||
CodeunitRange(258, 259),
|
||||
CodeunitRange(272, 273),
|
||||
CodeunitRange(296, 297),
|
||||
CodeunitRange(360, 361),
|
||||
CodeunitRange(416, 417),
|
||||
CodeunitRange(431, 432),
|
||||
CodeunitRange(7840, 7929),
|
||||
CodeunitRange(8363, 8363),
|
||||
]),
|
||||
];
|
||||
|
||||
class FallbackFontDownloadQueue {
|
||||
NotoDownloader downloader = NotoDownloader();
|
||||
|
||||
final Set<_ResolvedNotoSubset> downloadedSubsets = <_ResolvedNotoSubset>{};
|
||||
final Set<_ResolvedNotoSubset> pendingSubsets = <_ResolvedNotoSubset>{};
|
||||
|
||||
bool get isPending => pendingSubsets.isNotEmpty;
|
||||
|
||||
void add(_ResolvedNotoSubset subset) {
|
||||
if (downloadedSubsets.contains(subset) || pendingSubsets.contains(subset)) {
|
||||
return;
|
||||
}
|
||||
bool firstInBatch = pendingSubsets.isEmpty;
|
||||
pendingSubsets.add(subset);
|
||||
if (firstInBatch) {
|
||||
Timer.run(startDownloads);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> startDownloads() async {
|
||||
List<Future<void>> downloads = <Future>[];
|
||||
for (_ResolvedNotoSubset subset in pendingSubsets) {
|
||||
downloads.add(Future<void>(() async {
|
||||
ByteBuffer buffer;
|
||||
try {
|
||||
buffer = await downloader.downloadAsBytes(subset.url);
|
||||
} catch (e) {
|
||||
html.window.console
|
||||
.warn('Failed to load font ${subset.family} at ${subset.url}');
|
||||
html.window.console.warn(e);
|
||||
return;
|
||||
}
|
||||
|
||||
final Uint8List bytes = buffer.asUint8List();
|
||||
skiaFontCollection.registerFallbackFont(subset.family, bytes);
|
||||
|
||||
pendingSubsets.remove(subset);
|
||||
downloadedSubsets.add(subset);
|
||||
if (pendingSubsets.isEmpty) {
|
||||
await skiaFontCollection.ensureFontsLoaded();
|
||||
sendFontChangeMessage();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
await Future.wait<void>(downloads);
|
||||
if (pendingSubsets.isNotEmpty) {
|
||||
await startDownloads();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NotoDownloader {
|
||||
/// Downloads the [url] and returns it as a [ByteBuffer].
|
||||
///
|
||||
/// Override this for testing.
|
||||
Future<ByteBuffer> downloadAsBytes(String url) {
|
||||
return html.window.fetch(url).then((dynamic fetchResult) => fetchResult
|
||||
.arrayBuffer()
|
||||
.then<ByteBuffer>((dynamic x) => x as ByteBuffer));
|
||||
}
|
||||
|
||||
/// Downloads the [url] and returns is as a [String].
|
||||
///
|
||||
/// Override this for testing.
|
||||
Future<String> downloadAsString(String url) {
|
||||
return html.window.fetch(url).then((dynamic response) =>
|
||||
response.text().then<String>((dynamic x) => x as String));
|
||||
}
|
||||
}
|
||||
|
||||
/// The Noto font interval tree.
|
||||
IntervalTree<_NotoFont>? _notoTree;
|
||||
|
||||
FallbackFontDownloadQueue notoDownloadQueue = FallbackFontDownloadQueue();
|
||||
@ -22,15 +22,38 @@ class SkiaFontCollection {
|
||||
/// Fonts which have been registered and loaded.
|
||||
final List<_RegisteredFont> _registeredFonts = <_RegisteredFont>[];
|
||||
|
||||
final Set<String?> registeredFamilies = <String?>{};
|
||||
/// Fallback fonts which have been registered and loaded.
|
||||
final List<_RegisteredFont> _registeredFallbackFonts = <_RegisteredFont>[];
|
||||
|
||||
final Map<String, List<SkTypeface>> familyToTypefaceMap =
|
||||
<String, List<SkTypeface>>{};
|
||||
|
||||
final List<String> globalFontFallbacks = <String>[];
|
||||
|
||||
final Map<String, int> _fontFallbackCounts = <String, int>{};
|
||||
|
||||
Future<void> ensureFontsLoaded() async {
|
||||
await _loadFonts();
|
||||
|
||||
if (fontProvider != null) {
|
||||
fontProvider!.delete();
|
||||
fontProvider = null;
|
||||
}
|
||||
fontProvider = canvasKit.TypefaceFontProvider.Make();
|
||||
familyToTypefaceMap.clear();
|
||||
|
||||
for (var font in _registeredFonts) {
|
||||
fontProvider.registerFont(font.bytes, font.flutterFamily);
|
||||
fontProvider!.registerFont(font.bytes, font.family);
|
||||
familyToTypefaceMap
|
||||
.putIfAbsent(font.family, () => <SkTypeface>[])
|
||||
.add(font.typeface);
|
||||
}
|
||||
|
||||
for (var font in _registeredFallbackFonts) {
|
||||
fontProvider!.registerFont(font.bytes, font.family);
|
||||
familyToTypefaceMap
|
||||
.putIfAbsent(font.family, () => <SkTypeface>[])
|
||||
.add(font.typeface);
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,24 +74,16 @@ class SkiaFontCollection {
|
||||
}
|
||||
|
||||
Future<void> loadFontFromList(Uint8List list, {String? fontFamily}) async {
|
||||
String? actualFamily = _readActualFamilyName(list);
|
||||
|
||||
if (actualFamily == null) {
|
||||
if (fontFamily == null) {
|
||||
fontFamily = _readActualFamilyName(list);
|
||||
if (fontFamily == null) {
|
||||
html.window.console
|
||||
.warn('Failed to read font family name. Aborting font load.');
|
||||
return;
|
||||
}
|
||||
actualFamily = fontFamily;
|
||||
}
|
||||
|
||||
if (fontFamily == null) {
|
||||
fontFamily = actualFamily;
|
||||
}
|
||||
|
||||
registeredFamilies.add(fontFamily);
|
||||
|
||||
_registeredFonts.add(_RegisteredFont(list, fontFamily, actualFamily));
|
||||
_registeredFonts.add(_RegisteredFont(list, fontFamily));
|
||||
await ensureFontsLoaded();
|
||||
}
|
||||
|
||||
@ -94,12 +109,16 @@ class SkiaFontCollection {
|
||||
'There was a problem trying to load FontManifest.json');
|
||||
}
|
||||
|
||||
bool registeredRoboto = false;
|
||||
|
||||
for (Map<String, dynamic> fontFamily
|
||||
in fontManifest.cast<Map<String, dynamic>>()) {
|
||||
final String family = fontFamily['family']!;
|
||||
final List<dynamic> fontAssets = fontFamily['fonts'];
|
||||
|
||||
registeredFamilies.add(family);
|
||||
if (family == 'Roboto') {
|
||||
registeredRoboto = true;
|
||||
}
|
||||
|
||||
for (dynamic fontAssetItem in fontAssets) {
|
||||
final Map<String, dynamic> fontAsset = fontAssetItem;
|
||||
@ -112,7 +131,7 @@ class SkiaFontCollection {
|
||||
/// 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 (!registeredFamilies.contains('Roboto')) {
|
||||
if (!registeredRoboto) {
|
||||
// Download Roboto and add it to the font buffers.
|
||||
_unloadedFonts.add(_registerFont(_robotoUrl, 'Roboto'));
|
||||
}
|
||||
@ -129,15 +148,16 @@ class SkiaFontCollection {
|
||||
}
|
||||
|
||||
final Uint8List bytes = buffer.asUint8List();
|
||||
String? actualFamily = _readActualFamilyName(bytes);
|
||||
return _RegisteredFont(bytes, family);
|
||||
}
|
||||
|
||||
if (actualFamily == null) {
|
||||
html.window.console.warn('Failed to determine the actual name of the '
|
||||
'font $family at $url. Defaulting to $family.');
|
||||
actualFamily = family;
|
||||
}
|
||||
|
||||
return _RegisteredFont(bytes, family, actualFamily);
|
||||
void registerFallbackFont(String family, Uint8List bytes) {
|
||||
_fontFallbackCounts.putIfAbsent(family, () => 0);
|
||||
int fontFallbackTag = _fontFallbackCounts[family]!;
|
||||
_fontFallbackCounts[family] = _fontFallbackCounts[family]! + 1;
|
||||
String countedFamily = '$family $fontFallbackTag';
|
||||
_registeredFallbackFonts.add(_RegisteredFont(bytes, countedFamily));
|
||||
globalFontFallbacks.add(countedFamily);
|
||||
}
|
||||
|
||||
String? _readActualFamilyName(Uint8List bytes) {
|
||||
@ -154,20 +174,31 @@ class SkiaFontCollection {
|
||||
.then<ByteBuffer>((dynamic x) => x as ByteBuffer);
|
||||
}
|
||||
|
||||
/// Resets the fallback fonts. Used for tests.
|
||||
void debugResetFallbackFonts() {
|
||||
_registeredFallbackFonts.clear();
|
||||
globalFontFallbacks.clear();
|
||||
_fontFallbackCounts.clear();
|
||||
}
|
||||
|
||||
SkFontMgr? skFontMgr;
|
||||
late TypefaceFontProvider fontProvider;
|
||||
TypefaceFontProvider? fontProvider;
|
||||
}
|
||||
|
||||
/// Represents a font that has been registered.
|
||||
class _RegisteredFont {
|
||||
/// The font family that the font was declared to have by Flutter.
|
||||
final String flutterFamily;
|
||||
/// The font family name for this font.
|
||||
final String family;
|
||||
|
||||
/// The byte data for this font.
|
||||
final Uint8List bytes;
|
||||
|
||||
/// The font family that was parsed from the font's bytes.
|
||||
final String actualFamily;
|
||||
/// The [SkTypeface] created from this font's [bytes].
|
||||
///
|
||||
/// This is used to determine which code points are supported by this font.
|
||||
final SkTypeface typeface;
|
||||
|
||||
_RegisteredFont(this.bytes, this.flutterFamily, this.actualFamily);
|
||||
_RegisteredFont(this.bytes, this.family)
|
||||
: this.typeface =
|
||||
canvasKit.FontMgr.RefDefault().MakeTypefaceFromData(bytes);
|
||||
}
|
||||
|
||||
@ -40,7 +40,10 @@ const bool _autoDetect =
|
||||
const bool _useSkia =
|
||||
bool.fromEnvironment('FLUTTER_WEB_USE_SKIA', defaultValue: false);
|
||||
|
||||
// If set to true, forces CPU-only rendering (i.e. no WebGL).
|
||||
/// If set to true, forces CPU-only rendering (i.e. no WebGL).
|
||||
///
|
||||
/// This is mainly used for testing or for apps that want to ensure they
|
||||
/// run on devices which don't support WebGL.
|
||||
const bool canvasKitForceCpuOnly = bool.fromEnvironment(
|
||||
'FLUTTER_WEB_CANVASKIT_FORCE_CPU_ONLY',
|
||||
defaultValue: false);
|
||||
|
||||
@ -0,0 +1,114 @@
|
||||
// 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.
|
||||
|
||||
// @dart = 2.12
|
||||
part of engine;
|
||||
|
||||
/// A tree which stores a set of intervals that can be queried for intersection.
|
||||
class IntervalTree<T> {
|
||||
/// The root node of the interval tree.
|
||||
final IntervalTreeNode<T> root;
|
||||
|
||||
IntervalTree._(this.root);
|
||||
|
||||
/// Creates an interval tree from a mapping of [T] values to a list of ranges.
|
||||
///
|
||||
/// 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<T, List<CodeunitRange>> rangesMap) {
|
||||
// Get a list of all the ranges ordered by start index.
|
||||
List<IntervalTreeNode<T>> intervals = <IntervalTreeNode<T>>[];
|
||||
rangesMap.forEach((T key, List<CodeunitRange> rangeList) {
|
||||
for (CodeunitRange range in rangeList) {
|
||||
intervals.add(IntervalTreeNode<T>(key, range.start, range.end));
|
||||
}
|
||||
});
|
||||
|
||||
intervals
|
||||
.sort((IntervalTreeNode<T> a, IntervalTreeNode<T> b) => a.low - b.low);
|
||||
|
||||
// Make a balanced binary search tree from the nodes sorted by low value.
|
||||
IntervalTreeNode<T>? _makeBalancedTree(List<IntervalTreeNode<T>> nodes) {
|
||||
if (nodes.length == 0) {
|
||||
return null;
|
||||
}
|
||||
if (nodes.length == 1) {
|
||||
return nodes.single;
|
||||
}
|
||||
int mid = nodes.length ~/ 2;
|
||||
IntervalTreeNode<T> root = nodes[mid];
|
||||
root.left = _makeBalancedTree(nodes.sublist(0, mid));
|
||||
root.right = _makeBalancedTree(nodes.sublist(mid + 1));
|
||||
return root;
|
||||
}
|
||||
|
||||
// Given a node, computes the highest `high` point of all of the subnodes.
|
||||
//
|
||||
// As a side effect, this also computes the high point of all subnodes.
|
||||
void _computeHigh(IntervalTreeNode<T> root) {
|
||||
if (root.left == null && root.right == null) {
|
||||
root.computedHigh = root.high;
|
||||
} else if (root.left == null) {
|
||||
_computeHigh(root.right!);
|
||||
root.computedHigh = math.max(root.high, root.right!.computedHigh);
|
||||
} else if (root.right == null) {
|
||||
_computeHigh(root.left!);
|
||||
root.computedHigh = math.max(root.high, root.left!.computedHigh);
|
||||
} else {
|
||||
_computeHigh(root.right!);
|
||||
_computeHigh(root.left!);
|
||||
root.computedHigh = math.max(
|
||||
root.high,
|
||||
math.max(
|
||||
root.left!.computedHigh,
|
||||
root.right!.computedHigh,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
IntervalTreeNode<T> root = _makeBalancedTree(intervals)!;
|
||||
_computeHigh(root);
|
||||
|
||||
return IntervalTree._(root);
|
||||
}
|
||||
|
||||
/// Returns the list of objects which have been associated with intervals that
|
||||
/// intersect with [x].
|
||||
List<T> intersections(int x) {
|
||||
List<T> results = <T>[];
|
||||
root.searchForPoint(x, results);
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
class IntervalTreeNode<T> {
|
||||
final T value;
|
||||
final int low;
|
||||
final int high;
|
||||
int computedHigh;
|
||||
|
||||
IntervalTreeNode<T>? left;
|
||||
IntervalTreeNode<T>? right;
|
||||
|
||||
IntervalTreeNode(this.value, this.low, this.high) : computedHigh = high;
|
||||
|
||||
bool contains(int x) {
|
||||
return low <= x && x <= high;
|
||||
}
|
||||
|
||||
// Searches the tree rooted at this node for all T containing [x].
|
||||
void searchForPoint(int x, List<T> result) {
|
||||
if (x > computedHigh) {
|
||||
return;
|
||||
}
|
||||
left?.searchForPoint(x, result);
|
||||
if (this.contains(x)) {
|
||||
result.add(value);
|
||||
}
|
||||
if (x < low) {
|
||||
return;
|
||||
}
|
||||
right?.searchForPoint(x, result);
|
||||
}
|
||||
}
|
||||
@ -62,11 +62,7 @@ class CkParagraphStyle implements ui.ParagraphStyle {
|
||||
skTextStyle.fontSize = fontSize;
|
||||
}
|
||||
|
||||
if (fontFamily == null ||
|
||||
!skiaFontCollection.registeredFamilies.contains(fontFamily)) {
|
||||
fontFamily = 'Roboto';
|
||||
}
|
||||
skTextStyle.fontFamilies = [fontFamily];
|
||||
skTextStyle.fontFamilies = _getEffectiveFontFamilies(fontFamily);
|
||||
|
||||
return skTextStyle;
|
||||
}
|
||||
@ -74,20 +70,8 @@ class CkParagraphStyle implements ui.ParagraphStyle {
|
||||
static SkStrutStyleProperties toSkStrutStyleProperties(ui.StrutStyle value) {
|
||||
EngineStrutStyle style = value as EngineStrutStyle;
|
||||
final SkStrutStyleProperties skStrutStyle = SkStrutStyleProperties();
|
||||
if (style._fontFamily != null) {
|
||||
String fontFamily = style._fontFamily!;
|
||||
if (!skiaFontCollection.registeredFamilies.contains(fontFamily)) {
|
||||
fontFamily = 'Roboto';
|
||||
}
|
||||
final List<String> fontFamilies = <String>[fontFamily];
|
||||
if (style._fontFamilyFallback != null) {
|
||||
fontFamilies.addAll(style._fontFamilyFallback!);
|
||||
}
|
||||
skStrutStyle.fontFamilies = fontFamilies;
|
||||
} else {
|
||||
// If no strut font family is given, default to Roboto.
|
||||
skStrutStyle.fontFamilies = ['Roboto'];
|
||||
}
|
||||
skStrutStyle.fontFamilies =
|
||||
_getEffectiveFontFamilies(style._fontFamily, style._fontFamilyFallback);
|
||||
|
||||
if (style._fontSize != null) {
|
||||
skStrutStyle.fontSize = style._fontSize;
|
||||
@ -279,18 +263,8 @@ class CkTextStyle implements ui.TextStyle {
|
||||
properties.locale = locale.toLanguageTag();
|
||||
}
|
||||
|
||||
if (fontFamily == null ||
|
||||
!skiaFontCollection.registeredFamilies.contains(fontFamily)) {
|
||||
fontFamily = 'Roboto';
|
||||
}
|
||||
|
||||
List<String> fontFamilies = <String>[fontFamily];
|
||||
if (fontFamilyFallback != null &&
|
||||
!fontFamilyFallback.every((font) => fontFamily == font)) {
|
||||
fontFamilies.addAll(fontFamilyFallback);
|
||||
}
|
||||
|
||||
properties.fontFamilies = fontFamilies;
|
||||
properties.fontFamilies =
|
||||
_getEffectiveFontFamilies(fontFamily, fontFamilyFallback);
|
||||
|
||||
if (fontWeight != null || fontStyle != null) {
|
||||
properties.fontStyle = toSkFontStyle(fontWeight, fontStyle);
|
||||
@ -564,7 +538,6 @@ class CkParagraph extends ManagedSkiaObject<SkParagraph>
|
||||
|
||||
@override
|
||||
void layout(ui.ParagraphConstraints constraints) {
|
||||
assert(constraints.width != null); // ignore: unnecessary_null_comparison
|
||||
_lastLayoutConstraints = constraints;
|
||||
|
||||
// TODO(het): CanvasKit throws an exception when laid out with
|
||||
@ -667,8 +640,64 @@ class CkParagraphBuilder implements ui.ParagraphBuilder {
|
||||
return properties;
|
||||
}
|
||||
|
||||
/// Determines if the given [text] contains any code points which are not
|
||||
/// supported by the current set of fonts.
|
||||
void _ensureFontsSupportText(String text) {
|
||||
// TODO(hterkelsen): Make this faster for the common case where the text
|
||||
// is supported by the given fonts.
|
||||
|
||||
// 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;
|
||||
}
|
||||
CkTextStyle style = _peekStyle();
|
||||
List<String> fontFamilies =
|
||||
_getEffectiveFontFamilies(style.fontFamily, style.fontFamilyFallback);
|
||||
List<SkTypeface> typefaces = <SkTypeface>[];
|
||||
for (var font in fontFamilies) {
|
||||
List<SkTypeface>? typefacesForFamily =
|
||||
skiaFontCollection.familyToTypefaceMap[font];
|
||||
if (typefacesForFamily != null) {
|
||||
typefaces.addAll(typefacesForFamily);
|
||||
}
|
||||
}
|
||||
List<bool> codeUnitsSupported = List<bool>.filled(text.length, false);
|
||||
for (SkTypeface typeface in typefaces) {
|
||||
SkFont font = SkFont(typeface);
|
||||
Uint8List glyphs = font.getGlyphIDs(text);
|
||||
assert(glyphs.length == codeUnitsSupported.length);
|
||||
for (int i = 0; i < glyphs.length; i++) {
|
||||
codeUnitsSupported[i] |=
|
||||
glyphs[i] != 0 || _isControlCode(text.codeUnitAt(i));
|
||||
}
|
||||
}
|
||||
|
||||
if (codeUnitsSupported.any((x) => !x)) {
|
||||
List<int> missingCodeUnits = <int>[];
|
||||
for (int i = 0; i < codeUnitsSupported.length; i++) {
|
||||
if (!codeUnitsSupported[i]) {
|
||||
missingCodeUnits.add(text.codeUnitAt(i));
|
||||
}
|
||||
}
|
||||
_findFontsForMissingCodeunits(missingCodeUnits);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns [true] if [codepoint] is a Unicode control code.
|
||||
bool _isControlCode(int codepoint) {
|
||||
return codepoint < 32 || (codepoint > 127 && codepoint < 160);
|
||||
}
|
||||
|
||||
@override
|
||||
void addText(String text) {
|
||||
_ensureFontsSupportText(text);
|
||||
_commands.add(_ParagraphCommand.addText(text));
|
||||
_paragraphBuilder.addText(text);
|
||||
}
|
||||
@ -719,8 +748,10 @@ class CkParagraphBuilder implements ui.ParagraphBuilder {
|
||||
_styleStack.add(skStyle);
|
||||
_commands.add(_ParagraphCommand.pushStyle(ckStyle));
|
||||
if (skStyle.foreground != null || skStyle.background != null) {
|
||||
final SkPaint foreground = skStyle.foreground?.skiaObject ?? _defaultTextStylePaint;
|
||||
final SkPaint background = skStyle.background?.skiaObject ?? _defaultTextStylePaint;
|
||||
final SkPaint foreground =
|
||||
skStyle.foreground?.skiaObject ?? _defaultTextStylePaint;
|
||||
final SkPaint background =
|
||||
skStyle.background?.skiaObject ?? _defaultTextStylePaint;
|
||||
_paragraphBuilder.pushPaintStyle(
|
||||
skStyle.skTextStyle, foreground, background);
|
||||
} else {
|
||||
@ -779,3 +810,19 @@ enum _ParagraphCommandType {
|
||||
pushStyle,
|
||||
addPlaceholder,
|
||||
}
|
||||
|
||||
List<String> _getEffectiveFontFamilies(String? fontFamily,
|
||||
[List<String>? fontFamilyFallback]) {
|
||||
List<String> fontFamilies = <String>[];
|
||||
if (fontFamily == null) {
|
||||
fontFamilies.add('Roboto');
|
||||
} else {
|
||||
fontFamilies.add(fontFamily);
|
||||
}
|
||||
if (fontFamilyFallback != null &&
|
||||
!fontFamilyFallback.every((font) => fontFamily == font)) {
|
||||
fontFamilies.addAll(fontFamilyFallback);
|
||||
}
|
||||
fontFamilies.addAll(skiaFontCollection.globalFontFallbacks);
|
||||
return fontFamilies;
|
||||
}
|
||||
|
||||
@ -597,45 +597,45 @@ int clampInt(int value, int min, int max) {
|
||||
}
|
||||
|
||||
ui.Rect computeBoundingRectangleFromMatrix(Matrix4 transform, ui.Rect rect) {
|
||||
final Float32List m = transform.storage;
|
||||
// Apply perspective transform to all 4 corners. Can't use left,top, bottom,
|
||||
// right since for example rotating 45 degrees would yield inaccurate size.
|
||||
double x = rect.left;
|
||||
double y = rect.top;
|
||||
double wp = 1.0 / ((m[3] * x) + (m[7] * y) + m[15]);
|
||||
double xp = ((m[0] * x) + (m[4] * y) + m[12]) * wp;
|
||||
double yp = ((m[1] * x) + (m[5] * y) + m[13]) * wp;
|
||||
double minX = xp, maxX = xp;
|
||||
double minY =yp, maxY = yp;
|
||||
x = rect.right;
|
||||
y = rect.bottom;
|
||||
wp = 1.0 / ((m[3] * x) + (m[7] * y) + m[15]);
|
||||
xp = ((m[0] * x) + (m[4] * y) + m[12]) * wp;
|
||||
yp = ((m[1] * x) + (m[5] * y) + m[13]) * wp;
|
||||
final Float32List m = transform.storage;
|
||||
// Apply perspective transform to all 4 corners. Can't use left,top, bottom,
|
||||
// right since for example rotating 45 degrees would yield inaccurate size.
|
||||
double x = rect.left;
|
||||
double y = rect.top;
|
||||
double wp = 1.0 / ((m[3] * x) + (m[7] * y) + m[15]);
|
||||
double xp = ((m[0] * x) + (m[4] * y) + m[12]) * wp;
|
||||
double yp = ((m[1] * x) + (m[5] * y) + m[13]) * wp;
|
||||
double minX = xp, maxX = xp;
|
||||
double minY = yp, maxY = yp;
|
||||
x = rect.right;
|
||||
y = rect.bottom;
|
||||
wp = 1.0 / ((m[3] * x) + (m[7] * y) + m[15]);
|
||||
xp = ((m[0] * x) + (m[4] * y) + m[12]) * wp;
|
||||
yp = ((m[1] * x) + (m[5] * y) + m[13]) * wp;
|
||||
|
||||
minX = math.min(minX, xp);
|
||||
maxX = math.max(maxX, xp);
|
||||
minY = math.min(minY, yp);
|
||||
maxY = math.max(maxY, yp);
|
||||
minX = math.min(minX, xp);
|
||||
maxX = math.max(maxX, xp);
|
||||
minY = math.min(minY, yp);
|
||||
maxY = math.max(maxY, yp);
|
||||
|
||||
x = rect.left;
|
||||
y = rect.bottom;
|
||||
wp = 1.0 / ((m[3] * x) + (m[7] * y) + m[15]);
|
||||
xp = ((m[0] * x) + (m[4] * y) + m[12]) * wp;
|
||||
yp = ((m[1] * x) + (m[5] * y) + m[13]) * wp;
|
||||
minX = math.min(minX, xp);
|
||||
maxX = math.max(maxX, xp);
|
||||
minY = math.min(minY, yp);
|
||||
maxY = math.max(maxY, yp);
|
||||
x = rect.left;
|
||||
y = rect.bottom;
|
||||
wp = 1.0 / ((m[3] * x) + (m[7] * y) + m[15]);
|
||||
xp = ((m[0] * x) + (m[4] * y) + m[12]) * wp;
|
||||
yp = ((m[1] * x) + (m[5] * y) + m[13]) * wp;
|
||||
minX = math.min(minX, xp);
|
||||
maxX = math.max(maxX, xp);
|
||||
minY = math.min(minY, yp);
|
||||
maxY = math.max(maxY, yp);
|
||||
|
||||
x = rect.right;
|
||||
y = rect.top;
|
||||
wp = 1.0 / ((m[3] * x) + (m[7] * y) + m[15]);
|
||||
xp = ((m[0] * x) + (m[4] * y) + m[12]) * wp;
|
||||
yp = ((m[1] * x) + (m[5] * y) + m[13]) * wp;
|
||||
minX = math.min(minX, xp);
|
||||
maxX = math.max(maxX, xp);
|
||||
minY = math.min(minY, yp);
|
||||
maxY = math.max(maxY, yp);
|
||||
return ui.Rect.fromLTWH(minX, minY, maxX-minX, maxY-minY);
|
||||
}
|
||||
x = rect.right;
|
||||
y = rect.top;
|
||||
wp = 1.0 / ((m[3] * x) + (m[7] * y) + m[15]);
|
||||
xp = ((m[0] * x) + (m[4] * y) + m[12]) * wp;
|
||||
yp = ((m[1] * x) + (m[5] * y) + m[13]) * wp;
|
||||
minX = math.min(minX, xp);
|
||||
maxX = math.max(maxX, xp);
|
||||
minY = math.min(minY, yp);
|
||||
maxY = math.max(maxY, yp);
|
||||
return ui.Rect.fromLTWH(minX, minY, maxX - minX, maxY - minY);
|
||||
}
|
||||
|
||||
@ -0,0 +1,155 @@
|
||||
// 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.
|
||||
|
||||
// @dart = 2.12
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:ui/ui.dart' as ui;
|
||||
import 'package:ui/src/engine.dart';
|
||||
|
||||
import 'package:test/test.dart';
|
||||
import 'package:test/bootstrap/browser.dart';
|
||||
import 'package:web_engine_tester/golden_tester.dart';
|
||||
|
||||
import 'common.dart';
|
||||
|
||||
void main() {
|
||||
internalBootstrapBrowserTest(() => testMain);
|
||||
}
|
||||
|
||||
const ui.Rect kDefaultRegion = const ui.Rect.fromLTRB(0, 0, 500, 250);
|
||||
|
||||
Future<void> matchPictureGolden(String goldenFile, CkPicture picture,
|
||||
{ui.Rect region = kDefaultRegion, bool write = false}) async {
|
||||
final EnginePlatformDispatcher dispatcher =
|
||||
ui.window.platformDispatcher as EnginePlatformDispatcher;
|
||||
final LayerSceneBuilder sb = LayerSceneBuilder();
|
||||
sb.pushOffset(0, 0);
|
||||
sb.addPicture(ui.Offset.zero, picture);
|
||||
dispatcher.rasterizer!.draw(sb.build().layerTree);
|
||||
await matchGoldenFile(goldenFile,
|
||||
region: region, maxDiffRatePercent: 0.0, write: write);
|
||||
}
|
||||
|
||||
void testMain() {
|
||||
group('Font fallbacks', () {
|
||||
setUpCanvasKitTest();
|
||||
|
||||
/// Used to save and restore [ui.window.onPlatformMessage] after each test.
|
||||
ui.PlatformMessageCallback? savedCallback;
|
||||
|
||||
setUp(() {
|
||||
notoDownloadQueue.downloader = TestDownloader();
|
||||
TestDownloader.mockDownloads.clear();
|
||||
savedCallback = ui.window.onPlatformMessage;
|
||||
skiaFontCollection.debugResetFallbackFonts();
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
ui.window.onPlatformMessage = savedCallback;
|
||||
});
|
||||
|
||||
test('will download Noto Naskh Arabic if Arabic text is added', () async {
|
||||
final Completer<void> fontChangeCompleter = Completer<void>();
|
||||
// Intercept the system font change message.
|
||||
ui.window.onPlatformMessage = (String name, ByteData? data,
|
||||
ui.PlatformMessageResponseCallback? callback) {
|
||||
if (name == 'flutter/system') {
|
||||
const JSONMessageCodec codec = JSONMessageCodec();
|
||||
final dynamic message = codec.decodeMessage(data);
|
||||
if (message is Map) {
|
||||
if (message['type'] == 'fontsChange') {
|
||||
fontChangeCompleter.complete();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (savedCallback != null) {
|
||||
savedCallback!(name, data, callback);
|
||||
}
|
||||
};
|
||||
|
||||
TestDownloader.mockDownloads[
|
||||
'https://fonts.googleapis.com/css2?family=Noto+Naskh+Arabic+UI'] =
|
||||
'''
|
||||
/* arabic */
|
||||
@font-face {
|
||||
font-family: 'Noto Naskh Arabic UI';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(packages/ui/assets/NotoNaskhArabic-Regular.ttf) format('ttf');
|
||||
unicode-range: U+0600-06FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE80-FEFC;
|
||||
}
|
||||
''';
|
||||
|
||||
expect(skiaFontCollection.globalFontFallbacks, isEmpty);
|
||||
|
||||
// Creating this paragraph should cause us to start to download the
|
||||
// fallback font.
|
||||
CkParagraphBuilder pb = CkParagraphBuilder(
|
||||
CkParagraphStyle(),
|
||||
);
|
||||
pb.addText('مرحبا');
|
||||
|
||||
await fontChangeCompleter.future;
|
||||
|
||||
expect(skiaFontCollection.globalFontFallbacks,
|
||||
contains('Noto Naskh Arabic UI 0'));
|
||||
|
||||
final CkPictureRecorder recorder = CkPictureRecorder();
|
||||
final CkCanvas canvas = recorder.beginRecording(kDefaultRegion);
|
||||
|
||||
pb = CkParagraphBuilder(
|
||||
CkParagraphStyle(
|
||||
fontSize: 32,
|
||||
),
|
||||
);
|
||||
pb.addText('مرحبا');
|
||||
final CkParagraph paragraph = pb.build();
|
||||
paragraph.layout(ui.ParagraphConstraints(width: 1000));
|
||||
|
||||
canvas.drawParagraph(paragraph, ui.Offset(200, 120));
|
||||
|
||||
await matchPictureGolden(
|
||||
'canvaskit_font_fallback_arabic.png', recorder.endRecording());
|
||||
// TODO: https://github.com/flutter/flutter/issues/60040
|
||||
// TODO: https://github.com/flutter/flutter/issues/71520
|
||||
}, skip: isIosSafari || isFirefox);
|
||||
|
||||
test('will gracefully fail if we cannot parse the Google Fonts CSS',
|
||||
() async {
|
||||
TestDownloader.mockDownloads[
|
||||
'https://fonts.googleapis.com/css2?family=Noto+Naskh+Arabic+UI'] =
|
||||
'invalid CSS... this should cause our parser to fail';
|
||||
|
||||
expect(skiaFontCollection.globalFontFallbacks, isEmpty);
|
||||
|
||||
// Creating this paragraph should cause us to start to download the
|
||||
// fallback font.
|
||||
CkParagraphBuilder pb = CkParagraphBuilder(
|
||||
CkParagraphStyle(),
|
||||
);
|
||||
pb.addText('مرحبا');
|
||||
|
||||
// Flush microtasks and test that we didn't start any downloads.
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
expect(notoDownloadQueue.isPending, isFalse);
|
||||
expect(skiaFontCollection.globalFontFallbacks, isEmpty);
|
||||
});
|
||||
// TODO: https://github.com/flutter/flutter/issues/60040
|
||||
}, skip: isIosSafari);
|
||||
}
|
||||
|
||||
class TestDownloader extends NotoDownloader {
|
||||
static final Map<String, String> mockDownloads = <String, String>{};
|
||||
@override
|
||||
Future<String> downloadAsString(String url) async {
|
||||
if (mockDownloads.containsKey(url)) {
|
||||
return mockDownloads[url]!;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,85 @@
|
||||
// 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.
|
||||
|
||||
// @dart = 2.12
|
||||
|
||||
import 'package:ui/src/engine.dart';
|
||||
|
||||
import 'package:test/test.dart';
|
||||
import 'package:test/bootstrap/browser.dart';
|
||||
|
||||
void main() {
|
||||
internalBootstrapBrowserTest(() => testMain);
|
||||
}
|
||||
|
||||
void testMain() {
|
||||
group('$IntervalTree', () {
|
||||
test('is balanced', () {
|
||||
var ranges = <String, List<CodeunitRange>>{
|
||||
'A': [CodeunitRange(0, 5), CodeunitRange(6, 10)],
|
||||
'B': [CodeunitRange(4, 6)],
|
||||
};
|
||||
|
||||
// Should create a balanced 3-node tree with a root with a left and right
|
||||
// child.
|
||||
var tree = IntervalTree<String>.createFromRanges(ranges);
|
||||
var root = tree.root;
|
||||
expect(root.left, isNotNull);
|
||||
expect(root.right, isNotNull);
|
||||
expect(root.left!.left, isNull);
|
||||
expect(root.left!.right, isNull);
|
||||
expect(root.right!.left, isNull);
|
||||
expect(root.right!.right, isNull);
|
||||
|
||||
// Should create a balanced 15-node tree (4 layers deep).
|
||||
var ranges2 = <String, List<CodeunitRange>>{
|
||||
'A': [
|
||||
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),
|
||||
],
|
||||
};
|
||||
|
||||
// Should create a balanced 3-node tree with a root with a left and right
|
||||
// child.
|
||||
var tree2 = IntervalTree<String>.createFromRanges(ranges2);
|
||||
var root2 = tree2.root;
|
||||
|
||||
expect(root2.left!.left!.left, isNotNull);
|
||||
expect(root2.left!.left!.right, isNotNull);
|
||||
expect(root2.left!.right!.left, isNotNull);
|
||||
expect(root2.left!.right!.right, isNotNull);
|
||||
expect(root2.right!.left!.left, isNotNull);
|
||||
expect(root2.right!.left!.right, isNotNull);
|
||||
expect(root2.right!.right!.left, isNotNull);
|
||||
expect(root2.right!.right!.right, isNotNull);
|
||||
});
|
||||
|
||||
test('finds values whose intervals overlap with a given point', () {
|
||||
var ranges = <String, List<CodeunitRange>>{
|
||||
'A': [CodeunitRange(0, 5), CodeunitRange(7, 10)],
|
||||
'B': [CodeunitRange(4, 6)],
|
||||
};
|
||||
var tree = IntervalTree<String>.createFromRanges(ranges);
|
||||
|
||||
expect(tree.intersections(1), ['A']);
|
||||
expect(tree.intersections(4), ['A', 'B']);
|
||||
expect(tree.intersections(6), ['B']);
|
||||
expect(tree.intersections(7), ['A']);
|
||||
expect(tree.intersections(11), <String>[]);
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user