part of dart._engine; // 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. typedef SegmentationResult = ({Uint32List words, Uint32List graphemes, Uint32List breaks}); // The cache numbers below were picked based on the following logic. // // Most paragraphs in an app are small (e.g. icons, button labels, etc). These // paragraphs are also cheap to cache. So we cache a lot of them. 100,000 of // them amounts to a worst case of 5MB (10-character long text + words uint list // + graphemes uint list + breaks uint list). // // Large paragraphs are less common (a handful per page), but are expensive to // cache. So we cache fewer of them. 20 of them at a length of 50,000 characters // amount to a memory usage of 5MB (50,000-character long text + words uint list // + graphemes uint list + breaks uint list). // // Medium paragraphs are somewhere in between. 10,000 of them amount to a worst // case of 5MB (100-character long text + words uint list + graphemes uint list // + breaks uint list). typedef SegmentationCacheSpec = ({int cacheSize, int maxTextLength}); const SegmentationCacheSpec kSmallParagraphCacheSpec = (cacheSize: 100000, maxTextLength: 10); const SegmentationCacheSpec kMediumParagraphCacheSpec = (cacheSize: 10000, maxTextLength: 100); const SegmentationCacheSpec kLargeParagraphCacheSpec = (cacheSize: 20, maxTextLength: 50000); typedef SegmentationCache = ({ LruCache small, LruCache medium, LruCache large, }); /// Caches segmentation results for small, medium and large paragraphts. /// /// Paragraphs are frequently re-created because of style or font changes, while /// their text contents remain the same. This cache is effective at /// short-circuiting the segmentation of such paragraphs. final SegmentationCache segmentationCache = ( small: LruCache(kSmallParagraphCacheSpec.cacheSize), medium: LruCache(kMediumParagraphCacheSpec.cacheSize), large: LruCache(kLargeParagraphCacheSpec.cacheSize), ); extension SegmentationCacheExtensions on SegmentationCache { /// Gets the appropriate cache for the given [text]. LruCache? getCacheForText(String text) { if (text.length <= kSmallParagraphCacheSpec.maxTextLength) { return small; } if (text.length <= kMediumParagraphCacheSpec.maxTextLength) { return medium; } if (text.length <= kLargeParagraphCacheSpec.maxTextLength) { return large; } return null; } /// Clears all the caches. void clear() { small.clear(); medium.clear(); large.clear(); } } /// Injects required ICU data into the [builder]. /// /// This should only be used with the CanvasKit Chromium variant that's compiled /// without ICU data. void injectClientICU(SkParagraphBuilder builder) { assert( canvasKit.ParagraphBuilder.RequiresClientICU(), 'This method should only be used with the CanvasKit Chromium variant.', ); final SegmentationResult result = segmentText(builder.getText()); builder.setWordsUtf16(result.words); builder.setGraphemeBreaksUtf16(result.graphemes); builder.setLineBreaksUtf16(result.breaks); } /// Segments the [text] into words, graphemes and line breaks. /// /// Caches results in [segmentationCache]. SegmentationResult segmentText(String text) { final LruCache? cache = segmentationCache.getCacheForText(text); final SegmentationResult? cachedResult = cache?[text]; final SegmentationResult result; if (cachedResult != null) { result = cachedResult; } else { result = ( words: fragmentUsingIntlSegmenter(text, IntlSegmenterGranularity.word), graphemes: fragmentUsingIntlSegmenter(text, IntlSegmenterGranularity.grapheme), breaks: fragmentUsingV8LineBreaker(text), ); } // Save or promote to most recently used. cache?.cache(text, result); return result; } /// The granularity at which to segment text. /// /// To find all supported granularities, see: /// - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter/Segmenter enum IntlSegmenterGranularity { grapheme, word } final Map _intlSegmenters = { IntlSegmenterGranularity.grapheme: createIntlSegmenter(granularity: 'grapheme'), IntlSegmenterGranularity.word: createIntlSegmenter(granularity: 'word'), }; Uint32List fragmentUsingIntlSegmenter(String text, IntlSegmenterGranularity granularity) { final DomSegmenter segmenter = _intlSegmenters[granularity]!; final DomIteratorWrapper iterator = segmenter.segment(text).iterator(); final List breaks = []; while (iterator.moveNext()) { breaks.add(iterator.current.index); } breaks.add(text.length); return Uint32List.fromList(breaks); } // These are the soft/hard line break values expected by Skia's SkParagraph. const int _kSoftLineBreak = 0; const int _kHardLineBreak = 1; final DomV8BreakIterator _v8LineBreaker = createV8BreakIterator(); Uint32List fragmentUsingV8LineBreaker(String text) { final List fragments = breakLinesUsingV8BreakIterator( text, text.toJS, _v8LineBreaker, ); final int size = (fragments.length + 1) * 2; final Uint32List typedArray = Uint32List(size); typedArray[0] = 0; // start index typedArray[1] = _kSoftLineBreak; // break type for (int i = 0; i < fragments.length; i++) { final LineBreakFragment fragment = fragments[i]; final int uint32Index = 2 + i * 2; typedArray[uint32Index] = fragment.end; typedArray[uint32Index + 1] = fragment.type == LineBreakType.mandatory ? _kHardLineBreak : _kSoftLineBreak; } return typedArray; }